Compare commits

..

146 Commits

Author SHA1 Message Date
Richard Feldman
97c1abbeae Improve docs for max image sizes 2025-07-02 16:23:20 -04:00
Richard Feldman
fe905f1fd2 wip 2025-07-02 11:06:07 -04:00
Richard Feldman
4c442a66b0 Reject images that are too big 2025-06-30 18:27:17 -04:00
Richard Feldman
f38c6e9bd0 Introduce max_image_size 2025-06-30 16:44:02 -04:00
Conrad Irwin
a2e786e0f9 Allow repeat in visual mode (#33569)
Release Notes:

- vim: Allow `.` in visual mode.
2025-06-30 14:04:28 -06:00
Alejandro Fernández Gómez
b0086b472f Fix an interaction between vim's linewise yank and editor's paste (#33555)
Closes #32397

This PR fixes an issue when pasting text with the `editor::Paste`
command that was copied with `vim::Yank`'s linewise selection.

The change stops setting the `is_entire_line` setting when copying from
with vim linewise selections (<kbd>⇧v</kbd>) and motions (i.e.
<kbd>y2j</kbd>).

This flag is used when cutting/copying text without being selected (so,
place a cursor on line without selecting anything, and press
<kbd>⌘X</kbd>). When cutting/copying text in this manner, [the editor
pastes the text above the
cursor](36941253ee/crates/editor/src/editor.rs (L11936-L11947)).
However, this behaviour is not needed when cutting/copying with vim
motions.

Pasting with vim operations is not affected by this change. [They are
handled
elsewhere](36941253ee/crates/vim/src/normal/paste.rs)
and they don't consider the `is_entire_line` flag at all.

Note for maintainers: I'm not familiar with this codebase 🙃. This change
fixes the issue. I don't see anything breaking... but let me know if
it's not the case and a more thorough change is needed.

**Before:**

The text is copied above the first line, before the cursor.


https://github.com/user-attachments/assets/0c2f111a-5da0-4775-a7a0-2e4fb6f78bfc


**After:**
The text is copied at the cursor location:


https://github.com/user-attachments/assets/60a17985-fe8b-4149-a77b-d72bf531bf85


Release Notes:

- Fixed an issue when pasting text that was yanked with vim's linewise
selections.
2025-06-30 14:03:55 -06:00
fantacell
d10cc13924 helix: Add more tests (#33582)
These tests cover more edge cases

Release Notes:

- N/A
2025-06-30 13:57:20 -06:00
Alvaro Parker
2680a78f9c Support vim-mode in git commit editor (#33222)
Release Notes:

- Added support for vim-mode on git commit editor (modal included)

Side notes: 
- Maybe in the future (or even on this PR) a config could be added to
let the user choose whether to enable vim-mode on this editor or not?
And on the agent message editor as well.
2025-06-30 13:55:45 -06:00
Kirill Bulatov
197828980c Properly register initialized default prettier (#33669)
Stop doing useless prettier-related work when doing a project search.

Before, project search might cause

<img width="1728" alt="not_pretty"
src="https://github.com/user-attachments/assets/5f8b935f-962d-488e-984f-50dfbaee97ba"
/>

but now we debounce the prettier-related task first, and actually set
the "installed" state for the default prettier, when there's no install
needed.

Release Notes:

- N/A
2025-06-30 19:08:50 +00:00
Conrad Taylor
7c4da37322 emmet: Fix expansion for HEEx and H sigil files (#32208)
Closes #14149

Release Notes:

- Added support for the Emmet LSP in Elixir heex files
2025-06-30 12:45:10 -04:00
Julia Ryan
ce164f5e65 Remove ruby debug adapter (#33541)
Now that the extension version has been bumped we can remove our in-tree
one to avoid having duplicate debug adapters.

Release Notes:

- The ruby debug adapter has been moved to the [ruby
extension](https://github.com/zed-extensions/ruby), if you have any
saved debug scenarios you'll need to change `"adapter": "Ruby"` to
`"adapter": "rdbg"`.
2025-06-30 09:15:56 -07:00
Piotr Osiewicz
42c59014a9 debugger: Fix global debug tasks not being picked up (#33664)
Release Notes:

- Fixed a bug which caused global debug scenarios (from global
.zed/debug.json) to not be picked up.
2025-06-30 15:53:34 +00:00
Danilo Leal
3db452eec7 agent: Use a banner for the auto-retry message (#33661)
Follow-up to https://github.com/zed-industries/zed/pull/33275 so we use
the Banner component to display the auto-retry messages in the thread.

Release Notes:

- N/A
2025-06-30 15:34:28 +00:00
Kirill Bulatov
6e77e8405b Revert "languages: Bump ESLint LSP server to version 3.0.10 (#32717)" (#33659)
This reverts commit 1edaeebae5.

Based on an elevated number of ESLint-related issues, reverting the
upgrade.
Many people upvoted the issues and did not share any repro details, so
cannot be certain what's more broken: seems relatively generic as
related to *.ts ESLint configs.

Checked the revert on 2 projects from the issues below:

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

With https://github.com/adamhl8/zed-33425 as an example repo: there,
both eslint configurations worked for me when I stopped Zed and opened a
project.
Somehow, switching various Zed's with different vscode-eslint package
versions, eventually I get
`Error: Cannot find module
'~/.local/share/zed/languages/eslint/vscode-eslint-3.0.10/vscode-eslint/server/out/eslintServer.js'`-ish
error.

Not very related to issues with newer vscode-eslint integration, but
worth mentioning as is related to the package updates.


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

With a good example of
https://github.com/florian-lackner365/zed-eslint-bug monorepo project.
The monorepo part seems not to be related, but somehow,
`eslint.config.js` is involved as the newer vscode-eslint fails to find
a config.
Works well with the older vscode-eslint.

Release Notes:

- Downgraded to vscode-eslint-2.4.4 as a ESLint language server
2025-06-30 15:19:00 +00:00
Alejandro Fernández Gómez
465f64da7e Make the preview button the same as the other buttons (#33658)
This fixes a tiny visual defect I noticed today. The "Preview" button is
slightly smaller and has less padding than the other buttons in the
quick action bar.

**Before:**

Note how there is a small gap between the black guides and the button.


https://github.com/user-attachments/assets/04d3d83a-9193-47b1-80d8-94a5d1fbd750

**After:**


https://github.com/user-attachments/assets/98f878cc-c5e3-491c-abe9-9ef0d5cf678a



Release Notes:

- N/A
2025-06-30 15:16:01 +00:00
Piotr Osiewicz
e5a8cc7aab debugger: Fix DAP Logs mangling sessions across multiple Zed windows (#33656)
Release Notes:

- Fixed an issue with Debug Adapter log showing sessions from other Zed
windows in the dropdown.
2025-06-30 15:01:54 +00:00
Peter Tripp
bdf29bf76f Allow disabling tools when 'enable_all_context_servers = true' (#33536)
Closes https://github.com/zed-industries/zed/issues/33519

Release Notes:

- agent: Improved support for explicitly disabling individual tools when
`enable_all_context_servers` is true. (e.g. enable all tools except
XYZ).
2025-06-30 10:56:25 -04:00
Danilo Leal
402c61c00d Add small UI tweak to the inline color preview square (#33655)
Follow-up to https://github.com/zed-industries/zed/pull/33605 so it is
just a bit more subtle and smaller.

Release Notes:

- N/A
2025-06-30 11:19:58 -03:00
Mikal Sande
59e88ce82b Show regex query error under the search bar (#33638)
Closes #17223

Release Notes:

- Show regex parsing errors under the search bar for buffer and project
search.

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-06-30 11:05:33 -03:00
Peter Tripp
22ab4c53d1 R docs: Remove non-working configuration (#33654)
This config was meant to be commented out in #33594 because it does not
work.

Release Notes:

- N/A
2025-06-30 14:03:09 +00:00
Danilo Leal
f106ea7641 docs: Update custom MCP format template (#33649)
To match the new format added in
https://github.com/zed-industries/zed/pull/33539.

Release Notes:

- N/A
2025-06-30 10:42:38 -03:00
Kirill Bulatov
e37ef2a991 Use more generic error messages in gpui (#33651)
Follow-up of https://github.com/zed-industries/zed/pull/32537

Release Notes:

- N/A
2025-06-30 13:40:31 +00:00
Danilo Leal
1c05062482 agent: Always focus on to the active model in the picker (#33567)
Release Notes:

- agent: Improved the model selector by ensuring the active model is
always focused on open.
2025-06-30 10:32:27 -03:00
Piotr Osiewicz
8c04f12499 debugger: Tighten up breakpoint list (#33645)
Release Notes:

- N/A
2025-06-30 14:49:09 +02:00
Bennet Bo Fenner
aa7ccecc49 agent: Reduce log spam for context servers (#33644)
Previously we would always run `maintain_servers` even if the settings
did not change. While this would not cause any MCP servers to restart,
we would still go through all configured servers and call the
`command(...)` function on each installed MCP extension. This can cause
lots of logs to show up when an MCP server is not configured correctly.

Release Notes:

- N/A
2025-06-30 10:26:14 +00:00
Umesh Yadav
f4aeeda2d9 script: Fix license symlink and path in new-crate.sh (#33620)
While creating a new crate I realised the License symlink and path are
broken. The symlink was broken for LICENSE-GPL. Also the file created in
the new crate was not using the expected file name as per the
check-license script which was failing due to wrong filename in the new
crate. I fixed that as well.

Release Notes:

- N/A

Signed-off-by: Umesh Yadav <git@umesh.dev>
2025-06-30 09:51:58 +00:00
Bennet Bo Fenner
ca0bd53bed agent: Fix an issue with messages containing trailing whitespace (#33643)
Seeing this come up in our server logs when sending requests to
Anthropic: `final assistant content cannot end with trailing
whitespace`.


Release Notes:

- agent: Fixed an issue where Anthropic requests would sometimes fail
because of malformed assistant messages
2025-06-30 09:31:40 +00:00
Kirill Bulatov
ae6237178c Further improve color inlay hints in multi buffers (#33642)
Follow-up of https://github.com/zed-industries/zed/pull/33605

Release Notes:

- N/A
2025-06-30 09:18:43 +00:00
Bennet Bo Fenner
ac3328adb6 agent: Fix issue where web search could return 401 (#33639)
Closes #33524

Release Notes:

- agent: Fix an issue where performing a web search request would
sometimes fail
2025-06-30 11:12:12 +02:00
Bennet Bo Fenner
d63909c598 agent: Use standardized MCP configuration format in settings (#33539)
Changes our MCP settings from:

```json
{
  "context_servers": {
    "some-mcp-server": {
      "source": "custom",
      "command": {
        "path": "npx",
        "args": [
          "-y",
          "@supabase/mcp-server-supabase@latest",
          "--read-only",
          "--project-ref=<project-ref>",
        ],
        "env": {
          "SUPABASE_ACCESS_TOKEN": "<personal-access-token>",
        },
      },
    },
  },
}

```

to:
```json
{
  "context_servers": {
    "some-mcp-server": {
      "source": "custom",
      "command": "npx",
      "args": [
        "-y",
        "@supabase/mcp-server-supabase@latest",
        "--read-only",
        "--project-ref=<project-ref>",
      ],
      "env": {
        "SUPABASE_ACCESS_TOKEN": "<personal-access-token>",
      },
    },
  },
}
```


Which seems to be somewhat of a standard now (VSCode, Cursor, Windsurf,
...)

Release Notes:

- agent: Use standardised format for configuring MCP Servers
2025-06-30 08:05:52 +00:00
Danilo Leal
c3d0230f89 docs: Adjust heading sizes (#33628)
Just fine-tuning some heading sizes that were off, particularly h4s and
h5s.

Release Notes:

- N/A
2025-06-29 20:49:28 -03:00
Piotr Osiewicz
bc5927d5af debugger: Fix spec violation with threads request being issued before debug session is initialized (#33627)
Follow-up to #32852. This time we'll check if the debug session is
initialized before querying threads.

Release Notes:

- Fix Zed's debugger issuing threads request before it is allowed to do
so per DAP specification.
2025-06-29 23:38:16 +00:00
Piotr Osiewicz
d2cf995e27 debugger: Tweak layout of debug landing page in vertical dock position (#33625)
Release Notes:

- Reorganized layout of a debug panel without any sessions for a
vertical dock position.
- Moved parent directories of source breakpoints into a tooltip.
2025-06-30 00:48:14 +02:00
Michael Sloan
86161aa427 Use refs to deduplicate settings JSON schema (~1.7mb to ~0.26mb) (#33618)
Release Notes:

- N/A
2025-06-29 18:33:05 +00:00
Peter Tripp
a602b4b305 Improve R documentation (#33594)
Release Notes:

- N/A
2025-06-29 12:21:10 -04:00
Kirill Bulatov
047d515abf Rework color indicators visual representation (#33605)
Use a div-based rendering code instead of using a text

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

Before:
<img width="410" alt="before_dark"
src="https://github.com/user-attachments/assets/66ad63ae-7836-4dc7-8176-a2ff5a38bcd4"
/>
After:
<img width="407" alt="after_dark"
src="https://github.com/user-attachments/assets/0b627da8-461b-4f19-b236-4a69bf5952a0"
/>


Before:
<img width="409" alt="before_light"
src="https://github.com/user-attachments/assets/ebcfabec-fcda-4b63-aee6-c702888f0db4"
/>
After:
<img width="410" alt="after_light"
src="https://github.com/user-attachments/assets/c0da42a1-d6b3-4e08-a56c-9966c07e442d"
/>

The border is not that contrast as in VSCode examples in the issue, but
I'm supposed to use the right thing in

1e11de48ee/crates/editor/src/display_map/inlay_map.rs (L357)

based on 


41583fb066/crates/theme/src/styles/colors.rs (L16-L17)

Another oddity is that the border starts to shrink on `cmd-=`
(`zed::IncreaseBufferFontSize`):

<img width="1244" alt="image"
src="https://github.com/user-attachments/assets/f424edc0-ca0c-4b02-96d4-6da7bf70449a"
/>

but that needs a different part of code to be adjusted hence skipped.

Tailwind CSS example:

<img width="1108" alt="image"
src="https://github.com/user-attachments/assets/10ada4dc-ea8c-46d3-b285-d895bbd6a619"
/>


Release Notes:

- Reworked color indicators visual representation
2025-06-29 09:43:56 +00:00
Piotr Osiewicz
e5bcd720e1 debugger: Add UI for tweaking breakpoint properties directly from breakpoint list (#33097)
Release Notes:

- debugger: Breakpoint properties (log/hit condition/condition) can now
be set directly from breakpoint list.
2025-06-28 23:41:44 +02:00
Kirill Bulatov
41583fb066 Fix document colors issues with other inlays and multi buffers (#33598)
Closes https://github.com/zed-industries/zed/issues/33575

* Fixes inlay colors spoiled after document color displayed
* Optimizes the query pattern for large multi buffers

Release Notes:

- Fixed document colors issues with other inlays and multi buffers
2025-06-28 21:10:49 +00:00
Cole Miller
521a223681 Add editor::Rewrap binding to Emacs keymaps (#33588)
`M-q` is `fill-paragraph` which is like `editor::Rewrap`.

Release Notes:

- emacs: Bound `alt-q` to `editor::Rewrap` (like `M-q` or `M-x
fill-paragraph`)
2025-06-28 15:35:59 -04:00
Conrad Irwin
c8c6468f9c vim: Non-interactive shell (#33568)
Closes #33144

Release Notes:

- vim: Run r! in a non-interactive shell
2025-06-28 10:23:57 -06:00
Umesh Yadav
3f4098e87b open_ai: Make OpenAI error message generic (#33383)
Context: In this PR: https://github.com/zed-industries/zed/pull/33362,
we started to use underlying open_ai crate for making api calls for
vercel as well. Now whenever we get the error we get something like the
below. Where on part of the error mentions OpenAI but the rest of the
error returns the actual error from provider. This PR tries to make the
error generic for now so that people don't get confused seeing OpenAI in
their v0 integration.

```
Error interacting with language model
Failed to connect to OpenAI API: 403 Forbidden {"success":false,"error":"Premium or Team plan required to access the v0 API: https://v0.dev/chat/settings/billing"}
```

Release Notes:

- N/A
2025-06-28 14:38:27 +02:00
alphaArgon
1d684c8890 Add shadow back for blurred/transparent window on macOS (#27403)
Closes #15383
Closes #10993

`NSVisualEffectView` is an official API for implementing blur effects
and, by traversing the layers, we **can remove the background color**
that comes with the view. This avoids using private APIs and aligns
better with macOS’s native design.

Currently, `GPUIView` serves as the content view of the window. To add
the blurred view, `GPUIView` is downgraded to a subview of the content
view, placed at the same level as the blurred view.

Release Notes:

- Fixed the missing shadow for blurred-background windows on macOS.

---------

Co-authored-by: Peter Tripp <peter@zed.dev>
2025-06-28 09:50:54 +03:00
Rift
97c5c5a6e7 vim: Respect count for paragraphs (#33489)
Closes #32462 

Release Notes:

- vim: Paragraph objects now support counts (`d2ap`, `v2ap`, etc.)

---------

Co-authored-by: Rift <no@e.mail>
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-06-27 22:05:47 -06:00
5brian
ba4fc1bcfc vim: Add debug panel ex command (#33560)
Added :Debug to open debug panel, also added
[:display](https://neovim.io/doc/user/change.html#%3Adisplay), alias to
:reg

Release Notes:

- N/A
2025-06-27 21:32:40 -06:00
Smit Barmase
bbf16bda75 editor: Improve rewrap to respect indent and prefix boundaries (#33566)
1. Fixes bug where this would not rewrap:

```rs
// This is the first long comment block to be wrapped.
fn my_func(a: u32);
// This is the second long comment block to be wrapped.
```
2. Comment prefix boundaries (Notice now they don't merge between
different comment prefix):

Initial text:
```rs
// A regular long long comment to be wrapped.
// A second regular long comment to be wrapped.
/// A documentation long comment to be wrapped.
```
Upon rewrap:
```rs
// A regular long long comment to be
// wrapped. A second regular long
// comment to be wrapped.
/// A documentation long comment to be
/// wrapped.
```
3. Indent boundaries (Notice now they don't merge between different
indentation):

Initial text:
```rs
fn foo() {
      // This is a long comment at the base indent.
      // This is a long comment at the base indent.
                 // This is a long comment at the next indent.
                 // This is a long comment at the next indent.
      // This is a long comment at the base indent.
}
```
Upon rewrap:
```rs
fn foo() {
      // This is a long comment at the base
      // indent. This is a long comment at the
      // base indent.
                 // This is a long comment at the 
                 // next indent. This is a long 
                 // comment at the next indent.
      // This is a long comment at the base
      // indent.
}
```

Release Notes:

- Fixed an issue where rewrap would not work with selection when two
comment blocks are separated with line of code.
- Improved rewrap to respect changes in indentation or comment prefix
(e.g. `//` vs `///`) as boundaries so that it doesn't merge them into
one mangled text.
2025-06-28 05:38:18 +05:30
ddoemonn
c56b8904cc Prevent branch name overflow in git panel selection (#33529)
Closes #33527

Release Notes:

- Fixed long branch names overflowing to multiple lines in git panel
branch selector

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-06-27 20:50:53 -03:00
Artem Loenko
695118d110 agent: Show provider icon in model selectors (#30595)
I often switch between models, and I believe many people do. Currently,
it is difficult to determine which provider offers the selected model
because the same models are available from different providers. I
propose a simple change to the selector so that users can distinguish
between providers from the model they have chosen.

As a side note, I would actually prefer to have a text label with the
provider’s name next to the model name in the selector. However, I
understand that this is too opinionated and takes up too much space.

| Before | After |
| ------ | ------ |
|
![before_inline_assist](https://github.com/user-attachments/assets/35617ba5-e8d4-4dab-a997-f7286f73f2bf)
|
![after_inline_assist](https://github.com/user-attachments/assets/c37c81b4-73e4-49e2-955d-b8543b2855ad)
|
|
![before_text_thread](https://github.com/user-attachments/assets/af90303b-12d6-402c-90a5-8b36cd97396e)
|
![after_text_thread](https://github.com/user-attachments/assets/bca5b423-f12b-4eaf-a82e-424d09b7f447)
|
|
![before_thread](https://github.com/user-attachments/assets/0946775f-1d52-437b-a217-9708ee2e789a)
|
![after_thread](https://github.com/user-attachments/assets/f5e53968-9020-446f-9d5e-653ae9fdea3e)
|

Release Notes:

- The model selector has been improved with a provider icon, making it
easier to distinguish between providers.

---------

Co-authored-by: Danilo Leal <67129314+danilo-leal@users.noreply.github.com>
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-06-27 19:33:58 -03:00
Conrad Irwin
a675ca7a1e Remove into SelectionEffects from .change_selections (#33554)
In #32656 I generalized the argument to change selections to allow
controling both the scroll and the nav history (and the completion
trigger).

To avoid conflicting with ongoing debugger cherry-picks I left the
argument as an `impl Into<>`, but I think it's clearer to make callers
specify what they want here.

I converted a lot of `None` arguments to `SelectionEffects::no_scroll()`
to be exactly compatible; but I think many people used none as an "i
don't care" value in which case Default::default() might be more
appropraite

Closes #ISSUE

Release Notes:

- N/A
2025-06-27 14:31:31 -06:00
Conrad Irwin
6e762d9c05 Revert "Remove into SelectionEffects from .change_selections"
This reverts commit 28380d714d.
2025-06-27 14:06:17 -06:00
Conrad Irwin
28380d714d Remove into SelectionEffects from .change_selections
In #32656 I generalized the argument to change selections to allow
controling both the scroll and the nav history (and the completion
trigger).

To avoid conflicting with ongoing debugger cherry-picks I left the
argument as an `impl Into<>`, but I think it's clearer to make callers
specify what they want here.

I converted a lot of `None` arguments to `SelectionEffects::no_scroll()`
to be exactly compatible; but I think many people used none as an "i
don't care" value in which case Default::default() might be more
appropraite
2025-06-27 14:03:45 -06:00
Alejandro Fernández Gómez
f338c46bf7 Fix line indices when using a selection in the context (#33549)
Closes #33152.

The label for a file selection context had an off-by-one error on line
indices. This PR adjusts that.

Before:

<img width="1072" alt="Screenshot 2025-06-27 at 20 55 28"
src="https://github.com/user-attachments/assets/da0be503-056b-442b-9a79-38cf504d8a44"
/>

After:

<img width="1090" alt="Screenshot 2025-06-27 at 20 49 35"
src="https://github.com/user-attachments/assets/f98df57a-8f07-4ea5-a06b-a4b6fb8c2e4e"
/>


Release Notes:

- Fixed a off-by-one error on the line indices when using a selection as
context for the agent,
2025-06-27 13:41:17 -06:00
Umesh Yadav
5fbb7b0d40 copilot: Only set Copilot-Vision-Request header for vision requests (#33552)
Closes #31951

The fix is copied and translated from copilot chat actual implementation
code:
ad7cbcae9a/src/platform/openai/node/fetch.ts (L493C1-L495C3)

Release Notes:

- Fix copilot failing due to missing `Copilot-Vision-Request` from
request.
2025-06-27 21:37:26 +02:00
Cole Miller
f12b0dddf4 Touch up extension DAP schemas fix (#33548)
Updates #33546 

Release Notes:

- N/A

Co-authored-by: Piotr <piotr@zed.dev>
2025-06-27 15:34:21 -04:00
Conrad Irwin
14bb10d783 Don't panic on vintage files (#33543)
Release Notes:

- remoting: Fix a crash on the remote side when encountering files from
before 1970.
2025-06-27 13:15:50 -06:00
Cole Miller
c9ce4aec91 Fix debug adapters from extensions not being picked up (#33546)
Copy the debug adapter schemas so that they're available to the
extension host, like we do for other extension assets.

Release Notes:

- N/A
2025-06-27 14:31:58 -04:00
Kirill Bulatov
01dfb6fa82 Respect server capabilities on queries (#33538)
Closes https://github.com/zed-industries/zed/issues/33522

Turns out a bunch of Zed requests were not checking their capabilities
correctly, due to odd copy-paste and due to default that assumed that
the capabilities are met.

Adjust the code, which includes the document colors, add the test on the
colors case.

Release Notes:

- Fixed excessive document colors requests for unrelated files
2025-06-27 16:31:40 +00:00
5brian
f9987a1141 vim: Grep in visual line (#33414)
From
https://github.com/zed-industries/zed/pull/10831#issuecomment-2078523272

> I agree with not prefilling the search bar with a multiline query.

Not sure if it's a bug that a one-line visual line selection does not
get pre filled, this PR corrects the query to use the visual line
selection instead of the 'normal' selection

Release Notes:

- N/A
2025-06-27 10:18:26 -06:00
Ben Kunkle
7432e947bc Add element_selection_background highlight to theme (#32388)
Closes #32354

The issue is that we render selections over the text in the agent panel,
but under the text in editor, so themes that have no alpha for the
selection background color (defaults to 0xff) will just occlude the
selected region. Making the selection render under the text in markdown
would be a significant (and complicated) refactor, as selections can
cross element boundaries (i.e. spanning code block and a header after
the code block).

The solution is to add a new highlight to themes
`element_selection_background` that defaults to the local players
selection background with an alpha of 0.25 (roughly equal to 0x3D which
is the alpha we use for selection backgrounds in default themes) if the
alpha of the local players selection is 1.0. The idea here is to give
theme authors more control over how the selections look outside of
editor, as in the agent panel specifically, the background color is
different, so while an alpha of 0.25 looks acceptable, a different color
would likely be better.

CC: @iamnbutler. Would appreciate your thoughts on this. 

> Note: Before and after using Everforest theme

| Before | After | 
|-------| -----|
| <img width="618" alt="Screenshot 2025-06-09 at 5 23 10 PM"
src="https://github.com/user-attachments/assets/41c7aa02-5b3f-45c6-981c-646ab9e2a1f3"
/> | <img width="618" alt="Screenshot 2025-06-09 at 5 25 03 PM"
src="https://github.com/user-attachments/assets/dfb13ffc-1559-4f01-98f1-a7aea68079b7"
/> |

Clearly, the selection in the after doesn't look _that_ great, but it is
better than the before, and this PR makes the color of the selection
configurable by the theme so that this theme author could make it a
lighter color for better contrast.




Release Notes:

- agent panel: Fixed an issue with some themes where selections inside
the agent panel would occlude the selected text completely

Co-authored-by: Antonio <me@as-cii.com>
2025-06-27 15:46:04 +00:00
Conrad Irwin
157199b65b Replace newlines in search bar (#33504)
Release Notes:

- search: Pasted newlines are now rendered as "\n" (with an underline),
instead of line-wrapping. This should make it much clearer what you're
searching for.
 
<img width="675" alt="Screenshot 2025-06-27 at 00 34 52"
src="https://github.com/user-attachments/assets/67275bc6-bec1-463f-b351-6b9ed0a6df81"
/>
2025-06-27 09:39:38 -06:00
Conrad Irwin
d74f3f4ea6 Fix crash in git checkout (#33499)
Closes #33438

Release Notes:

- git: Use git cli to perform checkouts (to avoid a crash seen in
libgit2)
2025-06-27 09:16:15 -06:00
Smit Barmase
9e2023bffc editor: Fix editor tests from changing on format on save (#33532)
Use placeholder to prevent format-on-save from removing whitespace in
editor tests, which leads to unnecessary git diff and failing tests.

cc: https://github.com/zed-industries/zed/pull/32340

Release Notes:

- N/A
2025-06-27 20:14:01 +05:30
Umesh Yadav
3ab4ad6de8 language_models: Use JsonSchemaSubset for Gemini models in OpenRouter (#33477)
Closes #33466

Release Notes:

- N/A
2025-06-27 16:36:16 +02:00
Peter Tripp
e3ce0618a3 collab: Lookup avatars by GitHub ID instead of username (#33523)
Closes: https://github.com/zed-industries/zed/issues/19207

This will correctly show Avatars for recently renamed/deleted users and
for enterprise users where the username avatar url triggers a redirect
to an auth prompt. Also saves a request (302 redirect) per avatar.

Tested locally and avatars loaded as expected.

Release Notes:

- N/A
2025-06-27 09:40:50 -04:00
Kirill Bulatov
865dd4c5fc Rework LSP tool keyboard story (#33525)
https://github.com/user-attachments/assets/81da68fe-bbc5-4b23-8182-923c752a8bd2

* Removes all extra elements: headers, buttons, to simplify the menu
navigation approach and save space.
Implements the keyboard navigation and panel toggling.

* Keeps the status icon and the server name, and their ordering approach
(current buffer/other) in the menu.
The status icon can still be hovered, but that is not yet possible to
trigger from the keyboard: future ideas would be make a similar side
display instead of hover, as Zeta menu does:


![image](https://github.com/user-attachments/assets/c844bc39-00ed-4fe3-96d5-1c9d323a21cc)

* Allows to start (if all are stopped) and stop (if some are not
stopped) all servers at once now with the button at the bottom

Release Notes:

- N/A
2025-06-27 16:25:56 +03:00
Umesh Yadav
2178f66af6 agent_ui: Rename MaxModeTooltip to BurnModeTooltip (#33521)
Closes #ISSUE

Release Notes:

- N/A
2025-06-27 13:56:46 +02:00
ddoemonn
338a7395a7 Fix blend alpha colors with editor background in inline preview (#33513)
Closes #33505

## Before

<img width="434" alt="Screenshot 2025-06-27 at 12 22 57"
src="https://github.com/user-attachments/assets/ac215a39-b3fe-4c9e-bd7d-0d7568d5fd1f"
/>

## After

<img width="441" alt="Screenshot 2025-06-27 at 12 22 47"
src="https://github.com/user-attachments/assets/28218ed6-c1aa-4d3f-a268-def2fa9f0340"
/>

Release Notes:

- Fixed inline color previews not correctly blending alpha/transparency
values with the editor background
2025-06-27 12:37:05 +03:00
Finn Evers
4c2415b338 editor: Use em_advance everywhere for horizontal scroll position computations (#33514)
Closes #33472

This PR fixes some regressions that were introduced in
https://github.com/zed-industries/zed/pull/32558, which updated the
editor scrolling to use `em_advance` instead of `em_width` for the
horizontal scroll position calculation.
However, not all occurrences were updated, which caused issues with wrap
guides and some small stuttering with horizontal autoscroll whilst
typing/navigating with the keyboard.

Release Notes:

- Fixed an issue where horizontal autoscrolling would stutter and indent
guides would drift when scrolling horizontally.
2025-06-27 09:32:50 +00:00
Ron Harel
e6bc1308af Add SVG preview (#32694)
Closes #10454

Implements SVG file preview capability similar to the existing markdown
preview.
- Adds `svg_preview` crate with preview view and live reloading upon
file save.
- Integrates SVG preview button in quick action bar.
- File preview shortcuts (`ctrl/cmd+k v` and `ctrl/cmd+shift+v`) are
extension-aware.

Release Notes:

- Added SVG file preview, accessible via the quick action bar button or
keyboard shortcuts (`ctrl/cmd+k v` and `ctrl/cmd+shift+v`) when editing
SVG files.
2025-06-27 09:08:05 +00:00
Ben Brandt
6c46e1129d Cleanup remaining references to max mode (#33509)
Release Notes:

- N/A
2025-06-27 08:32:13 +00:00
fantacell
fbb5628ec6 Reset selection goal after helix motion (#33184)
Closes #33060

Motions like `NextWordStart` don't reset the selection goal in vim mode
`helix_normal` unlike in `normal` which can lead to the cursor jumping
back to the previous horizontal position after going up or down.

Release Notes:

- N/A
2025-06-26 22:32:35 -06:00
Conrad Irwin
8c9116daa5 Fix panic in ctrl-g (#33474)
Release Notes:

- vim: Fixed a crash with ctrl-g
2025-06-26 21:26:58 -06:00
Conrad Irwin
20a3e613b8 vim: Better jump list support (#33495)
Closes #23527
Closes #30183
Closes some Discord chats

Release Notes:

- vim: Motions now push to the jump list using the same logic as vim
(i.e.
`G`/`g g`/`g d` always do, but `j`/`k` always don't). Most non-vim
actions
(including clicking with the mouse) continue to push to the jump list
only
  when they move the cursor by 10 or more lines.
2025-06-26 21:25:07 -06:00
Ben Kunkle
ba1c05abf2 keymap: Add ability to update user keymaps (#33487)
Closes #ISSUE

The ability to update user keybindings in their keymap is required for
#32436. This PR adds the ability to do so, reusing much of the existing
infrastructure for updating settings JSON files.

However, the existing JSON update functionality was intended to work
only with objects, therefore, this PR simply wraps the object updating
code with non-general keymap-specific array updating logic, that only
works for top-level arrays and can only append or update entries in said
top-level arrays. This limited API is reflected in the limited
operations that the new `update_keymap` method on `KeymapFile` can take
as arguments.

Additionally, this PR pulls out the existing JSON updating code into its
own module (where array updating code has been added) and adds a
significant number of tests (hence the high line count in the diff)

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-06-26 21:52:26 -04:00
Danilo Leal
2823771c06 Add design improvements to the LSP popover (#33485)
Not the ideal design just yet as that will probably require a different
approach altogether, but am pushing here just some reasonably small UI
adjustments that will make this feel slightly nicer!

Release Notes:

- N/A
2025-06-26 22:27:21 +00:00
Cole Miller
343f155ab9 Update docs for Swift debugging (#33483)
Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-06-26 21:25:03 +00:00
Julia Ryan
2dece13d83 nix: Update to access new rust toolchain version (#33476)
Needed due to #33439

Release Notes:

- N/A
2025-06-26 20:31:24 +00:00
Piotr Osiewicz
985dcf7523 chore: Bump Rust version to 1.88 (#33439)
Goodies in this version:
- if-let chains 🎉
- Better compiler perf for Zed
(https://github.com/rust-lang/rust/pull/138522)

For more, see: https://releases.rs/docs/1.88.0/

Release Notes:

- N/A

---------

Co-authored-by: Junkui Zhang <364772080@qq.com>
2025-06-26 20:54:19 +02:00
Conrad Irwin
b079871428 Fix subtraction with overflow (#33471)
Release Notes:

- N/A
2025-06-26 12:38:54 -06:00
fantacell
4983b01c89 helix: Change word motions (#33408)
When starting on the newline character at the end of a line the helix
word motions select that character, unlike in helix itself. This makes
it easy to accidentaly join two lines together.
Also, word motions that go backwards should stop at the start of a line.
I added that.

Release Notes:

- helix: Fix edge-cases with word motions and newlines
2025-06-26 12:25:47 -06:00
Cole Miller
35863c4302 debugger: Fix treatment of node-terminal scenarios (#33432)
- Normalize `node-terminal` to `pwa-node` before sending to DAP
- Split `command` into `program` and `args`
- Run in external console

Release Notes:

- debugger: Fixed debugging JavaScript tasks that used `"type":
"node-terminal"`.
2025-06-26 14:02:09 -04:00
Kirill Bulatov
a0bd25f218 Feature gate the LSP button (#33463)
Follow-up of https://github.com/zed-industries/zed/pull/32490

The tool still looks like designed by professional developers, and still
may change its UX based on the internal feedback.

Release Notes:

- N/A
2025-06-26 15:41:42 +00:00
Agus Zubiaga
8a1e795746 agent: Move ActiveThread and MessageEditor into ActiveView (#33462)
`ActiveThread` and `MessageEditor` only make sense when `active_view` is
`Thread`, so we moved them in there. This will make it easier to work on
new agent threads.

Release Notes:

- N/A

---------

Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
2025-06-26 15:36:23 +00:00
Peter Tripp
f4818b648e linux: Add agent::ToggleBurnMode shortcut (super-ctrl-b) (#33458)
Follow-up to: 
- https://github.com/zed-industries/zed/pull/33452
- https://github.com/zed-industries/zed/pull/33190
- https://github.com/zed-industries/zed/pull/31630

Release Notes:

- N/A
2025-06-26 15:07:46 +00:00
Peter Tripp
7031ed8b87 ci: Fix duplicated/failed eval jobs (#33453)
I think this should fix the CI issues with Eval jobs:

1. Duplicated workflows because `synchronize` / `opened` were triggering
distinct runs. This caused failed job entries because the duplicated
workflows had a shared concurrency group and so one would pre-empt the
other.

3. Removes the no-op job, introduced as an attempted workaround in
   https://github.com/zed-industries/zed/pull/29420.

These should correctly show as "Skipped" now:

| Before | After |
| - | - |
| <img width="359" alt="Screenshot 2025-06-26 at 9 57 04"
src="https://github.com/user-attachments/assets/6ddd4f46-27c7-4d82-98ba-0f1166fc55e7"
/> | <img width="355" alt="Screenshot 2025-06-26 at 10 09 54"
src="https://github.com/user-attachments/assets/5faade2c-f17c-447a-9af9-6396f9e53016"
/> |

Release Notes:

- N/A
2025-06-26 11:01:16 -04:00
Richard Feldman
6073d2c93c Automatically retry when API is Overloaded or 500s (#33275)
<img width="484" alt="Screenshot 2025-06-25 at 2 26 16 PM"
src="https://github.com/user-attachments/assets/340f15d7-b115-4895-bae8-b12a915bfda1"
/>

<img width="460" alt="Screenshot 2025-06-25 at 2 26 08 PM"
src="https://github.com/user-attachments/assets/6e587a38-d542-405f-809f-402e87520538"
/>

Now we:
* Automatically retry up to 3 times on upstream Overloaded or 500 errors
(currently for Anthropic only; will add others in future PRs)
* Also automatically retry on rate limit errors (using the provided
duration to wait, if we were given one)
* Give you a notification if you don't have Zed open and we stopped the
thread because of an error

Still todo in future PRs:
* Update collab to report Overloaded and 500 errors differently if
collab itself is passing through an upstream error vs not (currently we
report these as "Zed's API is overloaded" when actually it's the
upstream one!)
* Updating providers other than Anthropic to categorize their errors so
that they benefit from this
* Expanding graceful error handling/retry to other things besides
Overloaded and 500 errors (e.g. connection reset)

Release Notes:

- Automatically retry in Agent Panel instead of erroring out when an
upstream AI API is overloaded or 500s
- Show a notification when an Agent thread errors out and Zed is not the
active window
2025-06-26 10:53:33 -04:00
Danilo Leal
00499aadd4 Add back default keybindings to Burn Mode and branch picker toggles (#33452)
Follow up to https://github.com/zed-industries/zed/pull/33190, as they
were removed because of conflict with VS Code's usage of those bindings
to toggle the right dock. `cmd-ctrl-b` seems like a safe alternative.
Note that this PR is macOS only, though. I couldn't find yet any good
options for Linux as they were all mostly conflicting with something
else.

Release Notes:

- N/A
2025-06-26 11:35:02 -03:00
Peter Tripp
d1eb69c6cd ci: Improve check_docs skipping (#33455)
Follow-up to: https://github.com/zed-industries/zed/pull/33398

Logic there was incomplete. Caused [this
failure](https://github.com/zed-industries/zed/actions/runs/15904169712/job/44854558276).

Release Notes:

- N/A
2025-06-26 14:34:20 +00:00
Marshall Bowers
40cbfb7eb2 docs: Add note about extension submodules needing to use HTTPS URLS (#33454)
This PR adds a note to the extension publishing docs about extension
submodules needing to use HTTPS URLs.

Release Notes:

- N/A
2025-06-26 14:18:56 +00:00
Peter Tripp
5d0f02d356 Add cmd-alt-b (workspace::ToggleRightDock) on macOS (#33450)
Release Notes:

- N/A
2025-06-26 13:54:21 +00:00
Renze Post
ca8e213151 Suggest reST extension for .rst files (#33413)
Suggest the reST extension when opening a .rst file for the first time.

Release Notes:

- N/A
2025-06-26 08:59:04 -04:00
Michael Sloan
90c893747c gpui: Prevent the same action name from being registered multiple times (#33359)
Also removes duplicate `editor::RevertFile` and `vim::HelixDelete`
actions

Release Notes:

- N/A
2025-06-26 06:24:14 +00:00
Smit Barmase
d09c7eb317 language: Add context-aware decrease indent for Python (#33370)
Closes #33238, follow-up to
https://github.com/zed-industries/zed/pull/29625.

Changes:

- Removed `significant_indentation`, which was the way to introduce
indentation scoping in languages like Python. However, it turned out to
be unnecessarily complicated to define and maintain.
- Introduced `decrease_indent_patterns`, which takes a `pattern` keyword
to automatically outdent and `valid_after` keywords to treat as valid
code points to snap to. The outdent happens to the most recent
`valid_after` keyword that also has less or equal indentation than the
currently typed keyword.

Fixes:

1. In Python, typing `except`, `finally`, `else`, and so on now
automatically indents intelligently based on the context in which it
appears. For instance:

```py
try:
    if a == 1:
        try:
             b = 2
             ^  # <-- typing "except:" here would indent it to inner try block
```

but,

```py
try:
    if a == 1:
        try:
             b = 2
    ^  # <-- typing "except:" here would indent it to outer try block
```

2. Fixes comments not maintaining indent.

Release Notes:

- Improved auto outdent for Python while typing keywords like `except`,
`else`, `finally`, etc.
- Fixed the issue where comments in Python would not maintain their
indentation.
2025-06-26 11:11:03 +05:30
Smit Barmase
1753432406 Fix tree sitter python try statement to accept missing else/except/finally (#33431)
We have fork now:
218fcbf3fd

Release Notes:

- N/A
2025-06-26 09:48:44 +05:30
Cole Miller
d9218b10ea Bump livekit-rust-sdks for candidate webrtc-sys build fix (#33387)
Incorporates https://github.com/zed-industries/livekit-rust-sdks/pull/5

Don't merge yet, waiting until after new releases are cut in case of
unexpected breakage.

Release Notes:

- N/A
2025-06-25 21:00:33 -04:00
Michael Sloan
dfdeb1bf51 linux: Don't insert characters if modifiers other than shift are held (#33424)
Closes #32219 #29666

Release Notes:

- Linux: Now skips insertion of characters when modifiers are held. Before, characters were inserted if there's no match in the keymap.
2025-06-25 17:11:59 -06:00
Max Brunsfeld
b9f81c7ce7 Restore missing initialization of text thread actions (#33422)
Fixes a regression introduced in
https://github.com/zed-industries/zed/pull/33289

Release Notes:

- Fixed a bug where some text thread actions were accidentally removed.
2025-06-25 22:48:40 +00:00
Michael Sloan
b1450b6d71 Remove git_panel::GenerateCommitMessage in favor of git::GenerateCommitMessage (#33421)
`git_panel::GenerateCommitMessage` has no handler,
`git::GenerateCommitMessage` should be preferred. Could add a
`#[action(deprecated_aliases = ["git_panel::GenerateCommitMessage"])]`,
but decided not to because that action didn't work. So instead uses of
it will show up as keymap errors.

Closes #32667

Release Notes:

- N/A
2025-06-25 22:29:30 +00:00
David Barsky
1af9f98c1d lsp-log: Avoid trimming leading space in language server logs (#33418)
Not sure what the full intention/right fix for this is, but
https://github.com/zed-industries/zed/pull/32659 re-introduced trimming
of leading spaces. rust-analyzer has [a custom tracing
formatter](317542c1e4/crates/rust-analyzer/src/tracing/hprof.rs)
that is _super_ useful for profiling what the heck rust-analyzer is
doing. It makes prodigious use of whitespace to delineate to create a
tree-shaped structure. This change reintroduces the leading whitespace.

I made a previous change similar to this that removed a `stderr:` in
https://github.com/zed-industries/zed/pull/27213/. If this is a
direction y'all are happy to go with, I'd be happy to add a test for
this!

<details>
<summary>A screenshot of the before</summary>

<img width="1624" alt="Screenshot 2025-06-25 at 2 12 45 PM"
src="https://github.com/user-attachments/assets/a714d973-9377-41ca-8087-3b0e82b41620"
/>

</details>

<details>
<summary>A screenshot of the after</summary>

<img width="1136" alt="Screenshot 2025-06-25 at 2 40 07 PM"
src="https://github.com/user-attachments/assets/b798ca13-11fc-4f97-9602-55e782068a5a"
/>

</details>

cc: @mgsloan.

Release Notes:

- Fixed the removal of leading whitespace in a language server's stderr
logs.
2025-06-25 16:24:51 -06:00
Danilo Leal
1330cb7a1f docs: Update instructions to use Vercel's v0 model (#33415)
To make sure this reflects the current reality as of today's
preview/stable version.

Release Notes:

- N/A
2025-06-25 17:52:23 -03:00
Peter Tripp
dae4e84bc5 Explicitly associate files as JSONC (#33410)
Fixes an issue when the zed repo was checked out to folder other than
`zed` (e.g. `zed2`) files were incorrectly identified as JSON instead of
JSONC.

Release Notes:

- N/A
2025-06-25 16:29:44 -04:00
morgankrey
6fb5500ef2 collab: Save Customer name and billing address to Customer on checkout (#33385)
We are collecting billing address and name on checkout now (for tax) but
we're not saving it back to the Customer level. Updating the Checkout
Session code to make`customer_update.address` equal to `auto`, instead
of the default `never`, as well as the same for `customer_update.name`.

Release Notes:

- N/A
2025-06-25 16:06:14 -04:00
vipex
8f9817173d pane: Update pinned tab count when it exceeds actual tab count (#33405)
## Summary

This PR improves the workaround introduced in #33335 that handles cases
where the pinned tab count exceeds the actual tab count during workspace
deserialization.

## Problem

The original workaround in #33335 successfully prevented the panic but
had two issues:
1. **Console spam**: The warning message was logged repeatedly because
`self.pinned_tab_count` wasn't updated to match the actual tab count
2. **Auto-pinning behavior**: New tabs up until you exceed the old safe
tab count were automatically pinned after the workaround was triggered.

## Solution

Updates the defensive code to set `self.pinned_tab_count = tab_count`
when the mismatch is detected, ensuring:
- The warning is only logged once when encountered.
- New tabs behave normally (aren't auto-pinned)
- The workspace remains in a consistent state

This is an immediate fix for the workaround. I'll attempt to open up a
follow-up PR when i get the chance that will address the root cause by
implementing serialization for empty untitled tabs, as discussed in
#33342.

Release Notes:

- N/A
2025-06-25 19:52:15 +00:00
Michael Sloan
aae4778b4e gpui: Add more flushing of x11 requests (#33407)
Flushes should happen after sending messages to X11 when effects should
be applied quickly. This is not needed for requests that return replies
since it automatically flushes in that case.

Release Notes:

- N/A
2025-06-25 19:46:15 +00:00
Matin Aniss
e5c812fbcb gpui: Dynamic element arena (#32079)
Implements a chunking strategy for the element arena that allows it to
grow dynamically based on allocations, it is initialised with a single
chunk of a total size of 1 mebibyte. On allocation of data with a size
greater than the remaining space of the current chunk a new chunk is
created.

This reduces the memory allocation from the static 32 mebibytes, this
especially helps GPUI applications that don't need such a large element
arena and even Zed in most cases. This also prevents the panic when
allocations ever exceed the element arena.

Release Notes:

- N/A

---------

Co-authored-by: Michael Sloan <michael@zed.dev>
2025-06-25 13:29:13 -06:00
Peter Tripp
294147f473 ci: Skip build_docs more often (#33398)
Don't run `build_docs` when the only change is:
`.github/{workflows,ISSUE_TEMPLATE}/**`.
Example [extra
run](https://github.com/zed-industries/zed/actions/runs/15883155767).

Release Notes:

- N/A
2025-06-25 14:24:47 -04:00
Richard Feldman
4516b099e7 Reduce segment cloning when rendering messages (#33340)
While working on retries, I discovered some opportunities to reduce
cloning of message segments. These segments have full `String`s (not
`SharedString`s), so cloning them means copying cloning all the bytes of
all the strings in the message, which would be nice to avoid!

Release Notes:

- N/A
2025-06-25 14:10:48 -04:00
Peter Tripp
8e831ced5b ci: Remove community_delete_comments (#33396)
This was a temporary mitigation against a spam campaign, I don't think
this is required any longer. We can easily revert if it's still active.

See:
- https://github.com/zed-industries/zed/pull/16886

Release Notes:

- N/A
2025-06-25 13:44:43 -04:00
Joseph T. Lyons
3740eec5bf Do not show update "View Release Notes" notification in nightly builds (#33394)
These are useless in nightly, as the link within the notification simply
directs us to a commit view on GitHub. We update frequently on nightly;
dismissing this after every update is annoying.

Release Notes:

- N/A
2025-06-25 17:28:06 +00:00
Peter Tripp
2a5a1814cd text_thread: Improve roles after assistant::Split (shift-enter) (#33215)
Default to `You` when triggering `assistant::Split` at the end of a thread

Release Notes:

- agent_thread: Improved roles when triggering `assistant::Split`
(`shift-enter`)
2025-06-25 13:26:24 -04:00
Umesh Yadav
cc62125244 agent: Add GEMINI.md as a supported rules file name (#33381)
Gemini cli creates GEMINI.md file. This PR adds support for it.

Release Notes:

- agent: Add GEMINI.md as a supported rules file name
2025-06-25 14:14:49 -03:00
Bennet Bo Fenner
224de2ec6c settings: Remove version fields (#33372)
This cleans up our settings to not include any `version` fields, as we
have an actual settings migrator now.

This PR removes `language_models > anthropic > version`,
`language_models > openai > version` and `agent > version`.

We had migration paths in the code for a long time, so in practice
almost everyone should be using the latest version of these settings.


Release Notes:

- Remove `version` fields in settings for `agent`, `language_models >
anthropic`, `language_models > openai`. Your settings will automatically
be migrated. If you're running into issues with this open an issue
[here](https://github.com/zed-industries/zed/issues)
2025-06-25 19:05:29 +02:00
Kirill Bulatov
c0acd8e8b1 Add language server control tool into the status bar (#32490)
Release Notes:

- Added the language server control tool into the status bar

---------

Co-authored-by: Nate Butler <iamnbutler@gmail.com>
2025-06-25 19:57:28 +03:00
Cole Miller
91c9281cea Default to cargo-zigbuild for ZED_BUILD_REMOTE_SERVER (#33391)
Follow-up to #31467. `cargo-zigbuild` will be installed if it's not
there already, but you have to install Zig yourself. Pass
`ZED_BUILD_REMOTE_SERVER=cross` to use the old way.

Release Notes:

- N/A
2025-06-25 16:49:37 +00:00
Peter Tripp
84494ab26b Make ctrl-alt-b / cmd-alt-b toggle right dock (#33190)
Closes: https://github.com/zed-industries/zed/issues/33147

In VSCode ctrl-alt-b / cmd-alt-b toggles the right dock. Zed should
follow this behavior.

See also:
- https://github.com/zed-industries/zed/pull/31630

Release Notes:

- N/A
2025-06-25 12:48:46 -04:00
Peter Tripp
93d670af13 Fix empty code actions menu trapping cursor (#33386)
Closes: https://github.com/zed-industries/zed/issues/33382
Follow-up to: https://github.com/zed-industries/zed/pull/32579

CC: @ConradIrwin @Anthony-Eid 

Release Notes:

- Fixed an issue with empty code actions menu locking the cursor
(Preview Only)
2025-06-25 12:48:15 -04:00
Peter Tripp
7d087ea5d2 docs: Improve visual-customization.md docs for Zed prompts (#33254)
Release Notes:

- N/A
2025-06-25 12:48:03 -04:00
ddoemonn
19c9fb3118 Allow multiple Markdown preview tabs (#32859)
Closes #32791


https://github.com/user-attachments/assets/8cb90e3d-ef7b-407f-b78b-7ba4ff6d8df2

Release Notes:
- Allowed multiple Markdown preview tabs
2025-06-25 16:43:00 +00:00
Oleksiy Syvokon
b0bab0bf9a agent: Prevent use of disabled tools (#33392)
The agent now checks if a tool is enabled in the current profile before
calling it. Previously, the agent could still call disabled tools, which
commonly happened after switching profiles in the middle of a thread.

Release Notes:

- Fixed a bug where the agent could use disabled tools sometimes
2025-06-25 16:30:22 +00:00
CharlesChen0823
630a326a07 file_finder: Fix create wrong file in multiple worktree (#33139)
When open multiple worktree, using `file_finder` to create a new file
shoud respect current focused worktree.
test case:
```
project:
   worktree A
        file1
   worktree B
        file2 <-  focused
```
when focused `file2`, `ctrl-p` toggle `file_finder` to create `file3`
should exists in worktreeB.


I try add test case for `CreateNew` in file_finder, but found not
worked, if you help me, I can try add this test case.

Release Notes:

- Fixed file finder selecting wrong worktree when creating a file
2025-06-25 19:17:41 +03:00
Joseph T. Lyons
6848073c38 Bump Zed to v0.194 (#33390)
Release Notes:

-N/A
2025-06-25 16:10:11 +00:00
Smit Barmase
eb51041154 debugger_ui: Fix variable completion accept in console appends the whole word (#33378)
Closes #32959

Release Notes:

- Fixed the issue where accepting variable completion in the Debugger
would append the entire variable name instead of the remaining part.
2025-06-25 19:54:08 +05:30
Sarmad Gulzar
308debe47f terminal: Fix trailing single quote included when opening link from terminal (#33376)
Closes #33210 

Release Notes:

- Fixed an issue where a trailing single quote was included when opening
a link from the terminal.
2025-06-25 19:51:33 +05:30
Vladimir Kuznichenkov
0905255fd1 bedrock: Add prompt caching support (#33194)
Closes https://github.com/zed-industries/zed/issues/33221

Bedrock has similar to anthropic caching api, if we want to cache
messages up to a certain point, we should add a special block into that
message.

Additionally, we can cache tools definition by adding cache point block
after tools spec.

See: [Bedrock User Guide: Prompt
Caching](https://docs.aws.amazon.com/bedrock/latest/userguide/prompt-caching.html#prompt-caching-models)

Release Notes:

- bedrock: Added prompt caching support

---------

Co-authored-by: Oleksiy Syvokon <oleksiy@zed.dev>
2025-06-25 17:15:13 +03:00
Bennet Bo Fenner
59aeede50d vercel: Use proper model identifiers and add image support (#33377)
Follow up to previous PRs:
- Return `true` in `supports_images` - v0 supports images already
- Rename model id to match the exact version of the model `v0-1.5-md`
(For now we do not expose `sm`/`lg` variants since they seem not to be
available via the API)
- Provide autocompletion in settings for using `vercel` as a `provider`

Release Notes:

- N/A
2025-06-25 13:26:41 +00:00
Bennet Bo Fenner
18f1221a44 vercel: Reuse existing OpenAI code (#33362)
Follow up to #33292

Since Vercel's API is OpenAI compatible, we can reuse a bunch of code.

Release Notes:

- N/A
2025-06-25 15:04:43 +02:00
Rodrigo Freire
c979452c2d Implement indent conversion editor commands (#32340)
## Description of Feature or Change

Zed currently lacks a built-in way to convert a file’s indentation style
on the fly. While it's possible to change indentation behavior via
global or language-specific settings, these changes are persistent and
broad in scope as they apply to all files or all files of a given
language. We believe this could be improved for quick one-off
adjustments to specific files.

This PR introduces two new editor commands:
`Editor::convert_indentation_to_spaces` and
`Editor::convert_indentation_to_tabs`. These commands allow users to
convert the indentation of either the entire buffer or a selection of
lines, to spaces or tabs. Indentation levels are preserved, and any
mixed whitespace lines are properly normalized.

This feature is inspired by VS Code’s "Convert Indentation to
Tabs/Spaces" commands, but offers faster execution and supports
selection-based conversion, making it more flexible for quick formatting
changes.

## Implementation Details

To enable selection-based indentation conversion, we initially
considered reusing the existing `Editor::manipulate_lines` function,
which handles selections for line-based manipulations. However, this
method was designed specifically for operations like sorting or
reversing lines, and does not allow modifications to the line contents
themselves.

To address this limitation, we refactored the method into a more
flexible version: `Editor::manipulate_generic_lines`. This new method
passes a reference to the selected text directly into a callback, giving
the callback full control over how to process and construct the
resulting lines. The callback returns a `String` containing the modified
text, as well as the number of lines before and after the
transformation. These counts are computed using `.len()` on the line
vectors during manipulation, which is more efficient than calculating
them after the fact.


```rust
fn manipulate_generic_lines<M>(
  &mut self,
  window: &mut Window,
  cx: &mut Context<Self>,
  mut manipulate: M,
) where
   M: FnMut(&str) -> (String, usize, usize),
 {
   // ... Get text from buffer.text_for_range() ...
   let (new_text, lines_before, lines_after) = manipulate(&text);
   // ...
``` 

We now introduce two specialized methods:
`Editor::manipulate_mutable_lines` and
`Editor::manipulate_immutable_lines`. Each editor command selects the
appropriate method based on whether it needs to modify line contents or
simply reorder them. This distinction is important for performance: when
line contents remain unchanged, working with an immutable reference as
`&mut Vec<&str>` is both faster and more memory-efficient than using an
owned `&mut Vec<String>`.

## Demonstration


https://github.com/user-attachments/assets/e50b37ea-a128-4c2a-b252-46c3c4530d97



Release Notes:

- Added `editor::ConvertIndentationToSpaces` and
`editor::ConvertIndentationToTabs` actions to change editor indents

---------

Co-authored-by: Pedro Silveira <pedroruanosilveira@tecnico.ulisboa.pt>
2025-06-25 12:02:42 +00:00
Shardul Vaidya
4396ac9dd6 bedrock: DeepSeek does not support receiving Reasoning Blocks (#33326)
Closes #32341

Release Notes:

- Fixed DeepSeek R1 errors for reasoning blocks being sent back to the model.
2025-06-25 14:51:25 +03:00
Vladimir Kuznichenkov
c6ff58675f bedrock: Fix empty tool input on project diagnostic in bedrock (#33369)
Bedrock [do not accept][1] `null` as a JSON value input for the tool
call when called back.

Instead of passing null, we will pass back an empty object, which is
accepted by API

Closes #33204

Release Notes:

- Fixed project diagnostic tool call for bedrock

[1]:
https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_ToolUseBlock.html
2025-06-25 14:28:36 +03:00
Bennet Bo Fenner
1c6b4712a3 agent: Fix issue where unconfigured MCP extensions would not start server (#33365)
Release Notes:

- agent: Fix an issue where MCP servers that were provided by extensions
would sometimes not start up
2025-06-25 11:48:38 +02:00
Umesh Yadav
108162423d language_models: Emit UsageUpdate events for token usage in DeepSeek and OpenAI (#33242)
Closes #ISSUE

Release Notes:

- N/A
2025-06-25 09:42:30 +02:00
Vladimir Kuznichenkov
098896146e bedrock: Fix subsequent bedrock tool calls fail (#33174)
Closes #30714

Bedrock converse api expect to see tool options if at least one tool was
used in conversation in the past messages.

Right now if `LanguageModelToolChoice::None` isn't supported edit agent
[remove][1] tools from request. That point breaks Converse API of
Bedrock. As was proposed in [the issue][2] we won't drop tool choose but
instead will deny any of them if model will respond with a tool choose.

[1]:
fceba6c795/crates/assistant_tools/src/edit_agent.rs (L703)
[2]:
https://github.com/zed-industries/zed/issues/30714#issuecomment-2886422716

Release Notes:

- Fixed bedrock tool calls in edit mode
2025-06-25 10:37:07 +03:00
Michael Sloan
96409965e4 Cleanup handling of surrounding word logic, fixing crash in editor::SelectAllMatches (#33353)
This reduces code complexity and avoids unnecessary roundtripping
through `DisplayPoint`. Hopefully this doesn't cause behavior changes,
but has one known behavior improvement:

`clip_at_line_ends` logic caused `is_inside_word` to return false when
on a word at the end of the line. In vim mode, this caused
`select_all_matches` to not select words at the end of lines, and in
some cases crashes due to not finding any selections.

Closes #29823

Release Notes:

- N/A
2025-06-24 23:18:35 -06:00
marton csutora
014f93008a Make remote mkdir shell-independent for compatibility (#32997)
- Closes: #30962 

Nushell does not support mkdir -p
So invoke sh -c "mkdir -p" instead which will also work under nushell.

Release Notes:

- Fixed ssh remotes running Nushell (and possibly other non
posix-compliant shells)

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-06-25 03:21:59 +00:00
Cole Miller
17774b17fb debugger: Add a tooltip to the session picker with the session ID (#33331)
This helps correlate sessions in the picker with entries in the debug
adapter logs view.

Release Notes:

- N/A
2025-06-25 02:20:31 +00:00
Remco Smits
cf086544e3 debugger: Add support for completion triggers in debug console (#33211)
Release Notes:

- Debugger: Add support for completion triggers in debug console
2025-06-24 22:19:00 -04:00
Michael Sloan
aa330fcf2c Use background task for settings migrations + notify about errors (#30009)
Release Notes:

- N/A
2025-06-24 23:54:03 +00:00
vipex
be95716e94 helix: Prevent cursor move on entering insert mode (#33201)
Closes #33061


https://github.com/user-attachments/assets/3b3e146e-7c12-412e-b4dd-c70411891b9e

Release Notes:

- Fixed cursor unexpectedly moving when entering/exiting insert mode in
Helix mode, making the behavior consistent with the Helix editor.
2025-06-24 15:20:14 -06:00
waffle
f738fbd4f8 gpui: Disable rounding in the layout engine (#31836)
Rounding broke (among other things, probably) pixel-perfect image
rendering with non-power-of-two scaling factor.

An example which reproduces the problem can be found
[here](https://github.com/WaffleLapkin/gpui_taffy_rounding_whyyyyy).

How it looks with `gpui` from `main`:
![2025-05-31
11:34:25+CEST](https://github.com/user-attachments/assets/2cb19312-6ba6-4e80-8072-f89ddedff77b)

How it looks with this patch:
![2025-05-31
11:35:28+CEST](https://github.com/user-attachments/assets/114b52a9-58c0-4600-871c-a20eceb7179e)

Both screenshots are made on kde+wayland with magnification using kde's
built-in magnification (`Meta`+`+`, `Meta`+`-`). Note that screenshot
apps have a high chance of lying 🙃

The image itself is 400 by 300 pixels of red/green checkerboard pattern
made specifically to exaggerate scaling issues.

Release Notes:

- N/A
2025-06-24 20:28:57 +00:00
Joseph T. Lyons
0c78a115de Patch panic around pinned tab count (#33335)
After much investigation, I have not been able to track down what is
causing [this
panic](https://github.com/zed-industries/zed/issues/33342). I'm clamping
the value for now, because a bug is better than a crash. Hopefully
someone finds reproduction steps, and I will implement a proper fix.

Release Notes:

- N/A
2025-06-24 20:24:06 +00:00
Michael Sloan
9427526a41 gpui: Clear the element arena after presenting the frame (#33338)
This is an easy way to shave some microseconds off the critical path for
frame rendering. On my machine this reduces typical frame rendering
latency by ~100 microseconds, probably quite a bit more on slower
machines.

Here is how long it typically takes to drop elements from the arena,
from a fairly brief run:


![image](https://github.com/user-attachments/assets/65cfd911-eccf-4393-887d-8cad2cd27148)

Release Notes:

- N/A
2025-06-24 19:43:33 +00:00
Kirill Bulatov
eec26c9a41 Add initial docs for editor diagnostics (#33325)
Release Notes:

- N/A
2025-06-24 22:21:27 +03:00
Anthony Eid
3c0475d182 debugger: Reorder step icons to be consistent with other editors (#33330)
Closes #33303

Release Notes:

- debugger: Swap step in/out icon positions in debug panel to be
consistent with other editors
2025-06-24 19:15:34 +00:00
Anthony Eid
fc1fc264ec debugger: Generate inline values based on debugger.scm file (#33081)
## Context

To support inline values a language will have to implement their own
provider trait that walks through tree sitter nodes. This is overly
complicated, hard to accurately implement for each language, and lacks
proper extension support.

This PR switches to a singular inline provider that uses a language's
`debugger.scm` query field to capture variables and scopes. The inline
provider is able to use this information to generate inlays that take
scope into account and work with any language that defines a debugger
query file.

### Todos
- [x] Implement a utility test function to easily test inline values
- [x] Generate inline values based on captures
- [x] Reimplement Python, Rust, and Go support
- [x] Take scope into account when iterating through variable captures
- [x] Add tests for Go inline values
- [x] Remove old inline provider code and trait implementations

Release Notes:

- debugger: Generate inline values based on a language debugger.scm file
2025-06-24 18:24:43 +00:00
Peter Tripp
800b925fd7 Improve Atom keymap (#33329)
Closes: https://github.com/zed-industries/zed/issues/33256

Move some Editor keymap entries into `Editor && mode == full`

Release Notes:

- N/A
2025-06-24 18:02:07 +00:00
fantacell
95cf153ad7 Simulate helix line wrapping (#32763)
In helix the `f`, `F`, `t`, `T`, left and right motions wrap lines. I
added that by default.

Release Notes:

- vim: The `use_multiline_find` setting is replaced by binding to the
correct action in the keymap:
    ```
"f": ["vim::PushFindForward", { "before": false, "multiline": true }],
"t": ["vim::PushFindForward", { "before": true, "multiline": true }],
"shift-f": ["vim::PushFindBackward", { "after": false, "multiline": true
}],
"shift-t": ["vim::PushFindBackward", { "after": true, "multiline": true
}],
    ```
- helix: `f`/`t`/`shift-f`/`shift-t`/`h`/`l`/`left`/`right` are now
multiline by default (like helix)
2025-06-24 10:51:41 -06:00
Bennet Bo Fenner
7be57baef0 agent: Fix issue with Anthropic thinking models (#33317)
cc @osyvokon 

We were seeing a bunch of errors in our backend when people were using
Claude models with thinking enabled.

In the logs we would see
> an error occurred while interacting with the Anthropic API:
invalid_request_error: messages.x.content.0.type: Expected `thinking` or
`redacted_thinking`, but found `text`. When `thinking` is enabled, a
final `assistant` message must start with a thinking block (preceeding
the lastmost set of `tool_use` and `tool_result` blocks). We recommend
you include thinking blocks from previous turns. To avoid this
requirement, disable `thinking`. Please consult our documentation at
https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking

However, this issue did not occur frequently and was not easily
reproducible. Turns out it was triggered by us not correctly handling
[Redacted Thinking
Blocks](https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking#thinking-redaction).

I could constantly reproduce this issue by including this magic string:
`ANTHROPIC_MAGIC_STRING_TRIGGER_REDACTED_THINKING_46C9A13E193C177646C7398A98432ECCCE4C1253D5E2D82641AC0E52CC2876CB
` in the request, which forces `claude-3-7-sonnet` to emit redacted
thinking blocks (confusingly the magic string does not seem to be
working for `claude-sonnet-4`). As soon as we hit a tool call Anthropic
would return an error.

Thanks to @osyvokon for pointing me in the right direction 😄!


Release Notes:

- agent: Fixed an issue where Anthropic models would sometimes return an
error when thinking was enabled
2025-06-24 16:23:59 +00:00
335 changed files with 16828 additions and 9731 deletions

View File

@@ -29,6 +29,7 @@ jobs:
outputs:
run_tests: ${{ steps.filter.outputs.run_tests }}
run_license: ${{ steps.filter.outputs.run_license }}
run_docs: ${{ steps.filter.outputs.run_docs }}
runs-on:
- ubuntu-latest
steps:
@@ -58,6 +59,11 @@ jobs:
else
echo "run_tests=false" >> $GITHUB_OUTPUT
fi
if [[ $(git diff --name-only $COMPARE_REV ${{ github.sha }} | grep '^docs/') ]]; then
echo "run_docs=true" >> $GITHUB_OUTPUT
else
echo "run_docs=false" >> $GITHUB_OUTPUT
fi
if [[ $(git diff --name-only $COMPARE_REV ${{ github.sha }} | grep '^Cargo.lock') ]]; then
echo "run_license=true" >> $GITHUB_OUTPUT
else
@@ -198,7 +204,9 @@ jobs:
timeout-minutes: 60
name: Check docs
needs: [job_spec]
if: github.repository_owner == 'zed-industries'
if: |
github.repository_owner == 'zed-industries' &&
(needs.job_spec.outputs.run_tests == 'true' || needs.job_spec.outputs.run_docs == 'true')
runs-on:
- buildjet-8vcpu-ubuntu-2204
steps:
@@ -452,8 +460,10 @@ jobs:
RET_CODE=0
# Always check style
[[ "${{ needs.style.result }}" != 'success' ]] && { RET_CODE=1; echo "style tests failed"; }
[[ "${{ needs.check_docs.result }}" != 'success' ]] && { RET_CODE=1; echo "docs checks failed"; }
if [[ "${{ needs.job_spec.outputs.run_docs }}" == "true" ]]; then
[[ "${{ needs.check_docs.result }}" != 'success' ]] && { RET_CODE=1; echo "docs checks failed"; }
fi
# Only check test jobs if they were supposed to run
if [[ "${{ needs.job_spec.outputs.run_tests }}" == "true" ]]; then
[[ "${{ needs.workspace_hack.result }}" != 'success' ]] && { RET_CODE=1; echo "Workspace Hack failed"; }

View File

@@ -1,34 +0,0 @@
name: Delete Mediafire Comments
on:
issue_comment:
types: [created]
permissions:
issues: write
jobs:
delete_comment:
if: github.repository_owner == 'zed-industries'
runs-on: ubuntu-latest
steps:
- name: Check for specific strings in comment
id: check_comment
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
with:
script: |
const comment = context.payload.comment.body;
const triggerStrings = ['www.mediafire.com'];
return triggerStrings.some(triggerString => comment.includes(triggerString));
- name: Delete comment if it contains any of the specific strings
if: steps.check_comment.outputs.result == 'true'
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
with:
script: |
const commentId = context.payload.comment.id;
await github.rest.issues.deleteComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: commentId
});

View File

@@ -7,7 +7,7 @@ on:
pull_request:
branches:
- "**"
types: [opened, synchronize, reopened, labeled]
types: [synchronize, reopened, labeled]
workflow_dispatch:
@@ -25,16 +25,6 @@ env:
ZED_EVAL_TELEMETRY: 1
jobs:
# This is a no-op job that we run to prevent GitHub from marking the workflow
# as failed for PRs that don't have the `run-eval` label.
noop:
name: No-op
runs-on: ubuntu-latest
if: github.repository_owner == 'zed-industries'
steps:
- name: No-op
run: echo "Nothing to do"
run_eval:
timeout-minutes: 60
name: Run Agent Eval

View File

@@ -40,6 +40,7 @@
},
"file_types": {
"Dockerfile": ["Dockerfile*[!dockerignore]"],
"JSONC": ["assets/**/*.json", "renovate.json"],
"Git Ignore": ["dockerignore"]
},
"hard_tabs": false,

63
Cargo.lock generated
View File

@@ -14,6 +14,7 @@ dependencies = [
"gpui",
"language",
"project",
"proto",
"release_channel",
"smallvec",
"ui",
@@ -77,6 +78,7 @@ dependencies = [
"language",
"language_model",
"log",
"parking_lot",
"paths",
"postage",
"pretty_assertions",
@@ -109,18 +111,11 @@ dependencies = [
name = "agent_settings"
version = "0.1.0"
dependencies = [
"anthropic",
"anyhow",
"collections",
"deepseek",
"fs",
"gpui",
"language_model",
"lmstudio",
"log",
"mistral",
"ollama",
"open_ai",
"paths",
"schemars",
"serde",
@@ -4161,6 +4156,7 @@ dependencies = [
"paths",
"serde",
"serde_json",
"shlex",
"task",
"util",
"workspace-hack",
@@ -4314,6 +4310,7 @@ version = "0.1.0"
dependencies = [
"alacritty_terminal",
"anyhow",
"bitflags 2.9.0",
"client",
"collections",
"command_palette_hooks",
@@ -4348,6 +4345,7 @@ dependencies = [
"terminal_view",
"theme",
"tree-sitter",
"tree-sitter-go",
"tree-sitter-json",
"ui",
"unindent",
@@ -9019,11 +9017,13 @@ dependencies = [
"collections",
"copilot",
"editor",
"feature_flags",
"futures 0.3.31",
"gpui",
"itertools 0.14.0",
"language",
"lsp",
"picker",
"project",
"release_channel",
"serde_json",
@@ -9241,7 +9241,7 @@ dependencies = [
[[package]]
name = "libwebrtc"
version = "0.3.10"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=80bb8f4c9112789f7c24cc98d8423010977806a6#80bb8f4c9112789f7c24cc98d8423010977806a6"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4#d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4"
dependencies = [
"cxx",
"jni",
@@ -9321,7 +9321,7 @@ checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856"
[[package]]
name = "livekit"
version = "0.7.8"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=80bb8f4c9112789f7c24cc98d8423010977806a6#80bb8f4c9112789f7c24cc98d8423010977806a6"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4#d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4"
dependencies = [
"chrono",
"futures-util",
@@ -9344,7 +9344,7 @@ dependencies = [
[[package]]
name = "livekit-api"
version = "0.4.2"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=80bb8f4c9112789f7c24cc98d8423010977806a6#80bb8f4c9112789f7c24cc98d8423010977806a6"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4#d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4"
dependencies = [
"futures-util",
"http 0.2.12",
@@ -9368,7 +9368,7 @@ dependencies = [
[[package]]
name = "livekit-protocol"
version = "0.3.9"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=80bb8f4c9112789f7c24cc98d8423010977806a6#80bb8f4c9112789f7c24cc98d8423010977806a6"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4#d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4"
dependencies = [
"futures-util",
"livekit-runtime",
@@ -9385,7 +9385,7 @@ dependencies = [
[[package]]
name = "livekit-runtime"
version = "0.4.0"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=80bb8f4c9112789f7c24cc98d8423010977806a6#80bb8f4c9112789f7c24cc98d8423010977806a6"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4#d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4"
dependencies = [
"tokio",
"tokio-stream",
@@ -13167,6 +13167,7 @@ dependencies = [
"thiserror 2.0.12",
"urlencoding",
"util",
"which 6.0.3",
"workspace-hack",
]
@@ -14554,34 +14555,25 @@ dependencies = [
"serde_json",
"serde_json_lenient",
"smallvec",
"streaming-iterator",
"tree-sitter",
"tree-sitter-json",
"unindent",
"util",
"workspace-hack",
"zlog",
]
[[package]]
name = "settings_ui"
version = "0.1.0"
dependencies = [
"collections",
"command_palette",
"command_palette_hooks",
"component",
"db",
"editor",
"feature_flags",
"fs",
"fuzzy",
"gpui",
"log",
"menu",
"paths",
"project",
"schemars",
"search",
"serde",
"settings",
"theme",
@@ -15535,6 +15527,18 @@ version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0193cc4331cfd2f3d2011ef287590868599a2f33c3e69bc22c1a3d3acf9e02fb"
[[package]]
name = "svg_preview"
version = "0.1.0"
dependencies = [
"editor",
"file_icons",
"gpui",
"ui",
"workspace",
"workspace-hack",
]
[[package]]
name = "svgtypes"
version = "0.15.3"
@@ -16046,7 +16050,6 @@ dependencies = [
"indexmap",
"log",
"palette",
"rust-embed",
"serde",
"serde_json",
"serde_json_lenient",
@@ -16877,8 +16880,7 @@ dependencies = [
[[package]]
name = "tree-sitter-python"
version = "0.23.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d065aaa27f3aaceaf60c1f0e0ac09e1cb9eb8ed28e7bcdaa52129cffc7f4b04"
source = "git+https://github.com/zed-industries/tree-sitter-python?rev=218fcbf3fda3d029225f3dec005cb497d111b35e#218fcbf3fda3d029225f3dec005cb497d111b35e"
dependencies = [
"cc",
"tree-sitter-language",
@@ -17057,7 +17059,6 @@ dependencies = [
"gpui_macros",
"icons",
"itertools 0.14.0",
"log",
"menu",
"serde",
"settings",
@@ -17440,11 +17441,8 @@ name = "vercel"
version = "0.1.0"
dependencies = [
"anyhow",
"futures 0.3.31",
"http_client",
"schemars",
"serde",
"serde_json",
"strum 0.27.1",
"workspace-hack",
]
@@ -18298,7 +18296,7 @@ dependencies = [
[[package]]
name = "webrtc-sys"
version = "0.3.7"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=80bb8f4c9112789f7c24cc98d8423010977806a6#80bb8f4c9112789f7c24cc98d8423010977806a6"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4#d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4"
dependencies = [
"cc",
"cxx",
@@ -18311,7 +18309,7 @@ dependencies = [
[[package]]
name = "webrtc-sys-build"
version = "0.3.6"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=80bb8f4c9112789f7c24cc98d8423010977806a6#80bb8f4c9112789f7c24cc98d8423010977806a6"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4#d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4"
dependencies = [
"fs2",
"regex",
@@ -19931,7 +19929,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.193.0"
version = "0.194.0"
dependencies = [
"activity_indicator",
"agent",
@@ -20036,6 +20034,7 @@ dependencies = [
"snippet_provider",
"snippets_ui",
"supermaven",
"svg_preview",
"sysinfo",
"tab_switcher",
"task",

View File

@@ -95,6 +95,7 @@ members = [
"crates/markdown_preview",
"crates/media",
"crates/menu",
"crates/svg_preview",
"crates/migrator",
"crates/mistral",
"crates/multi_buffer",
@@ -304,6 +305,7 @@ lmstudio = { path = "crates/lmstudio" }
lsp = { path = "crates/lsp" }
markdown = { path = "crates/markdown" }
markdown_preview = { path = "crates/markdown_preview" }
svg_preview = { path = "crates/svg_preview" }
media = { path = "crates/media" }
menu = { path = "crates/menu" }
migrator = { path = "crates/migrator" }
@@ -595,7 +597,7 @@ tree-sitter-html = "0.23"
tree-sitter-jsdoc = "0.23"
tree-sitter-json = "0.24"
tree-sitter-md = { git = "https://github.com/tree-sitter-grammars/tree-sitter-markdown", rev = "9a23c1a96c0513d8fc6520972beedd419a973539" }
tree-sitter-python = "0.23"
tree-sitter-python = { git = "https://github.com/zed-industries/tree-sitter-python", rev = "218fcbf3fda3d029225f3dec005cb497d111b35e" }
tree-sitter-regex = "0.24"
tree-sitter-ruby = "0.23"
tree-sitter-rust = "0.24"

View File

@@ -1,6 +1,6 @@
# syntax = docker/dockerfile:1.2
FROM rust:1.87-bookworm as builder
FROM rust:1.88-bookworm as builder
WORKDIR app
COPY . .

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-arrow-down10-icon lucide-arrow-down-1-0"><path d="m3 16 4 4 4-4"/><path d="M7 20V4"/><path d="M17 10V4h-2"/><path d="M15 10h4"/><rect x="15" y="14" width="4" height="6" ry="2"/></svg>

After

Width:  |  Height:  |  Size: 386 B

View File

@@ -0,0 +1,3 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.75776 5.50003H8.49988C8.70769 5.50003 8.89518 5.62971 8.95455 5.82346C9.04049 6.01876 8.9858 6.23906 8.82956 6.37656L4.82971 9.87643C4.65315 10.0295 4.39488 10.042 4.20614 9.90455C4.01724 9.76705 3.94849 9.51706 4.04052 9.30301L5.24219 6.49999H3.48601C3.2918 6.49999 3.10524 6.37031 3.03197 6.17657C2.9587 5.98126 3.014 5.76096 3.1708 5.62346L7.17018 2.12375C7.34674 1.97001 7.60454 1.95829 7.7936 2.09547C7.98265 2.23275 8.0514 2.48218 7.95922 2.69695L6.75776 5.50003Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 601 B

View File

@@ -0,0 +1,12 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 3L7 4" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9 4L10 3" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.002 6V5.51658C5.98992 5.32067 6.03266 5.12502 6.12762 4.94143C6.22259 4.75784 6.36781 4.59012 6.55453 4.44839C6.74125 4.30666 6.9656 4.19386 7.21403 4.1168C7.46246 4.03973 7.72983 4 8 4C8.27017 4 8.53754 4.03973 8.78597 4.1168C9.0344 4.19386 9.25875 4.30666 9.44547 4.44839C9.63219 4.59012 9.77741 4.75784 9.87238 4.94143C9.96734 5.12502 10.0101 5.32067 9.998 5.51658V6" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 13C6.35 13 5 11.5462 5 9.76923V8.15385C5 7.58261 5.21071 7.03477 5.58579 6.63085C5.96086 6.22692 6.46957 6 7 6H9C9.53043 6 10.0391 6.22692 10.4142 6.63085C10.7893 7.03477 11 7.58261 11 8.15385V9.76923C11 11.5462 9.65 13 8 13Z" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5 6.16663C3.90652 6.06663 3 5.21663 3 4.16663" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5 9H3" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3 13C3 11.95 3.89474 11.05 5 11" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13 4C13 5.05 12.0857 5.9 11 6" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13 9H11" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11 11C12.1053 11.05 13 11.95 13 13" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.84265 10.7778C4.39206 11.6001 5.17295 12.241 6.08658 12.6194C7.00021 12.9978 8.00555 13.0969 8.97545 12.9039C9.94535 12.711 10.8363 12.2348 11.5355 11.5355C12.2348 10.8363 12.711 9.94535 12.9039 8.97545C13.0969 8.00555 12.9978 7.00021 12.6194 6.08658C12.241 5.17295 11.6001 4.39206 10.7778 3.84265C9.9556 3.29324 8.9889 3 8 3C6.60219 3.00526 5.26054 3.55068 4.25556 4.52222L3 5.77778" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3 3V6H6" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 685 B

View File

@@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 13C10.7614 13 13 10.7614 13 8C13 5.23858 10.7614 3 8 3C5.23858 3 3 5.23858 3 8C3 10.7614 5.23858 13 8 13Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5 5L11 11" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 409 B

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-scroll-text-icon lucide-scroll-text"><path d="M15 12h-5"/><path d="M15 8h-5"/><path d="M19 17V5a2 2 0 0 0-2-2H4"/><path d="M8 21h12a2 2 0 0 0 2-2v-1a1 1 0 0 0-1-1H11a1 1 0 0 0-1 1v1a2 2 0 1 1-4 0V5a2 2 0 1 0-4 0v2a1 1 0 0 0 1 1h3"/></svg>

After

Width:  |  Height:  |  Size: 441 B

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-split-icon lucide-split"><path d="M16 3h5v5"/><path d="M8 3H3v5"/><path d="M12 22v-8.3a4 4 0 0 0-1.172-2.872L3 3"/><path d="m15 9 6-6"/></svg>

After

Width:  |  Height:  |  Size: 345 B

View File

@@ -41,7 +41,8 @@
"shift-f11": "debugger::StepOut",
"f11": "zed::ToggleFullScreen",
"ctrl-alt-z": "edit_prediction::RateCompletions",
"ctrl-shift-i": "edit_prediction::ToggleMenu"
"ctrl-shift-i": "edit_prediction::ToggleMenu",
"ctrl-alt-l": "lsp_tool::ToggleMenu"
}
},
{
@@ -243,8 +244,8 @@
"ctrl-alt-e": "agent::RemoveAllContext",
"ctrl-shift-e": "project_panel::ToggleFocus",
"ctrl-shift-enter": "agent::ContinueThread",
"alt-enter": "agent::ContinueWithBurnMode",
"ctrl-alt-b": "agent::ToggleBurnMode"
"super-ctrl-b": "agent::ToggleBurnMode",
"alt-enter": "agent::ContinueWithBurnMode"
}
},
{
@@ -490,13 +491,27 @@
"ctrl-k r": "editor::RevealInFileManager",
"ctrl-k p": "editor::CopyPath",
"ctrl-\\": "pane::SplitRight",
"ctrl-k v": "markdown::OpenPreviewToTheSide",
"ctrl-shift-v": "markdown::OpenPreview",
"ctrl-alt-shift-c": "editor::DisplayCursorNames",
"alt-.": "editor::GoToHunk",
"alt-,": "editor::GoToPreviousHunk"
}
},
{
"context": "Editor && extension == md",
"use_key_equivalents": true,
"bindings": {
"ctrl-k v": "markdown::OpenPreviewToTheSide",
"ctrl-shift-v": "markdown::OpenPreview"
}
},
{
"context": "Editor && extension == svg",
"use_key_equivalents": true,
"bindings": {
"ctrl-k v": "svg::OpenPreviewToTheSide",
"ctrl-shift-v": "svg::OpenPreview"
}
},
{
"context": "Editor && mode == full",
"bindings": {
@@ -904,7 +919,9 @@
"context": "BreakpointList",
"bindings": {
"space": "debugger::ToggleEnableBreakpoint",
"backspace": "debugger::UnsetBreakpoint"
"backspace": "debugger::UnsetBreakpoint",
"left": "debugger::PreviousBreakpointProperty",
"right": "debugger::NextBreakpointProperty"
}
},
{
@@ -1050,12 +1067,5 @@
"ctrl-tab": "pane::ActivateNextItem",
"ctrl-shift-tab": "pane::ActivatePreviousItem"
}
},
{
"context": "KeymapEditor",
"use_key_equivalents": true,
"bindings": {
"ctrl-f": "search::FocusSearch"
}
}
]

View File

@@ -47,7 +47,8 @@
"fn-f": "zed::ToggleFullScreen",
"ctrl-cmd-f": "zed::ToggleFullScreen",
"ctrl-cmd-z": "edit_prediction::RateCompletions",
"ctrl-cmd-i": "edit_prediction::ToggleMenu"
"ctrl-cmd-i": "edit_prediction::ToggleMenu",
"ctrl-cmd-l": "lsp_tool::ToggleMenu"
}
},
{
@@ -282,9 +283,9 @@
"cmd->": "assistant::QuoteSelection",
"cmd-alt-e": "agent::RemoveAllContext",
"cmd-shift-e": "project_panel::ToggleFocus",
"cmd-ctrl-b": "agent::ToggleBurnMode",
"cmd-shift-enter": "agent::ContinueThread",
"alt-enter": "agent::ContinueWithBurnMode",
"cmd-alt-b": "agent::ToggleBurnMode"
"alt-enter": "agent::ContinueWithBurnMode"
}
},
{
@@ -544,11 +545,25 @@
"cmd-k r": "editor::RevealInFileManager",
"cmd-k p": "editor::CopyPath",
"cmd-\\": "pane::SplitRight",
"cmd-k v": "markdown::OpenPreviewToTheSide",
"cmd-shift-v": "markdown::OpenPreview",
"ctrl-cmd-c": "editor::DisplayCursorNames"
}
},
{
"context": "Editor && extension == md",
"use_key_equivalents": true,
"bindings": {
"cmd-k v": "markdown::OpenPreviewToTheSide",
"cmd-shift-v": "markdown::OpenPreview"
}
},
{
"context": "Editor && extension == svg",
"use_key_equivalents": true,
"bindings": {
"cmd-k v": "svg::OpenPreviewToTheSide",
"cmd-shift-v": "svg::OpenPreview"
}
},
{
"context": "Editor && mode == full",
"use_key_equivalents": true,
@@ -587,7 +602,7 @@
"alt-cmd-o": ["projects::OpenRecent", { "create_new_window": false }],
"ctrl-cmd-o": ["projects::OpenRemote", { "from_existing_connection": false, "create_new_window": false }],
"ctrl-cmd-shift-o": ["projects::OpenRemote", { "from_existing_connection": true, "create_new_window": false }],
"alt-cmd-b": "branches::OpenRecent",
"cmd-ctrl-b": "branches::OpenRecent",
"ctrl-~": "workspace::NewTerminal",
"cmd-s": "workspace::Save",
"cmd-k s": "workspace::SaveWithoutFormat",
@@ -605,13 +620,14 @@
"cmd-8": ["workspace::ActivatePane", 7],
"cmd-9": ["workspace::ActivatePane", 8],
"cmd-b": "workspace::ToggleLeftDock",
"cmd-alt-b": "workspace::ToggleRightDock",
"cmd-r": "workspace::ToggleRightDock",
"cmd-j": "workspace::ToggleBottomDock",
"alt-cmd-y": "workspace::CloseAllDocks",
"cmd-shift-f": "pane::DeploySearch",
"cmd-shift-h": ["pane::DeploySearch", { "replace_enabled": true }],
"cmd-shift-t": "pane::ReopenClosedItem",
"cmd-k cmd-s": "zed::OpenKeymapEditor",
"cmd-k cmd-s": "zed::OpenKeymap",
"cmd-k cmd-t": "theme_selector::Toggle",
"cmd-t": "project_symbols::Toggle",
"cmd-p": "file_finder::Toggle",
@@ -964,7 +980,9 @@
"context": "BreakpointList",
"bindings": {
"space": "debugger::ToggleEnableBreakpoint",
"backspace": "debugger::UnsetBreakpoint"
"backspace": "debugger::UnsetBreakpoint",
"left": "debugger::PreviousBreakpointProperty",
"right": "debugger::NextBreakpointProperty"
}
},
{
@@ -1149,12 +1167,5 @@
"ctrl-tab": "pane::ActivateNextItem",
"ctrl-shift-tab": "pane::ActivatePreviousItem"
}
},
{
"context": "KeymapEditor",
"use_key_equivalents": true,
"bindings": {
"cmd-f": "search::FocusSearch"
}
}
]

View File

@@ -9,6 +9,13 @@
},
{
"context": "Editor",
"bindings": {
"ctrl-k ctrl-u": "editor::ConvertToUpperCase", // editor:upper-case
"ctrl-k ctrl-l": "editor::ConvertToLowerCase" // editor:lower-case
}
},
{
"context": "Editor && mode == full",
"bindings": {
"ctrl-shift-l": "language_selector::Toggle", // grammar-selector:show
"ctrl-|": "pane::RevealInProjectPanel", // tree-view:reveal-active-file
@@ -19,25 +26,20 @@
"shift-f3": ["editor::SelectPrevious", { "replace_newest": true }], //find-and-replace:find-previous
"alt-shift-down": "editor::AddSelectionBelow", // editor:add-selection-below
"alt-shift-up": "editor::AddSelectionAbove", // editor:add-selection-above
"ctrl-k ctrl-u": "editor::ConvertToUpperCase", // editor:upper-case
"ctrl-k ctrl-l": "editor::ConvertToLowerCase", // editor:lower-case
"ctrl-j": "editor::JoinLines", // editor:join-lines
"ctrl-shift-d": "editor::DuplicateLineDown", // editor:duplicate-lines
"ctrl-up": "editor::MoveLineUp", // editor:move-line-up
"ctrl-down": "editor::MoveLineDown", // editor:move-line-down
"ctrl-\\": "workspace::ToggleLeftDock", // tree-view:toggle
"ctrl-shift-m": "markdown::OpenPreviewToTheSide" // markdown-preview:toggle
}
},
{
"context": "Editor && mode == full",
"bindings": {
"ctrl-shift-m": "markdown::OpenPreviewToTheSide", // markdown-preview:toggle
"ctrl-r": "outline::Toggle" // symbols-view:toggle-project-symbols
}
},
{
"context": "BufferSearchBar",
"bindings": {
"f3": ["editor::SelectNext", { "replace_newest": true }], // find-and-replace:find-next
"shift-f3": ["editor::SelectPrevious", { "replace_newest": true }], //find-and-replace:find-previous
"ctrl-f3": "search::SelectNextMatch", // find-and-replace:find-next-selected
"ctrl-shift-f3": "search::SelectPreviousMatch" // find-and-replace:find-previous-selected
}

View File

@@ -8,7 +8,6 @@
"ctrl-shift-i": "agent::ToggleFocus",
"ctrl-l": "agent::ToggleFocus",
"ctrl-shift-l": "agent::ToggleFocus",
"ctrl-alt-b": "agent::ToggleFocus",
"ctrl-shift-j": "agent::OpenConfiguration"
}
},
@@ -42,7 +41,6 @@
"ctrl-shift-i": "workspace::ToggleRightDock",
"ctrl-l": "workspace::ToggleRightDock",
"ctrl-shift-l": "workspace::ToggleRightDock",
"ctrl-alt-b": "workspace::ToggleRightDock",
"ctrl-w": "workspace::ToggleRightDock", // technically should close chat
"ctrl-.": "agent::ToggleProfileSelector",
"ctrl-/": "agent::ToggleModelSelector",

View File

@@ -59,7 +59,8 @@
"alt->": "editor::MoveToEnd", // end-of-buffer
"ctrl-l": "editor::ScrollCursorCenterTopBottom", // recenter-top-bottom
"ctrl-s": "buffer_search::Deploy", // isearch-forward
"alt-^": "editor::JoinLines" // join-line
"alt-^": "editor::JoinLines", // join-line
"alt-q": "editor::Rewrap" // fill-paragraph
}
},
{

View File

@@ -9,6 +9,14 @@
},
{
"context": "Editor",
"bindings": {
"cmd-shift-backspace": "editor::DeleteToBeginningOfLine",
"cmd-k cmd-u": "editor::ConvertToUpperCase",
"cmd-k cmd-l": "editor::ConvertToLowerCase"
}
},
{
"context": "Editor && mode == full",
"bindings": {
"ctrl-shift-l": "language_selector::Toggle",
"cmd-|": "pane::RevealInProjectPanel",
@@ -19,26 +27,20 @@
"cmd-shift-g": ["editor::SelectPrevious", { "replace_newest": true }],
"ctrl-shift-down": "editor::AddSelectionBelow",
"ctrl-shift-up": "editor::AddSelectionAbove",
"cmd-shift-backspace": "editor::DeleteToBeginningOfLine",
"cmd-k cmd-u": "editor::ConvertToUpperCase",
"cmd-k cmd-l": "editor::ConvertToLowerCase",
"alt-enter": "editor::Newline",
"cmd-shift-d": "editor::DuplicateLineDown",
"ctrl-cmd-up": "editor::MoveLineUp",
"ctrl-cmd-down": "editor::MoveLineDown",
"cmd-\\": "workspace::ToggleLeftDock",
"ctrl-shift-m": "markdown::OpenPreviewToTheSide"
}
},
{
"context": "Editor && mode == full",
"bindings": {
"ctrl-shift-m": "markdown::OpenPreviewToTheSide",
"cmd-r": "outline::Toggle"
}
},
{
"context": "BufferSearchBar",
"bindings": {
"cmd-g": ["editor::SelectNext", { "replace_newest": true }],
"cmd-shift-g": ["editor::SelectPrevious", { "replace_newest": true }],
"cmd-f3": "search::SelectNextMatch",
"cmd-shift-f3": "search::SelectPreviousMatch"
}

View File

@@ -8,7 +8,6 @@
"cmd-shift-i": "agent::ToggleFocus",
"cmd-l": "agent::ToggleFocus",
"cmd-shift-l": "agent::ToggleFocus",
"cmd-alt-b": "agent::ToggleFocus",
"cmd-shift-j": "agent::OpenConfiguration"
}
},
@@ -43,7 +42,6 @@
"cmd-shift-i": "workspace::ToggleRightDock",
"cmd-l": "workspace::ToggleRightDock",
"cmd-shift-l": "workspace::ToggleRightDock",
"cmd-alt-b": "workspace::ToggleRightDock",
"cmd-w": "workspace::ToggleRightDock", // technically should close chat
"cmd-.": "agent::ToggleProfileSelector",
"cmd-/": "agent::ToggleModelSelector",

View File

@@ -59,7 +59,8 @@
"alt->": "editor::MoveToEnd", // end-of-buffer
"ctrl-l": "editor::ScrollCursorCenterTopBottom", // recenter-top-bottom
"ctrl-s": "buffer_search::Deploy", // isearch-forward
"alt-^": "editor::JoinLines" // join-line
"alt-^": "editor::JoinLines", // join-line
"alt-q": "editor::Rewrap" // fill-paragraph
}
},
{

View File

@@ -85,10 +85,10 @@
"[ {": ["vim::UnmatchedBackward", { "char": "{" }],
"] )": ["vim::UnmatchedForward", { "char": ")" }],
"[ (": ["vim::UnmatchedBackward", { "char": "(" }],
"f": ["vim::PushFindForward", { "before": false }],
"t": ["vim::PushFindForward", { "before": true }],
"shift-f": ["vim::PushFindBackward", { "after": false }],
"shift-t": ["vim::PushFindBackward", { "after": true }],
"f": ["vim::PushFindForward", { "before": false, "multiline": false }],
"t": ["vim::PushFindForward", { "before": true, "multiline": false }],
"shift-f": ["vim::PushFindBackward", { "after": false, "multiline": false }],
"shift-t": ["vim::PushFindBackward", { "after": true, "multiline": false }],
"m": "vim::PushMark",
"'": ["vim::PushJump", { "line": true }],
"`": ["vim::PushJump", { "line": false }],
@@ -210,7 +210,8 @@
"ctrl-w space": "editor::OpenExcerptsSplit",
"ctrl-w g space": "editor::OpenExcerptsSplit",
"ctrl-6": "pane::AlternateFile",
"ctrl-^": "pane::AlternateFile"
"ctrl-^": "pane::AlternateFile",
".": "vim::Repeat"
}
},
{
@@ -219,7 +220,6 @@
"ctrl-[": "editor::Cancel",
"escape": "editor::Cancel",
":": "command_palette::Toggle",
".": "vim::Repeat",
"c": "vim::PushChange",
"shift-c": "vim::ChangeToEndOfLine",
"d": "vim::PushDelete",
@@ -368,6 +368,10 @@
"escape": "editor::Cancel",
"ctrl-[": "editor::Cancel",
":": "command_palette::Toggle",
"left": "vim::WrappingLeft",
"right": "vim::WrappingRight",
"h": "vim::WrappingLeft",
"l": "vim::WrappingRight",
"shift-d": "vim::DeleteToEndOfLine",
"shift-j": "vim::JoinLines",
"y": "editor::Copy",
@@ -385,6 +389,10 @@
"shift-p": ["vim::Paste", { "before": true }],
"u": "vim::Undo",
"ctrl-r": "vim::Redo",
"f": ["vim::PushFindForward", { "before": false, "multiline": true }],
"t": ["vim::PushFindForward", { "before": true, "multiline": true }],
"shift-f": ["vim::PushFindBackward", { "after": false, "multiline": true }],
"shift-t": ["vim::PushFindBackward", { "after": true, "multiline": true }],
"r": "vim::PushReplace",
"s": "vim::Substitute",
"shift-s": "vim::SubstituteLine",
@@ -841,6 +849,25 @@
"shift-u": "git::UnstageAll"
}
},
{
"context": "Editor && mode == auto_height && VimControl",
"bindings": {
// TODO: Implement search
"/": null,
"?": null,
"#": null,
"*": null,
"n": null,
"shift-n": null
}
},
{
"context": "GitCommit > Editor && VimControl && vim_mode == normal",
"bindings": {
"ctrl-c": "menu::Cancel",
"escape": "menu::Cancel"
}
},
{
"context": "Editor && edit_prediction",
"bindings": {
@@ -852,14 +879,7 @@
{
"context": "MessageEditor > Editor && VimControl",
"bindings": {
"enter": "agent::Chat",
// TODO: Implement search
"/": null,
"?": null,
"#": null,
"*": null,
"n": null,
"shift-n": null
"enter": "agent::Chat"
}
},
{

View File

@@ -1720,6 +1720,11 @@
// }
// }
},
// Common language server settings.
"global_lsp_settings": {
// Whether to show the LSP servers button in the status bar.
"button": true
},
// Jupyter settings
"jupyter": {
"enabled": true
@@ -1734,7 +1739,6 @@
"default_mode": "normal",
"toggle_relative_line_numbers": false,
"use_system_clipboard": "always",
"use_multiline_find": false,
"use_smartcase_find": false,
"highlight_on_yank_duration": 200,
"custom_digraphs": {},

View File

@@ -21,6 +21,7 @@ futures.workspace = true
gpui.workspace = true
language.workspace = true
project.workspace = true
proto.workspace = true
smallvec.workspace = true
ui.workspace = true
util.workspace = true

View File

@@ -80,10 +80,13 @@ impl ActivityIndicator {
let this = cx.new(|cx| {
let mut status_events = languages.language_server_binary_statuses();
cx.spawn(async move |this, cx| {
while let Some((name, status)) = status_events.next().await {
while let Some((name, binary_status)) = status_events.next().await {
this.update(cx, |this: &mut ActivityIndicator, cx| {
this.statuses.retain(|s| s.name != name);
this.statuses.push(ServerStatus { name, status });
this.statuses.push(ServerStatus {
name,
status: LanguageServerStatusUpdate::Binary(binary_status),
});
cx.notify();
})?;
}
@@ -112,8 +115,76 @@ impl ActivityIndicator {
cx.subscribe(
&project.read(cx).lsp_store(),
|_, _, event, cx| match event {
LspStoreEvent::LanguageServerUpdate { .. } => cx.notify(),
|activity_indicator, _, event, cx| match event {
LspStoreEvent::LanguageServerUpdate { name, message, .. } => {
if let proto::update_language_server::Variant::StatusUpdate(status_update) =
message
{
let Some(name) = name.clone() else {
return;
};
let status = match &status_update.status {
Some(proto::status_update::Status::Binary(binary_status)) => {
if let Some(binary_status) =
proto::ServerBinaryStatus::from_i32(*binary_status)
{
let binary_status = match binary_status {
proto::ServerBinaryStatus::None => BinaryStatus::None,
proto::ServerBinaryStatus::CheckingForUpdate => {
BinaryStatus::CheckingForUpdate
}
proto::ServerBinaryStatus::Downloading => {
BinaryStatus::Downloading
}
proto::ServerBinaryStatus::Starting => {
BinaryStatus::Starting
}
proto::ServerBinaryStatus::Stopping => {
BinaryStatus::Stopping
}
proto::ServerBinaryStatus::Stopped => {
BinaryStatus::Stopped
}
proto::ServerBinaryStatus::Failed => {
let Some(error) = status_update.message.clone()
else {
return;
};
BinaryStatus::Failed { error }
}
};
LanguageServerStatusUpdate::Binary(binary_status)
} else {
return;
}
}
Some(proto::status_update::Status::Health(health_status)) => {
if let Some(health) =
proto::ServerHealth::from_i32(*health_status)
{
let health = match health {
proto::ServerHealth::Ok => ServerHealth::Ok,
proto::ServerHealth::Warning => ServerHealth::Warning,
proto::ServerHealth::Error => ServerHealth::Error,
};
LanguageServerStatusUpdate::Health(
health,
status_update.message.clone().map(SharedString::from),
)
} else {
return;
}
}
None => return,
};
activity_indicator.statuses.retain(|s| s.name != name);
activity_indicator
.statuses
.push(ServerStatus { name, status });
}
cx.notify()
}
_ => {}
},
)
@@ -228,9 +299,23 @@ impl ActivityIndicator {
_: &mut Window,
cx: &mut Context<Self>,
) {
if let Some(updater) = &self.auto_updater {
updater.update(cx, |updater, cx| updater.dismiss_error(cx));
let error_dismissed = if let Some(updater) = &self.auto_updater {
updater.update(cx, |updater, cx| updater.dismiss_error(cx))
} else {
false
};
if error_dismissed {
return;
}
self.project.update(cx, |project, cx| {
if project.last_formatting_failure(cx).is_some() {
project.reset_last_formatting_failure(cx);
true
} else {
false
}
});
}
fn pending_language_server_work<'a>(
@@ -399,6 +484,12 @@ impl ActivityIndicator {
let mut servers_to_clear_statuses = HashSet::<LanguageServerName>::default();
for status in &self.statuses {
match &status.status {
LanguageServerStatusUpdate::Binary(
BinaryStatus::Starting | BinaryStatus::Stopping,
) => {}
LanguageServerStatusUpdate::Binary(BinaryStatus::Stopped) => {
servers_to_clear_statuses.insert(status.name.clone());
}
LanguageServerStatusUpdate::Binary(BinaryStatus::CheckingForUpdate) => {
checking_for_update.push(status.name.clone());
}

View File

@@ -72,6 +72,7 @@ gpui = { workspace = true, "features" = ["test-support"] }
indoc.workspace = true
language = { workspace = true, "features" = ["test-support"] }
language_model = { workspace = true, "features" = ["test-support"] }
parking_lot.workspace = true
pretty_assertions.workspace = true
project = { workspace = true, features = ["test-support"] }
workspace = { workspace = true, features = ["test-support"] }

View File

@@ -85,19 +85,22 @@ impl AgentProfile {
.collect()
}
pub fn is_tool_enabled(&self, source: ToolSource, tool_name: String, cx: &App) -> bool {
let Some(settings) = AgentSettings::get_global(cx).profiles.get(&self.id) else {
return false;
};
return Self::is_enabled(settings, source, tool_name);
}
fn is_enabled(settings: &AgentProfileSettings, source: ToolSource, name: String) -> bool {
match source {
ToolSource::Native => *settings.tools.get(name.as_str()).unwrap_or(&false),
ToolSource::ContextServer { id } => {
if settings.enable_all_context_servers {
return true;
}
let Some(preset) = settings.context_servers.get(id.as_ref()) else {
return false;
};
*preset.tools.get(name.as_str()).unwrap_or(&false)
}
ToolSource::ContextServer { id } => settings
.context_servers
.get(id.as_ref())
.and_then(|preset| preset.tools.get(name.as_str()).copied())
.unwrap_or(settings.enable_all_context_servers),
}
}
}

View File

@@ -819,6 +819,134 @@ impl LoadedContext {
}
}
}
pub fn add_to_request_message_with_model(
&self,
request_message: &mut LanguageModelRequestMessage,
model: &Arc<dyn language_model::LanguageModel>,
) {
if !self.text.is_empty() {
request_message
.content
.push(MessageContent::Text(self.text.to_string()));
}
if !self.images.is_empty() {
let max_image_size = model.max_image_size();
let mut images_added = false;
for image in &self.images {
let image_size = image.len() as u64;
if image_size > max_image_size {
if max_image_size == 0 {
log::warn!(
"Skipping image attachment: model {:?} does not support images",
model.name()
);
} else {
log::warn!(
"Skipping image attachment: size {} bytes exceeds model {:?} limit of {} bytes",
image_size,
model.name(),
max_image_size
);
}
continue;
}
// Some providers only support image parts after an initial text part
if !images_added && request_message.content.is_empty() {
request_message
.content
.push(MessageContent::Text("Images attached by user:".to_string()));
}
request_message
.content
.push(MessageContent::Image(image.clone()));
images_added = true;
}
}
}
/// Checks images against model size limits and returns information about rejected images
pub fn check_image_size_limits(
&self,
model: &Arc<dyn language_model::LanguageModel>,
) -> Vec<RejectedImage> {
let mut rejected_images = Vec::new();
if !self.images.is_empty() {
let max_image_size = model.max_image_size();
for image in &self.images {
let image_size = image.len() as u64;
if image_size > max_image_size {
rejected_images.push(RejectedImage {
size: image_size,
max_size: max_image_size,
model_name: model.name().0.to_string(),
});
}
}
}
rejected_images
}
pub fn add_to_request_message_with_validation<F>(
&self,
request_message: &mut LanguageModelRequestMessage,
model: &Arc<dyn language_model::LanguageModel>,
mut on_image_rejected: F,
) where
F: FnMut(u64, u64, &str),
{
if !self.text.is_empty() {
request_message
.content
.push(MessageContent::Text(self.text.to_string()));
}
if !self.images.is_empty() {
let max_image_size = model.max_image_size();
let mut images_added = false;
for image in &self.images {
let image_size = image.len() as u64;
if image_size > max_image_size {
on_image_rejected(image_size, max_image_size, &model.name().0);
if max_image_size == 0 {
log::warn!(
"Skipping image attachment: model {:?} does not support images",
model.name()
);
} else {
log::warn!(
"Skipping image attachment: size {} bytes exceeds model {:?} limit of {} bytes",
image_size,
model.name(),
max_image_size
);
}
continue;
}
// Some providers only support image parts after an initial text part
if !images_added && request_message.content.is_empty() {
request_message
.content
.push(MessageContent::Text("Images attached by user:".to_string()));
}
request_message
.content
.push(MessageContent::Image(image.clone()));
images_added = true;
}
}
}
}
/// Loads and formats a collection of contexts.
@@ -1112,10 +1240,18 @@ impl Hash for AgentContextKey {
}
}
#[derive(Debug, Clone)]
pub struct RejectedImage {
pub size: u64,
pub max_size: u64,
pub model_name: String,
}
#[cfg(test)]
mod tests {
use super::*;
use gpui::TestAppContext;
use gpui::{AsyncApp, TestAppContext};
use language_model::{LanguageModelCacheConfiguration, LanguageModelId, LanguageModelName};
use project::{FakeFs, Project};
use serde_json::json;
use settings::SettingsStore;
@@ -1222,4 +1358,484 @@ mod tests {
})
.expect("Should have found a file context")
}
#[gpui::test]
async fn test_image_size_limit_filtering(_cx: &mut TestAppContext) {
use futures::stream::BoxStream;
use gpui::{AsyncApp, DevicePixels, SharedString};
use language_model::{
LanguageModelId, LanguageModelImage, LanguageModelName, LanguageModelProviderId,
LanguageModelProviderName, Role,
};
use std::sync::Arc;
// Create a mock image that's 10 bytes
let small_image = LanguageModelImage {
source: "small_data".into(),
size: gpui::size(DevicePixels(10), DevicePixels(10)),
};
// Create a mock image that's 1MB
let large_image_source = "x".repeat(1_048_576);
let large_image = LanguageModelImage {
source: large_image_source.into(),
size: gpui::size(DevicePixels(1024), DevicePixels(1024)),
};
let loaded_context = LoadedContext {
contexts: vec![],
text: "Some text".to_string(),
images: vec![small_image.clone(), large_image.clone()],
};
// Test with a model that supports images with 500KB limit
struct TestModel500KB;
impl language_model::LanguageModel for TestModel500KB {
fn id(&self) -> LanguageModelId {
LanguageModelId(SharedString::from("test-500kb"))
}
fn name(&self) -> LanguageModelName {
LanguageModelName(SharedString::from("Test Model 500KB"))
}
fn provider_id(&self) -> LanguageModelProviderId {
LanguageModelProviderId(SharedString::from("test"))
}
fn provider_name(&self) -> LanguageModelProviderName {
LanguageModelProviderName(SharedString::from("Test Provider"))
}
fn supports_tools(&self) -> bool {
false
}
fn supports_tool_choice(&self, _: language_model::LanguageModelToolChoice) -> bool {
false
}
fn max_image_size(&self) -> u64 {
512_000
} // 500KB
fn telemetry_id(&self) -> String {
"test-500kb".to_string()
}
fn max_token_count(&self) -> u64 {
100_000
}
fn count_tokens(
&self,
_request: language_model::LanguageModelRequest,
_cx: &App,
) -> futures::future::BoxFuture<'static, anyhow::Result<u64>> {
Box::pin(async { Ok(0) })
}
fn stream_completion(
&self,
_request: language_model::LanguageModelRequest,
_cx: &AsyncApp,
) -> futures::future::BoxFuture<
'static,
Result<
BoxStream<
'static,
Result<
language_model::LanguageModelCompletionEvent,
language_model::LanguageModelCompletionError,
>,
>,
language_model::LanguageModelCompletionError,
>,
> {
use language_model::LanguageModelCompletionError;
Box::pin(async {
Err(LanguageModelCompletionError::Other(anyhow::anyhow!(
"Not implemented"
)))
})
}
}
let model_500kb: Arc<dyn language_model::LanguageModel> = Arc::new(TestModel500KB);
let mut request_message = LanguageModelRequestMessage {
role: Role::User,
content: vec![],
cache: false,
};
loaded_context.add_to_request_message_with_model(&mut request_message, &model_500kb);
// Should have text and only the small image
assert_eq!(request_message.content.len(), 2); // text + small image
assert!(
matches!(&request_message.content[0], MessageContent::Text(text) if text == "Some text")
);
assert!(matches!(
&request_message.content[1],
MessageContent::Image(_)
));
// Test with a model that doesn't support images
struct TestModelNoImages;
impl language_model::LanguageModel for TestModelNoImages {
fn id(&self) -> LanguageModelId {
LanguageModelId(SharedString::from("test-no-images"))
}
fn name(&self) -> LanguageModelName {
LanguageModelName(SharedString::from("Test Model No Images"))
}
fn provider_id(&self) -> LanguageModelProviderId {
LanguageModelProviderId(SharedString::from("test"))
}
fn provider_name(&self) -> LanguageModelProviderName {
LanguageModelProviderName(SharedString::from("Test Provider"))
}
fn supports_tools(&self) -> bool {
false
}
fn supports_tool_choice(&self, _: language_model::LanguageModelToolChoice) -> bool {
false
}
fn max_image_size(&self) -> u64 {
0
} // No image support
fn telemetry_id(&self) -> String {
"test-no-images".to_string()
}
fn max_token_count(&self) -> u64 {
100_000
}
fn count_tokens(
&self,
_request: language_model::LanguageModelRequest,
_cx: &App,
) -> futures::future::BoxFuture<'static, anyhow::Result<u64>> {
Box::pin(async { Ok(0) })
}
fn stream_completion(
&self,
_request: language_model::LanguageModelRequest,
_cx: &AsyncApp,
) -> futures::future::BoxFuture<
'static,
Result<
BoxStream<
'static,
Result<
language_model::LanguageModelCompletionEvent,
language_model::LanguageModelCompletionError,
>,
>,
language_model::LanguageModelCompletionError,
>,
> {
use language_model::LanguageModelCompletionError;
Box::pin(async {
Err(LanguageModelCompletionError::Other(anyhow::anyhow!(
"Not implemented"
)))
})
}
}
let model_no_images: Arc<dyn language_model::LanguageModel> = Arc::new(TestModelNoImages);
let mut request_message_no_images = LanguageModelRequestMessage {
role: Role::User,
content: vec![],
cache: false,
};
loaded_context
.add_to_request_message_with_model(&mut request_message_no_images, &model_no_images);
// Should have only text, no images
assert_eq!(request_message_no_images.content.len(), 1);
assert!(
matches!(&request_message_no_images.content[0], MessageContent::Text(text) if text == "Some text")
);
}
#[gpui::test]
async fn test_check_image_size_limits() {
use gpui::DevicePixels;
use language_model::LanguageModelImage;
// Create test images of various sizes
let tiny_image = LanguageModelImage {
source: "tiny".into(),
size: gpui::size(DevicePixels(10), DevicePixels(10)),
};
let small_image = LanguageModelImage {
source: "x".repeat(100_000).into(), // 100KB
size: gpui::size(DevicePixels(100), DevicePixels(100)),
};
let medium_image = LanguageModelImage {
source: "x".repeat(500_000).into(), // 500KB
size: gpui::size(DevicePixels(500), DevicePixels(500)),
};
let large_image = LanguageModelImage {
source: "x".repeat(1_048_576).into(), // 1MB
size: gpui::size(DevicePixels(1024), DevicePixels(1024)),
};
let huge_image = LanguageModelImage {
source: "x".repeat(5_242_880).into(), // 5MB
size: gpui::size(DevicePixels(2048), DevicePixels(2048)),
};
// Test with model that has 1MB limit
let model_1mb = Arc::new(TestModel1MB);
let loaded_context = LoadedContext {
contexts: vec![],
text: String::new(),
images: vec![
tiny_image.clone(),
small_image.clone(),
medium_image.clone(),
large_image.clone(),
huge_image.clone(),
],
};
let rejected = loaded_context.check_image_size_limits(
&(model_1mb.clone() as Arc<dyn language_model::LanguageModel>),
);
assert_eq!(rejected.len(), 1);
assert_eq!(rejected[0].size, 5_242_880);
assert_eq!(rejected[0].max_size, 1_048_576);
assert_eq!(rejected[0].model_name, "Test Model 1MB");
// Test with model that doesn't support images
let model_no_images = Arc::new(TestModelNoImages);
let rejected = loaded_context.check_image_size_limits(
&(model_no_images.clone() as Arc<dyn language_model::LanguageModel>),
);
assert_eq!(rejected.len(), 5); // All images rejected
for (_i, rejected_image) in rejected.iter().enumerate() {
assert_eq!(rejected_image.max_size, 0);
assert_eq!(rejected_image.model_name, "Test Model No Images");
}
// Test with empty image list
let empty_context = LoadedContext {
contexts: vec![],
text: String::new(),
images: vec![],
};
let rejected = empty_context.check_image_size_limits(
&(model_1mb.clone() as Arc<dyn language_model::LanguageModel>),
);
assert!(rejected.is_empty());
// Test with all images within limit
let small_context = LoadedContext {
contexts: vec![],
text: String::new(),
images: vec![tiny_image.clone(), small_image.clone()],
};
let rejected = small_context
.check_image_size_limits(&(model_1mb as Arc<dyn language_model::LanguageModel>));
assert!(rejected.is_empty());
}
#[gpui::test]
async fn test_add_to_request_message_with_validation() {
use gpui::DevicePixels;
use language_model::{LanguageModelImage, MessageContent, Role};
let small_image = LanguageModelImage {
source: "small".into(),
size: gpui::size(DevicePixels(10), DevicePixels(10)),
};
let large_image = LanguageModelImage {
source: "x".repeat(2_097_152).into(), // 2MB
size: gpui::size(DevicePixels(1024), DevicePixels(1024)),
};
let loaded_context = LoadedContext {
contexts: vec![],
text: "Test message".to_string(),
images: vec![small_image.clone(), large_image.clone()],
};
let model = Arc::new(TestModel1MB);
let mut request_message = LanguageModelRequestMessage {
role: Role::User,
content: Vec::new(),
cache: false,
};
let mut rejected_count = 0;
let mut rejected_sizes = Vec::new();
let mut rejected_model_names = Vec::new();
loaded_context.add_to_request_message_with_validation(
&mut request_message,
&(model.clone() as Arc<dyn language_model::LanguageModel>),
|size, max_size, model_name| {
rejected_count += 1;
rejected_sizes.push((size, max_size));
rejected_model_names.push(model_name.to_string());
},
);
// Verify callback was called for the large image
assert_eq!(rejected_count, 1);
assert_eq!(rejected_sizes[0], (2_097_152, 1_048_576));
assert_eq!(rejected_model_names[0], "Test Model 1MB");
// Verify the request message contains text and only the small image
assert_eq!(request_message.content.len(), 2); // text + small image
assert!(
matches!(&request_message.content[0], MessageContent::Text(text) if text == "Test message")
);
assert!(matches!(
&request_message.content[1],
MessageContent::Image(_)
));
}
// Helper test models
struct TestModel1MB;
impl language_model::LanguageModel for TestModel1MB {
fn id(&self) -> LanguageModelId {
LanguageModelId(SharedString::from("test-1mb"))
}
fn name(&self) -> LanguageModelName {
LanguageModelName(SharedString::from("Test Model 1MB"))
}
fn provider_id(&self) -> language_model::LanguageModelProviderId {
language_model::LanguageModelProviderId(SharedString::from("test"))
}
fn provider_name(&self) -> language_model::LanguageModelProviderName {
language_model::LanguageModelProviderName(SharedString::from("Test Provider"))
}
fn supports_tools(&self) -> bool {
false
}
fn supports_tool_choice(&self, _: language_model::LanguageModelToolChoice) -> bool {
false
}
fn max_image_size(&self) -> u64 {
1_048_576 // 1MB
}
fn telemetry_id(&self) -> String {
"test-1mb".to_string()
}
fn max_token_count(&self) -> u64 {
100_000
}
fn max_output_tokens(&self) -> Option<u64> {
Some(4096)
}
fn cache_configuration(&self) -> Option<LanguageModelCacheConfiguration> {
Some(LanguageModelCacheConfiguration {
max_cache_anchors: 0,
should_speculate: false,
min_total_token: 1024,
})
}
fn count_tokens(
&self,
_request: language_model::LanguageModelRequest,
_cx: &App,
) -> futures::future::BoxFuture<'static, anyhow::Result<u64>> {
Box::pin(async { Ok(0) })
}
fn stream_completion(
&self,
_request: language_model::LanguageModelRequest,
_cx: &AsyncApp,
) -> futures::future::BoxFuture<
'static,
Result<
futures::stream::BoxStream<
'static,
Result<
language_model::LanguageModelCompletionEvent,
language_model::LanguageModelCompletionError,
>,
>,
language_model::LanguageModelCompletionError,
>,
> {
use language_model::LanguageModelCompletionError;
Box::pin(async {
Err(LanguageModelCompletionError::Other(anyhow::anyhow!(
"Not implemented"
)))
})
}
}
struct TestModelNoImages;
impl language_model::LanguageModel for TestModelNoImages {
fn id(&self) -> LanguageModelId {
LanguageModelId(SharedString::from("test-no-images"))
}
fn name(&self) -> LanguageModelName {
LanguageModelName(SharedString::from("Test Model No Images"))
}
fn provider_id(&self) -> language_model::LanguageModelProviderId {
language_model::LanguageModelProviderId(SharedString::from("test"))
}
fn provider_name(&self) -> language_model::LanguageModelProviderName {
language_model::LanguageModelProviderName(SharedString::from("Test Provider"))
}
fn supports_tools(&self) -> bool {
false
}
fn supports_tool_choice(&self, _: language_model::LanguageModelToolChoice) -> bool {
false
}
fn max_image_size(&self) -> u64 {
0 // No image support
}
fn telemetry_id(&self) -> String {
"test-no-images".to_string()
}
fn max_token_count(&self) -> u64 {
100_000
}
fn max_output_tokens(&self) -> Option<u64> {
Some(4096)
}
fn cache_configuration(&self) -> Option<LanguageModelCacheConfiguration> {
Some(LanguageModelCacheConfiguration {
max_cache_anchors: 0,
should_speculate: false,
min_total_token: 1024,
})
}
fn count_tokens(
&self,
_request: language_model::LanguageModelRequest,
_cx: &App,
) -> futures::future::BoxFuture<'static, anyhow::Result<u64>> {
Box::pin(async { Ok(0) })
}
fn stream_completion(
&self,
_request: language_model::LanguageModelRequest,
_cx: &AsyncApp,
) -> futures::future::BoxFuture<
'static,
Result<
futures::stream::BoxStream<
'static,
Result<
language_model::LanguageModelCompletionEvent,
language_model::LanguageModelCompletionError,
>,
>,
language_model::LanguageModelCompletionError,
>,
> {
use language_model::LanguageModelCompletionError;
Box::pin(async {
Err(LanguageModelCompletionError::Other(anyhow::anyhow!(
"Not implemented"
)))
})
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -71,7 +71,7 @@ impl Column for DataType {
}
}
const RULES_FILE_NAMES: [&'static str; 8] = [
const RULES_FILE_NAMES: [&'static str; 9] = [
".rules",
".cursorrules",
".windsurfrules",
@@ -80,6 +80,7 @@ const RULES_FILE_NAMES: [&'static str; 8] = [
"CLAUDE.md",
"AGENT.md",
"AGENTS.md",
"GEMINI.md",
];
pub fn init(cx: &mut App) {
@@ -731,7 +732,7 @@ pub enum SerializedMessageSegment {
signature: Option<String>,
},
RedactedThinking {
data: Vec<u8>,
data: String,
},
}

View File

@@ -12,17 +12,10 @@ workspace = true
path = "src/agent_settings.rs"
[dependencies]
anthropic = { workspace = true, features = ["schemars"] }
anyhow.workspace = true
collections.workspace = true
gpui.workspace = true
language_model.workspace = true
lmstudio = { workspace = true, features = ["schemars"] }
log.workspace = true
ollama = { workspace = true, features = ["schemars"] }
open_ai = { workspace = true, features = ["schemars"] }
deepseek = { workspace = true, features = ["schemars"] }
mistral = { workspace = true, features = ["schemars"] }
schemars.workspace = true
serde.workspace = true
settings.workspace = true

View File

@@ -2,16 +2,10 @@ mod agent_profile;
use std::sync::Arc;
use ::open_ai::Model as OpenAiModel;
use anthropic::Model as AnthropicModel;
use anyhow::{Result, bail};
use collections::IndexMap;
use deepseek::Model as DeepseekModel;
use gpui::{App, Pixels, SharedString};
use language_model::LanguageModel;
use lmstudio::Model as LmStudioModel;
use mistral::Model as MistralModel;
use ollama::Model as OllamaModel;
use schemars::{JsonSchema, schema::Schema};
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
@@ -48,45 +42,6 @@ pub enum NotifyWhenAgentWaiting {
Never,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(tag = "name", rename_all = "snake_case")]
#[schemars(deny_unknown_fields)]
pub enum AgentProviderContentV1 {
#[serde(rename = "zed.dev")]
ZedDotDev { default_model: Option<String> },
#[serde(rename = "openai")]
OpenAi {
default_model: Option<OpenAiModel>,
api_url: Option<String>,
available_models: Option<Vec<OpenAiModel>>,
},
#[serde(rename = "anthropic")]
Anthropic {
default_model: Option<AnthropicModel>,
api_url: Option<String>,
},
#[serde(rename = "ollama")]
Ollama {
default_model: Option<OllamaModel>,
api_url: Option<String>,
},
#[serde(rename = "lmstudio")]
LmStudio {
default_model: Option<LmStudioModel>,
api_url: Option<String>,
},
#[serde(rename = "deepseek")]
DeepSeek {
default_model: Option<DeepseekModel>,
api_url: Option<String>,
},
#[serde(rename = "mistral")]
Mistral {
default_model: Option<MistralModel>,
api_url: Option<String>,
},
}
#[derive(Default, Clone, Debug)]
pub struct AgentSettings {
pub enabled: bool,
@@ -168,366 +123,56 @@ impl LanguageModelParameters {
}
}
/// Agent panel settings
#[derive(Clone, Serialize, Deserialize, Debug, Default)]
pub struct AgentSettingsContent {
#[serde(flatten)]
pub inner: Option<AgentSettingsContentInner>,
}
#[derive(Clone, Serialize, Deserialize, Debug)]
#[serde(untagged)]
pub enum AgentSettingsContentInner {
Versioned(Box<VersionedAgentSettingsContent>),
Legacy(LegacyAgentSettingsContent),
}
impl AgentSettingsContentInner {
fn for_v2(content: AgentSettingsContentV2) -> Self {
AgentSettingsContentInner::Versioned(Box::new(VersionedAgentSettingsContent::V2(content)))
}
}
impl JsonSchema for AgentSettingsContent {
fn schema_name() -> String {
VersionedAgentSettingsContent::schema_name()
}
fn json_schema(r#gen: &mut schemars::r#gen::SchemaGenerator) -> Schema {
VersionedAgentSettingsContent::json_schema(r#gen)
}
fn is_referenceable() -> bool {
VersionedAgentSettingsContent::is_referenceable()
}
}
impl AgentSettingsContent {
pub fn is_version_outdated(&self) -> bool {
match &self.inner {
Some(AgentSettingsContentInner::Versioned(settings)) => match **settings {
VersionedAgentSettingsContent::V1(_) => true,
VersionedAgentSettingsContent::V2(_) => false,
},
Some(AgentSettingsContentInner::Legacy(_)) => true,
None => false,
}
}
fn upgrade(&self) -> AgentSettingsContentV2 {
match &self.inner {
Some(AgentSettingsContentInner::Versioned(settings)) => match **settings {
VersionedAgentSettingsContent::V1(ref settings) => AgentSettingsContentV2 {
enabled: settings.enabled,
button: settings.button,
dock: settings.dock,
default_width: settings.default_width,
default_height: settings.default_width,
default_model: settings
.provider
.clone()
.and_then(|provider| match provider {
AgentProviderContentV1::ZedDotDev { default_model } => default_model
.map(|model| LanguageModelSelection {
provider: "zed.dev".into(),
model,
}),
AgentProviderContentV1::OpenAi { default_model, .. } => default_model
.map(|model| LanguageModelSelection {
provider: "openai".into(),
model: model.id().to_string(),
}),
AgentProviderContentV1::Anthropic { default_model, .. } => {
default_model.map(|model| LanguageModelSelection {
provider: "anthropic".into(),
model: model.id().to_string(),
})
}
AgentProviderContentV1::Ollama { default_model, .. } => default_model
.map(|model| LanguageModelSelection {
provider: "ollama".into(),
model: model.id().to_string(),
}),
AgentProviderContentV1::LmStudio { default_model, .. } => default_model
.map(|model| LanguageModelSelection {
provider: "lmstudio".into(),
model: model.id().to_string(),
}),
AgentProviderContentV1::DeepSeek { default_model, .. } => default_model
.map(|model| LanguageModelSelection {
provider: "deepseek".into(),
model: model.id().to_string(),
}),
AgentProviderContentV1::Mistral { default_model, .. } => default_model
.map(|model| LanguageModelSelection {
provider: "mistral".into(),
model: model.id().to_string(),
}),
}),
inline_assistant_model: None,
commit_message_model: None,
thread_summary_model: None,
inline_alternatives: None,
default_profile: None,
default_view: None,
profiles: None,
always_allow_tool_actions: None,
notify_when_agent_waiting: None,
stream_edits: None,
single_file_review: None,
model_parameters: Vec::new(),
preferred_completion_mode: None,
enable_feedback: None,
play_sound_when_agent_done: None,
},
VersionedAgentSettingsContent::V2(ref settings) => settings.clone(),
},
Some(AgentSettingsContentInner::Legacy(settings)) => AgentSettingsContentV2 {
enabled: None,
button: settings.button,
dock: settings.dock,
default_width: settings.default_width,
default_height: settings.default_height,
default_model: Some(LanguageModelSelection {
provider: "openai".into(),
model: settings
.default_open_ai_model
.clone()
.unwrap_or_default()
.id()
.to_string(),
}),
inline_assistant_model: None,
commit_message_model: None,
thread_summary_model: None,
inline_alternatives: None,
default_profile: None,
default_view: None,
profiles: None,
always_allow_tool_actions: None,
notify_when_agent_waiting: None,
stream_edits: None,
single_file_review: None,
model_parameters: Vec::new(),
preferred_completion_mode: None,
enable_feedback: None,
play_sound_when_agent_done: None,
},
None => AgentSettingsContentV2::default(),
}
}
pub fn set_dock(&mut self, dock: AgentDockPosition) {
match &mut self.inner {
Some(AgentSettingsContentInner::Versioned(settings)) => match **settings {
VersionedAgentSettingsContent::V1(ref mut settings) => {
settings.dock = Some(dock);
}
VersionedAgentSettingsContent::V2(ref mut settings) => {
settings.dock = Some(dock);
}
},
Some(AgentSettingsContentInner::Legacy(settings)) => {
settings.dock = Some(dock);
}
None => {
self.inner = Some(AgentSettingsContentInner::for_v2(AgentSettingsContentV2 {
dock: Some(dock),
..Default::default()
}))
}
}
self.dock = Some(dock);
}
pub fn set_model(&mut self, language_model: Arc<dyn LanguageModel>) {
let model = language_model.id().0.to_string();
let provider = language_model.provider_id().0.to_string();
match &mut self.inner {
Some(AgentSettingsContentInner::Versioned(settings)) => match **settings {
VersionedAgentSettingsContent::V1(ref mut settings) => match provider.as_ref() {
"zed.dev" => {
log::warn!("attempted to set zed.dev model on outdated settings");
}
"anthropic" => {
let api_url = match &settings.provider {
Some(AgentProviderContentV1::Anthropic { api_url, .. }) => {
api_url.clone()
}
_ => None,
};
settings.provider = Some(AgentProviderContentV1::Anthropic {
default_model: AnthropicModel::from_id(&model).ok(),
api_url,
});
}
"ollama" => {
let api_url = match &settings.provider {
Some(AgentProviderContentV1::Ollama { api_url, .. }) => api_url.clone(),
_ => None,
};
settings.provider = Some(AgentProviderContentV1::Ollama {
default_model: Some(ollama::Model::new(
&model,
None,
None,
Some(language_model.supports_tools()),
Some(language_model.supports_images()),
None,
)),
api_url,
});
}
"lmstudio" => {
let api_url = match &settings.provider {
Some(AgentProviderContentV1::LmStudio { api_url, .. }) => {
api_url.clone()
}
_ => None,
};
settings.provider = Some(AgentProviderContentV1::LmStudio {
default_model: Some(lmstudio::Model::new(
&model, None, None, false, false,
)),
api_url,
});
}
"openai" => {
let (api_url, available_models) = match &settings.provider {
Some(AgentProviderContentV1::OpenAi {
api_url,
available_models,
..
}) => (api_url.clone(), available_models.clone()),
_ => (None, None),
};
settings.provider = Some(AgentProviderContentV1::OpenAi {
default_model: OpenAiModel::from_id(&model).ok(),
api_url,
available_models,
});
}
"deepseek" => {
let api_url = match &settings.provider {
Some(AgentProviderContentV1::DeepSeek { api_url, .. }) => {
api_url.clone()
}
_ => None,
};
settings.provider = Some(AgentProviderContentV1::DeepSeek {
default_model: DeepseekModel::from_id(&model).ok(),
api_url,
});
}
_ => {}
},
VersionedAgentSettingsContent::V2(ref mut settings) => {
settings.default_model = Some(LanguageModelSelection {
provider: provider.into(),
model,
});
}
},
Some(AgentSettingsContentInner::Legacy(settings)) => {
if let Ok(model) = OpenAiModel::from_id(&language_model.id().0) {
settings.default_open_ai_model = Some(model);
}
}
None => {
self.inner = Some(AgentSettingsContentInner::for_v2(AgentSettingsContentV2 {
default_model: Some(LanguageModelSelection {
provider: provider.into(),
model,
}),
..Default::default()
}));
}
}
self.default_model = Some(LanguageModelSelection {
provider: provider.into(),
model,
});
}
pub fn set_inline_assistant_model(&mut self, provider: String, model: String) {
self.v2_setting(|setting| {
setting.inline_assistant_model = Some(LanguageModelSelection {
provider: provider.into(),
model,
});
Ok(())
})
.ok();
self.inline_assistant_model = Some(LanguageModelSelection {
provider: provider.into(),
model,
});
}
pub fn set_commit_message_model(&mut self, provider: String, model: String) {
self.v2_setting(|setting| {
setting.commit_message_model = Some(LanguageModelSelection {
provider: provider.into(),
model,
});
Ok(())
})
.ok();
}
pub fn v2_setting(
&mut self,
f: impl FnOnce(&mut AgentSettingsContentV2) -> anyhow::Result<()>,
) -> anyhow::Result<()> {
match self.inner.get_or_insert_with(|| {
AgentSettingsContentInner::for_v2(AgentSettingsContentV2 {
..Default::default()
})
}) {
AgentSettingsContentInner::Versioned(boxed) => {
if let VersionedAgentSettingsContent::V2(ref mut settings) = **boxed {
f(settings)
} else {
Ok(())
}
}
_ => Ok(()),
}
self.commit_message_model = Some(LanguageModelSelection {
provider: provider.into(),
model,
});
}
pub fn set_thread_summary_model(&mut self, provider: String, model: String) {
self.v2_setting(|setting| {
setting.thread_summary_model = Some(LanguageModelSelection {
provider: provider.into(),
model,
});
Ok(())
})
.ok();
self.thread_summary_model = Some(LanguageModelSelection {
provider: provider.into(),
model,
});
}
pub fn set_always_allow_tool_actions(&mut self, allow: bool) {
self.v2_setting(|setting| {
setting.always_allow_tool_actions = Some(allow);
Ok(())
})
.ok();
self.always_allow_tool_actions = Some(allow);
}
pub fn set_play_sound_when_agent_done(&mut self, allow: bool) {
self.v2_setting(|setting| {
setting.play_sound_when_agent_done = Some(allow);
Ok(())
})
.ok();
self.play_sound_when_agent_done = Some(allow);
}
pub fn set_single_file_review(&mut self, allow: bool) {
self.v2_setting(|setting| {
setting.single_file_review = Some(allow);
Ok(())
})
.ok();
self.single_file_review = Some(allow);
}
pub fn set_profile(&mut self, profile_id: AgentProfileId) {
self.v2_setting(|setting| {
setting.default_profile = Some(profile_id);
Ok(())
})
.ok();
self.default_profile = Some(profile_id);
}
pub fn create_profile(
@@ -535,79 +180,39 @@ impl AgentSettingsContent {
profile_id: AgentProfileId,
profile_settings: AgentProfileSettings,
) -> Result<()> {
self.v2_setting(|settings| {
let profiles = settings.profiles.get_or_insert_default();
if profiles.contains_key(&profile_id) {
bail!("profile with ID '{profile_id}' already exists");
}
let profiles = self.profiles.get_or_insert_default();
if profiles.contains_key(&profile_id) {
bail!("profile with ID '{profile_id}' already exists");
}
profiles.insert(
profile_id,
AgentProfileContent {
name: profile_settings.name.into(),
tools: profile_settings.tools,
enable_all_context_servers: Some(profile_settings.enable_all_context_servers),
context_servers: profile_settings
.context_servers
.into_iter()
.map(|(server_id, preset)| {
(
server_id,
ContextServerPresetContent {
tools: preset.tools,
},
)
})
.collect(),
},
);
profiles.insert(
profile_id,
AgentProfileContent {
name: profile_settings.name.into(),
tools: profile_settings.tools,
enable_all_context_servers: Some(profile_settings.enable_all_context_servers),
context_servers: profile_settings
.context_servers
.into_iter()
.map(|(server_id, preset)| {
(
server_id,
ContextServerPresetContent {
tools: preset.tools,
},
)
})
.collect(),
},
);
Ok(())
})
}
}
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
#[serde(tag = "version")]
#[schemars(deny_unknown_fields)]
pub enum VersionedAgentSettingsContent {
#[serde(rename = "1")]
V1(AgentSettingsContentV1),
#[serde(rename = "2")]
V2(AgentSettingsContentV2),
}
impl Default for VersionedAgentSettingsContent {
fn default() -> Self {
Self::V2(AgentSettingsContentV2 {
enabled: None,
button: None,
dock: None,
default_width: None,
default_height: None,
default_model: None,
inline_assistant_model: None,
commit_message_model: None,
thread_summary_model: None,
inline_alternatives: None,
default_profile: None,
default_view: None,
profiles: None,
always_allow_tool_actions: None,
notify_when_agent_waiting: None,
stream_edits: None,
single_file_review: None,
model_parameters: Vec::new(),
preferred_completion_mode: None,
enable_feedback: None,
play_sound_when_agent_done: None,
})
Ok(())
}
}
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, Default)]
#[schemars(deny_unknown_fields)]
pub struct AgentSettingsContentV2 {
pub struct AgentSettingsContent {
/// Whether the Agent is enabled.
///
/// Default: true
@@ -734,6 +339,7 @@ impl JsonSchema for LanguageModelProviderSetting {
"deepseek".into(),
"openrouter".into(),
"mistral".into(),
"vercel".into(),
]),
..Default::default()
}
@@ -778,65 +384,6 @@ pub struct ContextServerPresetContent {
pub tools: IndexMap<Arc<str>, bool>,
}
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
#[schemars(deny_unknown_fields)]
pub struct AgentSettingsContentV1 {
/// Whether the Agent is enabled.
///
/// Default: true
enabled: Option<bool>,
/// Whether to show the Agent panel button in the status bar.
///
/// Default: true
button: Option<bool>,
/// Where to dock the Agent.
///
/// Default: right
dock: Option<AgentDockPosition>,
/// Default width in pixels when the Agent is docked to the left or right.
///
/// Default: 640
default_width: Option<f32>,
/// Default height in pixels when the Agent is docked to the bottom.
///
/// Default: 320
default_height: Option<f32>,
/// The provider of the Agent service.
///
/// This can be "openai", "anthropic", "ollama", "lmstudio", "deepseek", "zed.dev"
/// each with their respective default models and configurations.
provider: Option<AgentProviderContentV1>,
}
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
#[schemars(deny_unknown_fields)]
pub struct LegacyAgentSettingsContent {
/// Whether to show the Agent panel button in the status bar.
///
/// Default: true
pub button: Option<bool>,
/// Where to dock the Agent.
///
/// Default: right
pub dock: Option<AgentDockPosition>,
/// Default width in pixels when the Agent is docked to the left or right.
///
/// Default: 640
pub default_width: Option<f32>,
/// Default height in pixels when the Agent is docked to the bottom.
///
/// Default: 320
pub default_height: Option<f32>,
/// The default OpenAI model to use when creating new chats.
///
/// Default: gpt-4-1106-preview
pub default_open_ai_model: Option<OpenAiModel>,
/// OpenAI API base URL to use when creating new chats.
///
/// Default: <https://api.openai.com/v1>
pub openai_api_url: Option<String>,
}
impl Settings for AgentSettings {
const KEY: Option<&'static str> = Some("agent");
@@ -853,11 +400,6 @@ impl Settings for AgentSettings {
let mut settings = AgentSettings::default();
for value in sources.defaults_and_customizations() {
if value.is_version_outdated() {
settings.using_outdated_settings_version = true;
}
let value = value.upgrade();
merge(&mut settings.enabled, value.enabled);
merge(&mut settings.button, value.button);
merge(&mut settings.dock, value.dock);
@@ -869,17 +411,23 @@ impl Settings for AgentSettings {
&mut settings.default_height,
value.default_height.map(Into::into),
);
merge(&mut settings.default_model, value.default_model);
merge(&mut settings.default_model, value.default_model.clone());
settings.inline_assistant_model = value
.inline_assistant_model
.clone()
.or(settings.inline_assistant_model.take());
settings.commit_message_model = value
.clone()
.commit_message_model
.or(settings.commit_message_model.take());
settings.thread_summary_model = value
.clone()
.thread_summary_model
.or(settings.thread_summary_model.take());
merge(&mut settings.inline_alternatives, value.inline_alternatives);
merge(
&mut settings.inline_alternatives,
value.inline_alternatives.clone(),
);
merge(
&mut settings.always_allow_tool_actions,
value.always_allow_tool_actions,
@@ -894,7 +442,7 @@ impl Settings for AgentSettings {
);
merge(&mut settings.stream_edits, value.stream_edits);
merge(&mut settings.single_file_review, value.single_file_review);
merge(&mut settings.default_profile, value.default_profile);
merge(&mut settings.default_profile, value.default_profile.clone());
merge(&mut settings.default_view, value.default_view);
merge(
&mut settings.preferred_completion_mode,
@@ -906,24 +454,24 @@ impl Settings for AgentSettings {
.model_parameters
.extend_from_slice(&value.model_parameters);
if let Some(profiles) = value.profiles {
if let Some(profiles) = value.profiles.as_ref() {
settings
.profiles
.extend(profiles.into_iter().map(|(id, profile)| {
(
id,
id.clone(),
AgentProfileSettings {
name: profile.name.into(),
tools: profile.tools,
name: profile.name.clone().into(),
tools: profile.tools.clone(),
enable_all_context_servers: profile
.enable_all_context_servers
.unwrap_or_default(),
context_servers: profile
.context_servers
.into_iter()
.iter()
.map(|(context_server_id, preset)| {
(
context_server_id,
context_server_id.clone(),
ContextServerPreset {
tools: preset.tools.clone(),
},
@@ -944,28 +492,8 @@ impl Settings for AgentSettings {
.read_value("chat.agent.enabled")
.and_then(|b| b.as_bool())
{
match &mut current.inner {
Some(AgentSettingsContentInner::Versioned(versioned)) => match versioned.as_mut() {
VersionedAgentSettingsContent::V1(setting) => {
setting.enabled = Some(b);
setting.button = Some(b);
}
VersionedAgentSettingsContent::V2(setting) => {
setting.enabled = Some(b);
setting.button = Some(b);
}
},
Some(AgentSettingsContentInner::Legacy(setting)) => setting.button = Some(b),
None => {
current.inner =
Some(AgentSettingsContentInner::for_v2(AgentSettingsContentV2 {
enabled: Some(b),
button: Some(b),
..Default::default()
}));
}
}
current.enabled = Some(b);
current.button = Some(b);
}
}
}
@@ -975,149 +503,3 @@ fn merge<T>(target: &mut T, value: Option<T>) {
*target = value;
}
}
#[cfg(test)]
mod tests {
use fs::Fs;
use gpui::{ReadGlobal, TestAppContext};
use settings::SettingsStore;
use super::*;
#[gpui::test]
async fn test_deserialize_agent_settings_with_version(cx: &mut TestAppContext) {
let fs = fs::FakeFs::new(cx.executor().clone());
fs.create_dir(paths::settings_file().parent().unwrap())
.await
.unwrap();
cx.update(|cx| {
let test_settings = settings::SettingsStore::test(cx);
cx.set_global(test_settings);
AgentSettings::register(cx);
});
cx.update(|cx| {
assert!(!AgentSettings::get_global(cx).using_outdated_settings_version);
assert_eq!(
AgentSettings::get_global(cx).default_model,
LanguageModelSelection {
provider: "zed.dev".into(),
model: "claude-sonnet-4".into(),
}
);
});
cx.update(|cx| {
settings::SettingsStore::global(cx).update_settings_file::<AgentSettings>(
fs.clone(),
|settings, _| {
*settings = AgentSettingsContent {
inner: Some(AgentSettingsContentInner::for_v2(AgentSettingsContentV2 {
default_model: Some(LanguageModelSelection {
provider: "test-provider".into(),
model: "gpt-99".into(),
}),
inline_assistant_model: None,
commit_message_model: None,
thread_summary_model: None,
inline_alternatives: None,
enabled: None,
button: None,
dock: None,
default_width: None,
default_height: None,
default_profile: None,
default_view: None,
profiles: None,
always_allow_tool_actions: None,
play_sound_when_agent_done: None,
notify_when_agent_waiting: None,
stream_edits: None,
single_file_review: None,
enable_feedback: None,
model_parameters: Vec::new(),
preferred_completion_mode: None,
})),
}
},
);
});
cx.run_until_parked();
let raw_settings_value = fs.load(paths::settings_file()).await.unwrap();
assert!(raw_settings_value.contains(r#""version": "2""#));
#[derive(Debug, Deserialize)]
struct AgentSettingsTest {
agent: AgentSettingsContent,
}
let agent_settings: AgentSettingsTest =
serde_json_lenient::from_str(&raw_settings_value).unwrap();
assert!(!agent_settings.agent.is_version_outdated());
}
#[gpui::test]
async fn test_load_settings_from_old_key(cx: &mut TestAppContext) {
let fs = fs::FakeFs::new(cx.executor().clone());
fs.create_dir(paths::settings_file().parent().unwrap())
.await
.unwrap();
cx.update(|cx| {
let mut test_settings = settings::SettingsStore::test(cx);
let user_settings_content = r#"{
"assistant": {
"enabled": true,
"version": "2",
"default_model": {
"provider": "zed.dev",
"model": "gpt-99"
},
}}"#;
test_settings
.set_user_settings(user_settings_content, cx)
.unwrap();
cx.set_global(test_settings);
AgentSettings::register(cx);
});
cx.run_until_parked();
let agent_settings = cx.update(|cx| AgentSettings::get_global(cx).clone());
assert!(agent_settings.enabled);
assert!(!agent_settings.using_outdated_settings_version);
assert_eq!(agent_settings.default_model.model, "gpt-99");
cx.update_global::<SettingsStore, _>(|settings_store, cx| {
settings_store.update_user_settings::<AgentSettings>(cx, |settings| {
*settings = AgentSettingsContent {
inner: Some(AgentSettingsContentInner::for_v2(AgentSettingsContentV2 {
enabled: Some(false),
default_model: Some(LanguageModelSelection {
provider: "xai".to_owned().into(),
model: "grok".to_owned(),
}),
..Default::default()
})),
};
});
});
cx.run_until_parked();
let settings = cx.update(|cx| SettingsStore::global(cx).raw_user_settings().clone());
#[derive(Debug, Deserialize)]
struct AgentSettingsTest {
assistant: AgentSettingsContent,
agent: Option<serde_json_lenient::Value>,
}
let agent_settings: AgentSettingsTest = serde_json::from_value(settings).unwrap();
assert!(agent_settings.agent.is_none());
}
}

View File

@@ -19,7 +19,7 @@ use audio::{Audio, Sound};
use collections::{HashMap, HashSet};
use editor::actions::{MoveUp, Paste};
use editor::scroll::Autoscroll;
use editor::{Editor, EditorElement, EditorEvent, EditorStyle, MultiBuffer};
use editor::{Editor, EditorElement, EditorEvent, EditorStyle, MultiBuffer, SelectionEffects};
use gpui::{
AbsoluteLength, Animation, AnimationExt, AnyElement, App, ClickEvent, ClipboardEntry,
ClipboardItem, DefiniteLength, EdgesRefinement, Empty, Entity, EventEmitter, Focusable, Hsla,
@@ -47,8 +47,8 @@ use std::time::Duration;
use text::ToPoint;
use theme::ThemeSettings;
use ui::{
Disclosure, KeyBinding, PopoverMenuHandle, Scrollbar, ScrollbarState, TextSize, Tooltip,
prelude::*,
Banner, Disclosure, KeyBinding, PopoverMenuHandle, Scrollbar, ScrollbarState, TextSize,
Tooltip, prelude::*,
};
use util::ResultExt as _;
use util::markdown::MarkdownCodeBlock;
@@ -58,6 +58,7 @@ use zed_llm_client::CompletionIntent;
const CODEBLOCK_CONTAINER_GROUP: &str = "codeblock_container";
const EDIT_PREVIOUS_MESSAGE_MIN_LINES: usize = 1;
const RESPONSE_PADDING_X: Pixels = px(19.);
pub struct ActiveThread {
context_store: Entity<ContextStore>,
@@ -204,7 +205,7 @@ pub(crate) fn default_markdown_style(window: &Window, cx: &App) -> MarkdownStyle
MarkdownStyle {
base_text_style: text_style.clone(),
syntax: cx.theme().syntax().clone(),
selection_background_color: cx.theme().players().local().selection,
selection_background_color: cx.theme().colors().element_selection_background,
code_block_overflow_x_scroll: true,
table_overflow_x_scroll: true,
heading_level_styles: Some(HeadingLevelStyles {
@@ -301,7 +302,7 @@ fn tool_use_markdown_style(window: &Window, cx: &mut App) -> MarkdownStyle {
MarkdownStyle {
base_text_style: text_style,
syntax: cx.theme().syntax().clone(),
selection_background_color: cx.theme().players().local().selection,
selection_background_color: cx.theme().colors().element_selection_background,
code_block_overflow_x_scroll: false,
code_block: StyleRefinement {
margin: EdgesRefinement::default(),
@@ -689,9 +690,12 @@ fn open_markdown_link(
})
.context("Could not find matching symbol")?;
editor.change_selections(Some(Autoscroll::center()), window, cx, |s| {
s.select_anchor_ranges([symbol_range.start..symbol_range.start])
});
editor.change_selections(
SelectionEffects::scroll(Autoscroll::center()),
window,
cx,
|s| s.select_anchor_ranges([symbol_range.start..symbol_range.start]),
);
anyhow::Ok(())
})
})
@@ -708,10 +712,15 @@ fn open_markdown_link(
.downcast::<Editor>()
.context("Item is not an editor")?;
active_editor.update_in(cx, |editor, window, cx| {
editor.change_selections(Some(Autoscroll::center()), window, cx, |s| {
s.select_ranges([Point::new(line_range.start as u32, 0)
..Point::new(line_range.start as u32, 0)])
});
editor.change_selections(
SelectionEffects::scroll(Autoscroll::center()),
window,
cx,
|s| {
s.select_ranges([Point::new(line_range.start as u32, 0)
..Point::new(line_range.start as u32, 0)])
},
);
anyhow::Ok(())
})
})
@@ -809,7 +818,12 @@ impl ActiveThread {
};
for message in thread.read(cx).messages().cloned().collect::<Vec<_>>() {
this.push_message(&message.id, &message.segments, window, cx);
let rendered_message = RenderedMessage::from_segments(
&message.segments,
this.language_registry.clone(),
cx,
);
this.push_rendered_message(message.id, rendered_message);
for tool_use in thread.read(cx).tool_uses_for_message(message.id, cx) {
this.render_tool_use_markdown(
@@ -875,36 +889,51 @@ impl ActiveThread {
&self.text_thread_store
}
fn push_message(
&mut self,
id: &MessageId,
segments: &[MessageSegment],
_window: &mut Window,
cx: &mut Context<Self>,
) {
let old_len = self.messages.len();
self.messages.push(*id);
self.list_state.splice(old_len..old_len, 1);
pub fn validate_image(&self, image: &Arc<gpui::Image>, cx: &App) -> Result<(), String> {
let image_size = image.bytes().len() as u64;
let rendered_message =
RenderedMessage::from_segments(segments, self.language_registry.clone(), cx);
self.rendered_messages_by_id.insert(*id, rendered_message);
if let Some(model) = self.thread.read(cx).configured_model() {
let max_size = model.model.max_image_size();
if image_size > max_size {
if max_size == 0 {
Err(format!(
"{} does not support image attachments",
model.model.name().0
))
} else {
let size_mb = image_size as f64 / 1_048_576.0;
let max_size_mb = max_size as f64 / 1_048_576.0;
Err(format!(
"Image ({:.1} MB) exceeds {}'s {:.1} MB size limit",
size_mb,
model.model.name().0,
max_size_mb
))
}
} else {
Ok(())
}
} else {
// No model configured, use default 10MB limit
const DEFAULT_MAX_SIZE: u64 = 10 * 1024 * 1024;
if image_size > DEFAULT_MAX_SIZE {
let size_mb = image_size as f64 / 1_048_576.0;
Err(format!(
"Image ({:.1} MB) exceeds the 10 MB size limit",
size_mb
))
} else {
Ok(())
}
}
}
fn edited_message(
&mut self,
id: &MessageId,
segments: &[MessageSegment],
_window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(index) = self.messages.iter().position(|message_id| message_id == id) else {
return;
};
self.list_state.splice(index..index + 1, 1);
let rendered_message =
RenderedMessage::from_segments(segments, self.language_registry.clone(), cx);
self.rendered_messages_by_id.insert(*id, rendered_message);
fn push_rendered_message(&mut self, id: MessageId, rendered_message: RenderedMessage) {
let old_len = self.messages.len();
self.messages.push(id);
self.list_state.splice(old_len..old_len, 1);
self.rendered_messages_by_id.insert(id, rendered_message);
}
fn deleted_message(&mut self, id: &MessageId) {
@@ -1037,31 +1066,43 @@ impl ActiveThread {
}
}
ThreadEvent::MessageAdded(message_id) => {
if let Some(message_segments) = self
.thread
.read(cx)
.message(*message_id)
.map(|message| message.segments.clone())
{
self.push_message(message_id, &message_segments, window, cx);
if let Some(rendered_message) = self.thread.update(cx, |thread, cx| {
thread.message(*message_id).map(|message| {
RenderedMessage::from_segments(
&message.segments,
self.language_registry.clone(),
cx,
)
})
}) {
self.push_rendered_message(*message_id, rendered_message);
}
self.save_thread(cx);
cx.notify();
}
ThreadEvent::MessageEdited(message_id) => {
if let Some(message_segments) = self
.thread
.read(cx)
.message(*message_id)
.map(|message| message.segments.clone())
{
self.edited_message(message_id, &message_segments, window, cx);
if let Some(index) = self.messages.iter().position(|id| id == message_id) {
if let Some(rendered_message) = self.thread.update(cx, |thread, cx| {
thread.message(*message_id).map(|message| {
let mut rendered_message = RenderedMessage {
language_registry: self.language_registry.clone(),
segments: Vec::with_capacity(message.segments.len()),
};
for segment in &message.segments {
rendered_message.push_segment(segment, cx);
}
rendered_message
})
}) {
self.list_state.splice(index..index + 1, 1);
self.rendered_messages_by_id
.insert(*message_id, rendered_message);
self.scroll_to_bottom(cx);
self.save_thread(cx);
cx.notify();
}
}
self.scroll_to_bottom(cx);
self.save_thread(cx);
cx.notify();
}
ThreadEvent::MessageDeleted(message_id) => {
self.deleted_message(message_id);
@@ -1148,6 +1189,9 @@ impl ActiveThread {
self.save_thread(cx);
cx.notify();
}
ThreadEvent::RetriesFailed { message } => {
self.show_notification(message, ui::IconName::Warning, window, cx);
}
}
}
@@ -1311,17 +1355,11 @@ impl ActiveThread {
fn start_editing_message(
&mut self,
message_id: MessageId,
message_segments: &[MessageSegment],
message_text: impl Into<Arc<str>>,
message_creases: &[MessageCrease],
window: &mut Window,
cx: &mut Context<Self>,
) {
// User message should always consist of a single text segment,
// therefore we can skip returning early if it's not a text segment.
let Some(MessageSegment::Text(message_text)) = message_segments.first() else {
return;
};
let editor = crate::message_editor::create_editor(
self.workspace.clone(),
self.context_store.downgrade(),
@@ -1333,7 +1371,7 @@ impl ActiveThread {
cx,
);
editor.update(cx, |editor, cx| {
editor.set_text(message_text.clone(), window, cx);
editor.set_text(message_text, window, cx);
insert_message_creases(editor, message_creases, &self.context_store, window, cx);
editor.focus_handle(cx).focus(window);
editor.move_to_end(&editor::actions::MoveToEnd, window, cx);
@@ -1524,7 +1562,7 @@ impl ActiveThread {
}
fn paste(&mut self, _: &Paste, _window: &mut Window, cx: &mut Context<Self>) {
attach_pasted_images_as_context(&self.context_store, cx);
attach_pasted_images_as_context_with_validation(&self.context_store, Some(self), cx);
}
fn cancel_editing_message(
@@ -1828,8 +1866,6 @@ impl ActiveThread {
return div().children(loading_dots).into_any();
}
let message_creases = message.creases.clone();
let Some(rendered_message) = self.rendered_messages_by_id.get(&message_id) else {
return Empty.into_any();
};
@@ -1851,9 +1887,10 @@ impl ActiveThread {
.filter(|(id, _)| *id == message_id)
.map(|(_, state)| state);
let colors = cx.theme().colors();
let editor_bg_color = colors.editor_background;
let panel_bg = colors.panel_background;
let (editor_bg_color, panel_bg) = {
let colors = cx.theme().colors();
(colors.editor_background, colors.panel_background)
};
let open_as_markdown = IconButton::new(("open-as-markdown", ix), IconName::DocumentText)
.icon_size(IconSize::XSmall)
@@ -1878,9 +1915,6 @@ impl ActiveThread {
this.scroll_to_top(cx);
}));
// For all items that should be aligned with the LLM's response.
const RESPONSE_PADDING_X: Pixels = px(19.);
let show_feedback = thread.is_turn_end(ix);
let feedback_container = h_flex()
.group("feedback_container")
@@ -2041,137 +2075,162 @@ impl ActiveThread {
}
});
let styled_message = match message.role {
Role::User => v_flex()
.id(("message-container", ix))
.pt_2()
.pl_2()
.pr_2p5()
.pb_4()
.child(
let styled_message = if message.ui_only {
self.render_ui_notification(message_content, ix, cx)
} else {
match message.role {
Role::User => {
let colors = cx.theme().colors();
v_flex()
.id(("user-message", ix))
.bg(editor_bg_color)
.rounded_lg()
.shadow_md()
.border_1()
.border_color(colors.border)
.hover(|hover| hover.border_color(colors.text_accent.opacity(0.5)))
.id(("message-container", ix))
.pt_2()
.pl_2()
.pr_2p5()
.pb_4()
.child(
v_flex()
.p_2p5()
.gap_1()
.children(message_content)
.when_some(editing_message_state, |this, state| {
let focus_handle = state.editor.focus_handle(cx).clone();
.id(("user-message", ix))
.bg(editor_bg_color)
.rounded_lg()
.shadow_md()
.border_1()
.border_color(colors.border)
.hover(|hover| hover.border_color(colors.text_accent.opacity(0.5)))
.child(
v_flex()
.p_2p5()
.gap_1()
.children(message_content)
.when_some(editing_message_state, |this, state| {
let focus_handle = state.editor.focus_handle(cx).clone();
this.child(
h_flex()
.w_full()
.gap_1()
.justify_between()
.flex_wrap()
.child(
this.child(
h_flex()
.gap_1p5()
.w_full()
.gap_1()
.justify_between()
.flex_wrap()
.child(
div()
.opacity(0.8)
h_flex()
.gap_1p5()
.child(
Icon::new(IconName::Warning)
.size(IconSize::Indicator)
.color(Color::Warning)
div()
.opacity(0.8)
.child(
Icon::new(IconName::Warning)
.size(IconSize::Indicator)
.color(Color::Warning)
),
)
.child(
Label::new("Editing will restart the thread from this point.")
.color(Color::Muted)
.size(LabelSize::XSmall),
),
)
.child(
Label::new("Editing will restart the thread from this point.")
.color(Color::Muted)
.size(LabelSize::XSmall),
),
)
.child(
h_flex()
.gap_0p5()
.child(
IconButton::new(
"cancel-edit-message",
IconName::Close,
)
.shape(ui::IconButtonShape::Square)
.icon_color(Color::Error)
.icon_size(IconSize::Small)
.tooltip({
let focus_handle = focus_handle.clone();
move |window, cx| {
Tooltip::for_action_in(
"Cancel Edit",
&menu::Cancel,
&focus_handle,
window,
cx,
h_flex()
.gap_0p5()
.child(
IconButton::new(
"cancel-edit-message",
IconName::Close,
)
}
})
.on_click(cx.listener(Self::handle_cancel_click)),
.shape(ui::IconButtonShape::Square)
.icon_color(Color::Error)
.icon_size(IconSize::Small)
.tooltip({
let focus_handle = focus_handle.clone();
move |window, cx| {
Tooltip::for_action_in(
"Cancel Edit",
&menu::Cancel,
&focus_handle,
window,
cx,
)
}
})
.on_click(cx.listener(Self::handle_cancel_click)),
)
.child(
IconButton::new(
"confirm-edit-message",
IconName::Return,
)
.disabled(state.editor.read(cx).is_empty(cx))
.shape(ui::IconButtonShape::Square)
.icon_color(Color::Muted)
.icon_size(IconSize::Small)
.tooltip({
let focus_handle = focus_handle.clone();
move |window, cx| {
Tooltip::for_action_in(
"Regenerate",
&menu::Confirm,
&focus_handle,
window,
cx,
)
}
})
.on_click(
cx.listener(Self::handle_regenerate_click),
),
),
)
.child(
IconButton::new(
"confirm-edit-message",
IconName::Return,
)
.disabled(state.editor.read(cx).is_empty(cx))
.shape(ui::IconButtonShape::Square)
.icon_color(Color::Muted)
.icon_size(IconSize::Small)
.tooltip({
let focus_handle = focus_handle.clone();
move |window, cx| {
Tooltip::for_action_in(
"Regenerate",
&menu::Confirm,
&focus_handle,
window,
cx,
)
}
})
.on_click(
cx.listener(Self::handle_regenerate_click),
),
),
)
)
}),
}),
)
.on_click(cx.listener({
let message_creases = message.creases.clone();
move |this, _, window, cx| {
if let Some(message_text) =
this.thread.read(cx).message(message_id).and_then(|message| {
message.segments.first().and_then(|segment| {
match segment {
MessageSegment::Text(message_text) => {
Some(Into::<Arc<str>>::into(message_text.as_str()))
}
_ => {
None
}
}
})
})
{
this.start_editing_message(
message_id,
message_text,
&message_creases,
window,
cx,
);
}
}
})),
)
.on_click(cx.listener({
let message_segments = message.segments.clone();
move |this, _, window, cx| {
this.start_editing_message(
message_id,
&message_segments,
&message_creases,
window,
cx,
);
}
})),
),
Role::Assistant => v_flex()
.id(("message-container", ix))
.px(RESPONSE_PADDING_X)
.gap_2()
.children(message_content)
.when(has_tool_uses, |parent| {
parent.children(tool_uses.into_iter().map(|tool_use| {
self.render_tool_use(tool_use, window, workspace.clone(), cx)
}))
}),
Role::System => div().id(("message-container", ix)).py_1().px_2().child(
v_flex()
.bg(colors.editor_background)
.rounded_sm()
.child(div().p_4().children(message_content)),
),
}
Role::Assistant => v_flex()
.id(("message-container", ix))
.px(RESPONSE_PADDING_X)
.gap_2()
.children(message_content)
.when(has_tool_uses, |parent| {
parent.children(tool_uses.into_iter().map(|tool_use| {
self.render_tool_use(tool_use, window, workspace.clone(), cx)
}))
}),
Role::System => {
let colors = cx.theme().colors();
div().id(("message-container", ix)).py_1().px_2().child(
v_flex()
.bg(colors.editor_background)
.rounded_sm()
.child(div().p_4().children(message_content)),
)
}
}
};
let after_editing_message = self
@@ -2510,6 +2569,26 @@ impl ActiveThread {
.blend(cx.theme().colors().editor_foreground.opacity(0.025))
}
fn render_ui_notification(
&self,
message_content: impl IntoIterator<Item = impl IntoElement>,
ix: usize,
cx: &mut Context<Self>,
) -> Stateful<Div> {
let message = div()
.flex_1()
.min_w_0()
.text_size(TextSize::XSmall.rems(cx))
.text_color(cx.theme().colors().text_muted)
.children(message_content);
div()
.id(("message-container", ix))
.py_1()
.px_2p5()
.child(Banner::new().severity(ui::Severity::Warning).child(message))
}
fn render_message_thinking_segment(
&self,
message_id: MessageId,
@@ -3664,6 +3743,14 @@ pub(crate) fn open_context(
pub(crate) fn attach_pasted_images_as_context(
context_store: &Entity<ContextStore>,
cx: &mut App,
) -> bool {
attach_pasted_images_as_context_with_validation(context_store, None, cx)
}
pub(crate) fn attach_pasted_images_as_context_with_validation(
context_store: &Entity<ContextStore>,
active_thread: Option<&ActiveThread>,
cx: &mut App,
) -> bool {
let images = cx
.read_from_clipboard()
@@ -3685,9 +3772,67 @@ pub(crate) fn attach_pasted_images_as_context(
}
cx.stop_propagation();
// Try to find the workspace for showing toasts
let workspace = cx
.active_window()
.and_then(|window| window.downcast::<Workspace>());
context_store.update(cx, |store, cx| {
for image in images {
store.add_image_instance(Arc::new(image), cx);
let image_arc = Arc::new(image);
// Validate image if we have an active thread
let should_add = if let Some(thread) = active_thread {
match thread.validate_image(&image_arc, cx) {
Ok(()) => true,
Err(err) => {
// Show error toast if we have a workspace
if let Some(workspace) = workspace {
let _ = workspace.update(cx, |workspace, _, cx| {
use workspace::{Toast, notifications::NotificationId};
struct ImageRejectionToast;
workspace.show_toast(
Toast::new(
NotificationId::unique::<ImageRejectionToast>(),
err,
),
cx,
);
});
}
false
}
}
} else {
// No active thread, check against default limit
let image_size = image_arc.bytes().len() as u64;
const DEFAULT_MAX_SIZE: u64 = 10 * 1024 * 1024; // 10MB
if image_size > DEFAULT_MAX_SIZE {
let size_mb = image_size as f64 / 1_048_576.0;
let err = format!("Image ({:.1} MB) exceeds the 10 MB size limit", size_mb);
if let Some(workspace) = workspace {
let _ = workspace.update(cx, |workspace, _, cx| {
use workspace::{Toast, notifications::NotificationId};
struct ImageRejectionToast;
workspace.show_toast(
Toast::new(NotificationId::unique::<ImageRejectionToast>(), err),
cx,
);
});
}
false
} else {
true
}
};
if should_add {
store.add_image_instance(image_arc, cx);
}
}
});
true
@@ -3764,9 +3909,9 @@ mod tests {
// Stream response to user message
thread.update(cx, |thread, cx| {
let request =
thread.to_completion_request(model.clone(), CompletionIntent::UserPrompt, cx);
thread.stream_completion(request, model, cx.active_window(), cx)
let intent = CompletionIntent::UserPrompt;
let request = thread.to_completion_request(model.clone(), intent, cx);
thread.stream_completion(request, model, intent, cx.active_window(), cx)
});
// Follow the agent
cx.update(|window, cx| {
@@ -3826,13 +3971,15 @@ mod tests {
});
active_thread.update_in(cx, |active_thread, window, cx| {
active_thread.start_editing_message(
message.id,
message.segments.as_slice(),
message.creases.as_slice(),
window,
cx,
);
if let Some(message_text) = message.segments.first().and_then(MessageSegment::text) {
active_thread.start_editing_message(
message.id,
message_text,
message.creases.as_slice(),
window,
cx,
);
}
let editor = active_thread
.editing_message
.as_ref()
@@ -3847,13 +3994,15 @@ mod tests {
let message = thread.update(cx, |thread, _| thread.message(message.id).cloned().unwrap());
active_thread.update_in(cx, |active_thread, window, cx| {
active_thread.start_editing_message(
message.id,
message.segments.as_slice(),
message.creases.as_slice(),
window,
cx,
);
if let Some(message_text) = message.segments.first().and_then(MessageSegment::text) {
active_thread.start_editing_message(
message.id,
message_text,
message.creases.as_slice(),
window,
cx,
);
}
let editor = active_thread
.editing_message
.as_ref()
@@ -3935,13 +4084,15 @@ mod tests {
// Edit the message while the completion is still running
active_thread.update_in(cx, |active_thread, window, cx| {
active_thread.start_editing_message(
message.id,
message.segments.as_slice(),
message.creases.as_slice(),
window,
cx,
);
if let Some(message_text) = message.segments.first().and_then(MessageSegment::text) {
active_thread.start_editing_message(
message.id,
message_text,
message.creases.as_slice(),
window,
cx,
);
}
let editor = active_thread
.editing_message
.as_ref()

View File

@@ -180,7 +180,7 @@ impl ConfigurationSource {
}
fn context_server_input(existing: Option<(ContextServerId, ContextServerCommand)>) -> String {
let (name, path, args, env) = match existing {
let (name, command, args, env) = match existing {
Some((id, cmd)) => {
let args = serde_json::to_string(&cmd.args).unwrap();
let env = serde_json::to_string(&cmd.env.unwrap_or_default()).unwrap();
@@ -198,14 +198,12 @@ fn context_server_input(existing: Option<(ContextServerId, ContextServerCommand)
r#"{{
/// The name of your MCP server
"{name}": {{
"command": {{
/// The path to the executable
"path": "{path}",
/// The arguments to pass to the executable
"args": {args},
/// The environment variables to set for the executable
"env": {env}
}}
/// The command which runs the MCP server
"command": "{command}",
/// The arguments to pass to the MCP server
"args": {args},
/// The environment variables to set
"env": {env}
}}
}}"#
)
@@ -295,10 +293,7 @@ impl ConfigureContextServerModal {
ContextServerDescriptorRegistry::default_global(cx)
.read(cx)
.context_server_descriptor(&server_id.0)
.map(|_| ContextServerSettings::Extension {
enabled: true,
settings: serde_json::json!({}),
})
.map(|_| ContextServerSettings::default_extension())
})
else {
return Task::ready(Err(anyhow::anyhow!("Context server not found")));
@@ -442,8 +437,7 @@ fn parse_input(text: &str) -> Result<(ContextServerId, ContextServerCommand)> {
let object = value.as_object().context("Expected object")?;
anyhow::ensure!(object.len() == 1, "Expected exactly one key-value pair");
let (context_server_name, value) = object.into_iter().next().unwrap();
let command = value.get("command").context("Expected command")?;
let command: ContextServerCommand = serde_json::from_value(command.clone())?;
let command: ContextServerCommand = serde_json::from_value(value.clone())?;
Ok((ContextServerId(context_server_name.clone().into()), command))
}
@@ -751,7 +745,7 @@ pub(crate) fn default_markdown_style(window: &Window, cx: &App) -> MarkdownStyle
MarkdownStyle {
base_text_style: text_style.clone(),
selection_background_color: cx.theme().players().local().selection,
selection_background_color: colors.element_selection_background,
link: TextStyleRefinement {
background_color: Some(colors.editor_foreground.opacity(0.025)),
underline: Some(UnderlineStyle {

View File

@@ -272,42 +272,35 @@ impl PickerDelegate for ToolPickerDelegate {
let server_id = server_id.clone();
let tool_name = tool_name.clone();
move |settings: &mut AgentSettingsContent, _cx| {
settings
.v2_setting(|v2_settings| {
let profiles = v2_settings.profiles.get_or_insert_default();
let profile =
profiles
.entry(profile_id)
.or_insert_with(|| AgentProfileContent {
name: default_profile.name.into(),
tools: default_profile.tools,
enable_all_context_servers: Some(
default_profile.enable_all_context_servers,
),
context_servers: default_profile
.context_servers
.into_iter()
.map(|(server_id, preset)| {
(
server_id,
ContextServerPresetContent {
tools: preset.tools,
},
)
})
.collect(),
});
let profiles = settings.profiles.get_or_insert_default();
let profile = profiles
.entry(profile_id)
.or_insert_with(|| AgentProfileContent {
name: default_profile.name.into(),
tools: default_profile.tools,
enable_all_context_servers: Some(
default_profile.enable_all_context_servers,
),
context_servers: default_profile
.context_servers
.into_iter()
.map(|(server_id, preset)| {
(
server_id,
ContextServerPresetContent {
tools: preset.tools,
},
)
})
.collect(),
});
if let Some(server_id) = server_id {
let preset = profile.context_servers.entry(server_id).or_default();
*preset.tools.entry(tool_name).or_default() = !is_currently_enabled;
} else {
*profile.tools.entry(tool_name).or_default() = !is_currently_enabled;
}
Ok(())
})
.ok();
if let Some(server_id) = server_id {
let preset = profile.context_servers.entry(server_id).or_default();
*preset.tools.entry(tool_name).or_default() = !is_currently_enabled;
} else {
*profile.tools.entry(tool_name).or_default() = !is_currently_enabled;
}
}
});
}

View File

@@ -5,7 +5,8 @@ use anyhow::Result;
use buffer_diff::DiffHunkStatus;
use collections::{HashMap, HashSet};
use editor::{
Direction, Editor, EditorEvent, EditorSettings, MultiBuffer, MultiBufferSnapshot, ToPoint,
Direction, Editor, EditorEvent, EditorSettings, MultiBuffer, MultiBufferSnapshot,
SelectionEffects, ToPoint,
actions::{GoToHunk, GoToPreviousHunk},
scroll::Autoscroll,
};
@@ -171,15 +172,9 @@ impl AgentDiffPane {
if let Some(first_hunk) = first_hunk {
let first_hunk_start = first_hunk.multi_buffer_range().start;
editor.change_selections(
Some(Autoscroll::fit()),
window,
cx,
|selections| {
selections
.select_anchor_ranges([first_hunk_start..first_hunk_start]);
},
)
editor.change_selections(Default::default(), window, cx, |selections| {
selections.select_anchor_ranges([first_hunk_start..first_hunk_start]);
})
}
}
@@ -242,7 +237,7 @@ impl AgentDiffPane {
if let Some(first_hunk) = first_hunk {
let first_hunk_start = first_hunk.multi_buffer_range().start;
editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
editor.change_selections(Default::default(), window, cx, |selections| {
selections.select_anchor_ranges([first_hunk_start..first_hunk_start]);
})
}
@@ -416,7 +411,7 @@ fn update_editor_selection(
};
if let Some(target_hunk) = target_hunk {
editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
editor.change_selections(Default::default(), window, cx, |selections| {
let next_hunk_start = target_hunk.multi_buffer_range().start;
selections.select_anchor_ranges([next_hunk_start..next_hunk_start]);
})
@@ -1380,6 +1375,7 @@ impl AgentDiff {
| ThreadEvent::ToolConfirmationNeeded
| ThreadEvent::ToolUseLimitReached
| ThreadEvent::CancelEditing
| ThreadEvent::RetriesFailed { .. }
| ThreadEvent::ProfileChanged => {}
}
}
@@ -1543,7 +1539,7 @@ impl AgentDiff {
let first_hunk_start = first_hunk.multi_buffer_range().start;
editor.change_selections(
Some(Autoscroll::center()),
SelectionEffects::scroll(Autoscroll::center()),
window,
cx,
|selections| {
@@ -1867,7 +1863,7 @@ mod tests {
// Rejecting a hunk also moves the cursor to the next hunk, possibly cycling if it's at the end.
editor.update_in(cx, |editor, window, cx| {
editor.change_selections(None, window, cx, |selections| {
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
selections.select_ranges([Point::new(10, 0)..Point::new(10, 0)])
});
});
@@ -2123,7 +2119,7 @@ mod tests {
// Rejecting a hunk also moves the cursor to the next hunk, possibly cycling if it's at the end.
editor1.update_in(cx, |editor, window, cx| {
editor.change_selections(None, window, cx, |selections| {
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
selections.select_ranges([Point::new(10, 0)..Point::new(10, 0)])
});
});

View File

@@ -11,7 +11,7 @@ use language_model::{ConfiguredModel, LanguageModelRegistry};
use picker::popover_menu::PickerPopoverMenu;
use settings::update_settings_file;
use std::sync::Arc;
use ui::{PopoverMenuHandle, Tooltip, prelude::*};
use ui::{ButtonLike, PopoverMenuHandle, Tooltip, prelude::*};
pub struct AgentModelSelector {
selector: Entity<LanguageModelSelector>,
@@ -94,20 +94,35 @@ impl Render for AgentModelSelector {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let model = self.selector.read(cx).delegate.active_model(cx);
let model_name = model
.as_ref()
.map(|model| model.model.name().0)
.unwrap_or_else(|| SharedString::from("No model selected"));
let provider_icon = model
.as_ref()
.map(|model| model.provider.icon())
.unwrap_or_else(|| IconName::Ai);
let focus_handle = self.focus_handle.clone();
PickerPopoverMenu::new(
self.selector.clone(),
Button::new("active-model", model_name)
.label_size(LabelSize::Small)
.color(Color::Muted)
.icon(IconName::ChevronDown)
.icon_size(IconSize::XSmall)
.icon_position(IconPosition::End)
.icon_color(Color::Muted),
ButtonLike::new("active-model")
.child(
Icon::new(provider_icon)
.color(Color::Muted)
.size(IconSize::XSmall),
)
.child(
Label::new(model_name)
.color(Color::Muted)
.size(LabelSize::Small)
.ml_0p5(),
)
.child(
Icon::new(IconName::ChevronDown)
.color(Color::Muted)
.size(IconSize::XSmall),
),
move |window, cx| {
Tooltip::for_action_in(
"Change Model",

File diff suppressed because it is too large Load Diff

View File

@@ -4,14 +4,15 @@ mod agent_diff;
mod agent_model_selector;
mod agent_panel;
mod buffer_codegen;
mod burn_mode_tooltip;
mod context_picker;
mod context_server_configuration;
mod context_strip;
mod debug;
mod inline_assistant;
mod inline_prompt_editor;
mod language_model_selector;
mod max_mode_tooltip;
mod message_editor;
mod profile_selector;
mod slash_command;
@@ -48,7 +49,7 @@ pub use crate::agent_panel::{AgentPanel, ConcreteAssistantPanelDelegate};
pub use crate::inline_assistant::InlineAssistant;
use crate::slash_command_settings::SlashCommandSettings;
pub use agent_diff::{AgentDiffPane, AgentDiffToolbar};
pub use text_thread_editor::AgentPanelDelegate;
pub use text_thread_editor::{AgentPanelDelegate, TextThreadEditor};
pub use ui::preview::{all_agent_previews, get_agent_preview};
actions!(
@@ -157,6 +158,7 @@ pub fn init(
agent::init(cx);
agent_panel::init(cx);
context_server_configuration::init(language_registry.clone(), fs.clone(), cx);
TextThreadEditor::init(cx);
register_slash_commands(cx);
inline_assistant::init(

View File

@@ -1094,15 +1094,9 @@ mod tests {
};
use language_model::{LanguageModelRegistry, TokenUsage};
use rand::prelude::*;
use serde::Serialize;
use settings::SettingsStore;
use std::{future, sync::Arc};
#[derive(Serialize)]
pub struct DummyCompletionRequest {
pub name: String,
}
#[gpui::test(iterations = 10)]
async fn test_transform_autoindent(cx: &mut TestAppContext, mut rng: StdRng) {
init_test(cx);

View File

@@ -1,11 +1,11 @@
use gpui::{Context, FontWeight, IntoElement, Render, Window};
use ui::{prelude::*, tooltip_container};
pub struct MaxModeTooltip {
pub struct BurnModeTooltip {
selected: bool,
}
impl MaxModeTooltip {
impl BurnModeTooltip {
pub fn new() -> Self {
Self { selected: false }
}
@@ -16,7 +16,7 @@ impl MaxModeTooltip {
}
}
impl Render for MaxModeTooltip {
impl Render for BurnModeTooltip {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let (icon, color) = if self.selected {
(IconName::ZedBurnModeOn, Color::Error)

View File

@@ -661,7 +661,7 @@ fn recent_context_picker_entries(
let active_thread_id = workspace
.panel::<AgentPanel>(cx)
.and_then(|panel| Some(panel.read(cx).active_thread()?.read(cx).id()));
.and_then(|panel| Some(panel.read(cx).active_thread(cx)?.read(cx).id()));
if let Some((thread_store, text_thread_store)) = thread_store
.and_then(|store| store.upgrade())
@@ -930,8 +930,8 @@ impl MentionLink {
format!(
"[@{} ({}-{})]({}:{}:{}-{})",
file_name,
line_range.start,
line_range.end,
line_range.start + 1,
line_range.end + 1,
Self::SELECTION,
full_path,
line_range.start,

View File

@@ -161,7 +161,7 @@ impl ContextStrip {
let workspace = self.workspace.upgrade()?;
let panel = workspace.read(cx).panel::<AgentPanel>(cx)?.read(cx);
if let Some(active_thread) = panel.active_thread() {
if let Some(active_thread) = panel.active_thread(cx) {
let weak_active_thread = active_thread.downgrade();
let active_thread = active_thread.read(cx);

View File

@@ -18,6 +18,7 @@ use agent_settings::AgentSettings;
use anyhow::{Context as _, Result};
use client::telemetry::Telemetry;
use collections::{HashMap, HashSet, VecDeque, hash_map};
use editor::SelectionEffects;
use editor::{
Anchor, AnchorRangeExt, CodeActionProvider, Editor, EditorEvent, ExcerptId, ExcerptRange,
MultiBuffer, MultiBufferSnapshot, ToOffset as _, ToPoint,
@@ -1159,7 +1160,7 @@ impl InlineAssistant {
let position = assist.range.start;
editor.update(cx, |editor, cx| {
editor.change_selections(None, window, cx, |selections| {
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
selections.select_anchor_ranges([position..position])
});

View File

@@ -399,7 +399,7 @@ impl PickerDelegate for LanguageModelPickerDelegate {
cx: &mut Context<Picker<Self>>,
) -> Task<()> {
let all_models = self.all_models.clone();
let current_index = self.selected_index;
let active_model = (self.get_active_model)(cx);
let bg_executor = cx.background_executor();
let language_model_registry = LanguageModelRegistry::global(cx);
@@ -441,12 +441,9 @@ impl PickerDelegate for LanguageModelPickerDelegate {
cx.spawn_in(window, async move |this, cx| {
this.update_in(cx, |this, window, cx| {
this.delegate.filtered_entries = filtered_models.entries();
// Preserve selection focus
let new_index = if current_index >= this.delegate.filtered_entries.len() {
0
} else {
current_index
};
// Finds the currently selected model in the list
let new_index =
Self::get_active_model_index(&this.delegate.filtered_entries, active_model);
this.set_selected_index(new_index, Some(picker::Direction::Down), true, window, cx);
cx.notify();
})
@@ -659,6 +656,10 @@ mod tests {
false
}
fn max_image_size(&self) -> u64 {
0
}
fn telemetry_id(&self) -> String {
format!("{}/{}", self.provider_id.0, self.name.0)
}

View File

@@ -27,6 +27,7 @@ use file_icons::FileIcons;
use fs::Fs;
use futures::future::Shared;
use futures::{FutureExt as _, future};
use gpui::AsyncApp;
use gpui::{
Animation, AnimationExt, App, Entity, EventEmitter, Focusable, Subscription, Task, TextStyle,
WeakEntity, linear_color_stop, linear_gradient, point, pulsating_between,
@@ -62,6 +63,7 @@ use agent::{
context_store::ContextStore,
thread_store::{TextThreadStore, ThreadStore},
};
use workspace::{Toast, notifications::NotificationId};
#[derive(RegisterComponent)]
pub struct MessageEditor {
@@ -380,11 +382,15 @@ impl MessageEditor {
let checkpoint = git_store.update(cx, |git_store, cx| git_store.checkpoint(cx));
let context_task = self.reload_context(cx);
let window_handle = window.window_handle();
let workspace = self.workspace.clone();
cx.spawn(async move |_this, cx| {
let (checkpoint, loaded_context) = future::join(checkpoint, context_task).await;
let loaded_context = loaded_context.unwrap_or_default();
// Check for rejected images and show notifications
Self::notify_rejected_images(&loaded_context, &model, &workspace, &cx);
thread
.update(cx, |thread, cx| {
thread.insert_user_message(
@@ -412,6 +418,80 @@ impl MessageEditor {
.detach();
}
fn notify_rejected_images(
loaded_context: &agent::context::ContextLoadResult,
model: &Arc<dyn language_model::LanguageModel>,
workspace: &WeakEntity<Workspace>,
cx: &AsyncApp,
) {
let rejected_images = loaded_context.loaded_context.check_image_size_limits(model);
if rejected_images.is_empty() {
return;
}
let workspace = workspace.clone();
let model_name = rejected_images[0].model_name.clone();
let max_size = model.max_image_size();
let count = rejected_images.len();
let rejected_images = rejected_images.clone();
cx.update(|cx| {
if let Some(workspace) = workspace.upgrade() {
workspace.update(cx, |workspace, cx| {
let message = if max_size == 0 {
Self::format_unsupported_images_message(&model_name, count)
} else {
Self::format_size_limit_message(
&model_name,
count,
max_size,
&rejected_images,
)
};
struct ImageRejectionToast;
workspace.show_toast(
Toast::new(NotificationId::unique::<ImageRejectionToast>(), message),
cx,
);
});
}
})
.log_err();
}
fn format_unsupported_images_message(model_name: &str, count: usize) -> String {
let plural = if count > 1 { "s" } else { "" };
format!(
"{} does not support image attachments. {} image{} will be excluded from your message.",
model_name, count, plural
)
}
fn format_size_limit_message(
model_name: &str,
count: usize,
max_size: u64,
rejected_images: &[agent::context::RejectedImage],
) -> String {
let plural = if count > 1 { "s" } else { "" };
let max_size_mb = max_size as f64 / 1_048_576.0;
// If only one image, show its specific size
if count == 1 {
let image_size_mb = rejected_images[0].size as f64 / 1_048_576.0;
format!(
"Image ({:.1} MB) exceeds {}'s {:.1} MB size limit and will be excluded.",
image_size_mb, model_name, max_size_mb
)
} else {
format!(
"{} image{} exceeded {}'s {:.1} MB size limit and will be excluded.",
count, plural, model_name, max_size_mb
)
}
}
fn stop_current_and_send_new_message(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.thread.update(cx, |thread, cx| {
thread.cancel_editing(cx);
@@ -575,7 +655,7 @@ impl MessageEditor {
fn render_burn_mode_toggle(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
let thread = self.thread.read(cx);
let model = thread.configured_model();
if !model?.model.supports_max_mode() {
if !model?.model.supports_burn_mode() {
return None;
}

View File

@@ -1,8 +1,8 @@
use crate::{
burn_mode_tooltip::BurnModeTooltip,
language_model_selector::{
LanguageModelSelector, ToggleModelSelector, language_model_selector,
},
max_mode_tooltip::MaxModeTooltip,
};
use agent_settings::{AgentSettings, CompletionMode};
use anyhow::Result;
@@ -21,7 +21,6 @@ use editor::{
BlockPlacement, BlockProperties, BlockStyle, Crease, CreaseMetadata, CustomBlockId, FoldId,
RenderBlock, ToDisplayPoint,
},
scroll::Autoscroll,
};
use editor::{FoldPlaceholder, display_map::CreaseId};
use fs::Fs;
@@ -69,7 +68,7 @@ use workspace::{
searchable::{Direction, SearchableItemHandle},
};
use workspace::{
Save, Toast, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
Save, Toast, Workspace,
item::{self, FollowableItem, Item, ItemHandle},
notifications::NotificationId,
pane,
@@ -389,7 +388,7 @@ impl TextThreadEditor {
cursor..cursor
};
self.editor.update(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
editor.change_selections(Default::default(), window, cx, |selections| {
selections.select_ranges([new_selection])
});
});
@@ -449,8 +448,7 @@ impl TextThreadEditor {
if let Some(command) = self.slash_commands.command(name, cx) {
self.editor.update(cx, |editor, cx| {
editor.transact(window, cx, |editor, window, cx| {
editor
.change_selections(Some(Autoscroll::fit()), window, cx, |s| s.try_cancel());
editor.change_selections(Default::default(), window, cx, |s| s.try_cancel());
let snapshot = editor.buffer().read(cx).snapshot(cx);
let newest_cursor = editor.selections.newest::<Point>(cx).head();
if newest_cursor.column > 0
@@ -1583,7 +1581,7 @@ impl TextThreadEditor {
self.editor.update(cx, |editor, cx| {
editor.transact(window, cx, |this, window, cx| {
this.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
this.change_selections(Default::default(), window, cx, |s| {
s.select(selections);
});
this.insert("", window, cx);
@@ -2075,12 +2073,12 @@ impl TextThreadEditor {
)
}
fn render_max_mode_toggle(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
fn render_burn_mode_toggle(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
let context = self.context().read(cx);
let active_model = LanguageModelRegistry::read_global(cx)
.default_model()
.map(|default| default.model)?;
if !active_model.supports_max_mode() {
if !active_model.supports_burn_mode() {
return None;
}
@@ -2107,7 +2105,7 @@ impl TextThreadEditor {
});
}))
.tooltip(move |_window, cx| {
cx.new(|_| MaxModeTooltip::new().selected(burn_mode_enabled))
cx.new(|_| BurnModeTooltip::new().selected(burn_mode_enabled))
.into()
})
.into_any_element(),
@@ -2122,12 +2120,21 @@ impl TextThreadEditor {
let active_model = LanguageModelRegistry::read_global(cx)
.default_model()
.map(|default| default.model);
let focus_handle = self.editor().focus_handle(cx).clone();
let model_name = match active_model {
Some(model) => model.name().0,
None => SharedString::from("No model selected"),
};
let active_provider = LanguageModelRegistry::read_global(cx)
.default_model()
.map(|default| default.provider);
let provider_icon = match active_provider {
Some(provider) => provider.icon(),
None => IconName::Ai,
};
let focus_handle = self.editor().focus_handle(cx).clone();
PickerPopoverMenu::new(
self.language_model_selector.clone(),
ButtonLike::new("active-model")
@@ -2135,10 +2142,16 @@ impl TextThreadEditor {
.child(
h_flex()
.gap_0p5()
.child(
Icon::new(provider_icon)
.color(Color::Muted)
.size(IconSize::XSmall),
)
.child(
Label::new(model_name)
.color(Color::Muted)
.size(LabelSize::Small)
.color(Color::Muted),
.ml_0p5(),
)
.child(
Icon::new(IconName::ChevronDown)
@@ -2575,7 +2588,7 @@ impl Render for TextThreadEditor {
};
let language_model_selector = self.language_model_selector_menu_handle.clone();
let max_mode_toggle = self.render_max_mode_toggle(cx);
let burn_mode_toggle = self.render_burn_mode_toggle(cx);
v_flex()
.key_context("ContextEditor")
@@ -2630,7 +2643,7 @@ impl Render for TextThreadEditor {
h_flex()
.gap_0p5()
.child(self.render_inject_context_menu(cx))
.when_some(max_mode_toggle, |this, element| this.child(element)),
.when_some(burn_mode_toggle, |this, element| this.child(element)),
)
.child(
h_flex()
@@ -2924,13 +2937,6 @@ impl FollowableItem for TextThreadEditor {
}
}
pub struct ContextEditorToolbarItem {
active_context_editor: Option<WeakEntity<TextThreadEditor>>,
model_summary_editor: Entity<Editor>,
}
impl ContextEditorToolbarItem {}
pub fn render_remaining_tokens(
context_editor: &Entity<TextThreadEditor>,
cx: &App,
@@ -2983,98 +2989,6 @@ pub fn render_remaining_tokens(
)
}
impl Render for ContextEditorToolbarItem {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let left_side = h_flex()
.group("chat-title-group")
.gap_1()
.items_center()
.flex_grow()
.child(
div()
.w_full()
.when(self.active_context_editor.is_some(), |left_side| {
left_side.child(self.model_summary_editor.clone())
}),
)
.child(
div().visible_on_hover("chat-title-group").child(
IconButton::new("regenerate-context", IconName::RefreshTitle)
.shape(ui::IconButtonShape::Square)
.tooltip(Tooltip::text("Regenerate Title"))
.on_click(cx.listener(move |_, _, _window, cx| {
cx.emit(ContextEditorToolbarItemEvent::RegenerateSummary)
})),
),
);
let right_side = h_flex()
.gap_2()
// TODO display this in a nicer way, once we have a design for it.
// .children({
// let project = self
// .workspace
// .upgrade()
// .map(|workspace| workspace.read(cx).project().downgrade());
//
// let scan_items_remaining = cx.update_global(|db: &mut SemanticDb, cx| {
// project.and_then(|project| db.remaining_summaries(&project, cx))
// });
// scan_items_remaining
// .map(|remaining_items| format!("Files to scan: {}", remaining_items))
// })
.children(
self.active_context_editor
.as_ref()
.and_then(|editor| editor.upgrade())
.and_then(|editor| render_remaining_tokens(&editor, cx)),
);
h_flex()
.px_0p5()
.size_full()
.gap_2()
.justify_between()
.child(left_side)
.child(right_side)
}
}
impl ToolbarItemView for ContextEditorToolbarItem {
fn set_active_pane_item(
&mut self,
active_pane_item: Option<&dyn ItemHandle>,
_window: &mut Window,
cx: &mut Context<Self>,
) -> ToolbarItemLocation {
self.active_context_editor = active_pane_item
.and_then(|item| item.act_as::<TextThreadEditor>(cx))
.map(|editor| editor.downgrade());
cx.notify();
if self.active_context_editor.is_none() {
ToolbarItemLocation::Hidden
} else {
ToolbarItemLocation::PrimaryRight
}
}
fn pane_focus_update(
&mut self,
_pane_focused: bool,
_window: &mut Window,
cx: &mut Context<Self>,
) {
cx.notify();
}
}
impl EventEmitter<ToolbarItemEvent> for ContextEditorToolbarItem {}
pub enum ContextEditorToolbarItemEvent {
RegenerateSummary,
}
impl EventEmitter<ContextEditorToolbarItemEvent> for ContextEditorToolbarItem {}
enum PendingSlashCommand {}
fn invoked_slash_command_fold_placeholder(
@@ -3240,6 +3154,7 @@ pub fn make_lsp_adapter_delegate(
#[cfg(test)]
mod tests {
use super::*;
use editor::SelectionEffects;
use fs::FakeFs;
use gpui::{App, TestAppContext, VisualTestContext};
use indoc::indoc;
@@ -3465,7 +3380,9 @@ mod tests {
) {
context_editor.update_in(cx, |context_editor, window, cx| {
context_editor.editor.update(cx, |editor, cx| {
editor.change_selections(None, window, cx, |s| s.select_ranges([range]));
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([range])
});
});
context_editor.copy(&Default::default(), window, cx);

View File

@@ -1,13 +1,13 @@
mod agent_notification;
mod animated_label;
mod burn_mode_tooltip;
mod context_pill;
mod max_mode_tooltip;
mod onboarding_modal;
pub mod preview;
mod upsell;
pub use agent_notification::*;
pub use animated_label::*;
pub use burn_mode_tooltip::*;
pub use context_pill::*;
pub use max_mode_tooltip::*;
pub use onboarding_modal::*;

View File

@@ -2117,6 +2117,7 @@ impl AssistantContext {
);
}
}
LanguageModelCompletionEvent::RedactedThinking { .. } => {},
LanguageModelCompletionEvent::Text(mut chunk) => {
if let Some(start) = thought_process_stack.pop() {
let end = buffer.anchor_before(message_old_end_offset);
@@ -2345,13 +2346,13 @@ impl AssistantContext {
completion_request.messages.push(request_message);
}
}
let supports_max_mode = if let Some(model) = model {
model.supports_max_mode()
let supports_burn_mode = if let Some(model) = model {
model.supports_burn_mode()
} else {
false
};
if supports_max_mode {
if supports_burn_mode {
completion_request.mode = Some(self.completion_mode.into());
}
completion_request
@@ -2522,6 +2523,12 @@ impl AssistantContext {
}
let message = start_message;
let at_end = range.end >= message.offset_range.end.saturating_sub(1);
let role_after = if range.start == range.end || at_end {
Role::User
} else {
message.role
};
let role = message.role;
let mut edited_buffer = false;
@@ -2556,7 +2563,7 @@ impl AssistantContext {
};
let suffix_metadata = MessageMetadata {
role,
role: role_after,
status: MessageStatus::Done,
timestamp: suffix.id.0,
cache: None,

View File

@@ -74,7 +74,7 @@ impl SlashCommand for DeltaSlashCommand {
.slice(section.range.to_offset(&context_buffer)),
);
file_command_new_outputs.push(Arc::new(FileSlashCommand).run(
&[metadata.path.clone()],
std::slice::from_ref(&metadata.path),
context_slash_command_output_sections,
context_buffer.clone(),
workspace.clone(),

View File

@@ -1470,7 +1470,7 @@ impl EditAgentTest {
Project::init_settings(cx);
language::init(cx);
language_model::init(client.clone(), cx);
language_models::init(user_store.clone(), client.clone(), fs.clone(), cx);
language_models::init(user_store.clone(), client.clone(), cx);
crate::init(client.http_client(), cx);
});

View File

@@ -9132,7 +9132,7 @@ impl Editor {
window: &mut Window,
cx: &mut Context<Self>,
) {
self.manipulate_lines(window, cx, |lines| lines.sort())
self.manipulate_immutable_lines(window, cx, |lines| lines.sort())
}
pub fn sort_lines_case_insensitive(
@@ -9141,7 +9141,7 @@ impl Editor {
window: &mut Window,
cx: &mut Context<Self>,
) {
self.manipulate_lines(window, cx, |lines| {
self.manipulate_immutable_lines(window, cx, |lines| {
lines.sort_by_key(|line| line.to_lowercase())
})
}
@@ -9152,7 +9152,7 @@ impl Editor {
window: &mut Window,
cx: &mut Context<Self>,
) {
self.manipulate_lines(window, cx, |lines| {
self.manipulate_immutable_lines(window, cx, |lines| {
let mut seen = HashSet::default();
lines.retain(|line| seen.insert(line.to_lowercase()));
})
@@ -9164,7 +9164,7 @@ impl Editor {
window: &mut Window,
cx: &mut Context<Self>,
) {
self.manipulate_lines(window, cx, |lines| {
self.manipulate_immutable_lines(window, cx, |lines| {
let mut seen = HashSet::default();
lines.retain(|line| seen.insert(*line));
})
@@ -9606,20 +9606,20 @@ impl Editor {
}
pub fn reverse_lines(&mut self, _: &ReverseLines, window: &mut Window, cx: &mut Context<Self>) {
self.manipulate_lines(window, cx, |lines| lines.reverse())
self.manipulate_immutable_lines(window, cx, |lines| lines.reverse())
}
pub fn shuffle_lines(&mut self, _: &ShuffleLines, window: &mut Window, cx: &mut Context<Self>) {
self.manipulate_lines(window, cx, |lines| lines.shuffle(&mut thread_rng()))
self.manipulate_immutable_lines(window, cx, |lines| lines.shuffle(&mut thread_rng()))
}
fn manipulate_lines<Fn>(
fn manipulate_lines<M>(
&mut self,
window: &mut Window,
cx: &mut Context<Self>,
mut callback: Fn,
mut manipulate: M,
) where
Fn: FnMut(&mut Vec<&str>),
M: FnMut(&str) -> LineManipulationResult,
{
self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction);
@@ -9652,18 +9652,14 @@ impl Editor {
.text_for_range(start_point..end_point)
.collect::<String>();
let mut lines = text.split('\n').collect_vec();
let LineManipulationResult { new_text, line_count_before, line_count_after} = manipulate(&text);
let lines_before = lines.len();
callback(&mut lines);
let lines_after = lines.len();
edits.push((start_point..end_point, lines.join("\n")));
edits.push((start_point..end_point, new_text));
// Selections must change based on added and removed line count
let start_row =
MultiBufferRow(start_point.row + added_lines as u32 - removed_lines as u32);
let end_row = MultiBufferRow(start_row.0 + lines_after.saturating_sub(1) as u32);
let end_row = MultiBufferRow(start_row.0 + line_count_after.saturating_sub(1) as u32);
new_selections.push(Selection {
id: selection.id,
start: start_row,
@@ -9672,10 +9668,10 @@ impl Editor {
reversed: selection.reversed,
});
if lines_after > lines_before {
added_lines += lines_after - lines_before;
} else if lines_before > lines_after {
removed_lines += lines_before - lines_after;
if line_count_after > line_count_before {
added_lines += line_count_after - line_count_before;
} else if line_count_before > line_count_after {
removed_lines += line_count_before - line_count_after;
}
}
@@ -9720,6 +9716,171 @@ impl Editor {
})
}
fn manipulate_immutable_lines<Fn>(
&mut self,
window: &mut Window,
cx: &mut Context<Self>,
mut callback: Fn,
) where
Fn: FnMut(&mut Vec<&str>),
{
self.manipulate_lines(window, cx, |text| {
let mut lines: Vec<&str> = text.split('\n').collect();
let line_count_before = lines.len();
callback(&mut lines);
LineManipulationResult {
new_text: lines.join("\n"),
line_count_before,
line_count_after: lines.len(),
}
});
}
fn manipulate_mutable_lines<Fn>(
&mut self,
window: &mut Window,
cx: &mut Context<Self>,
mut callback: Fn,
) where
Fn: FnMut(&mut Vec<Cow<'_, str>>),
{
self.manipulate_lines(window, cx, |text| {
let mut lines: Vec<Cow<str>> = text.split('\n').map(Cow::from).collect();
let line_count_before = lines.len();
callback(&mut lines);
LineManipulationResult {
new_text: lines.join("\n"),
line_count_before,
line_count_after: lines.len(),
}
});
}
pub fn convert_indentation_to_spaces(
&mut self,
_: &ConvertIndentationToSpaces,
window: &mut Window,
cx: &mut Context<Self>,
) {
let settings = self.buffer.read(cx).language_settings(cx);
let tab_size = settings.tab_size.get() as usize;
self.manipulate_mutable_lines(window, cx, |lines| {
// Allocates a reasonably sized scratch buffer once for the whole loop
let mut reindented_line = String::with_capacity(MAX_LINE_LEN);
// Avoids recomputing spaces that could be inserted many times
let space_cache: Vec<Vec<char>> = (1..=tab_size)
.map(|n| IndentSize::spaces(n as u32).chars().collect())
.collect();
for line in lines.iter_mut().filter(|line| !line.is_empty()) {
let mut chars = line.as_ref().chars();
let mut col = 0;
let mut changed = false;
while let Some(ch) = chars.next() {
match ch {
' ' => {
reindented_line.push(' ');
col += 1;
}
'\t' => {
// \t are converted to spaces depending on the current column
let spaces_len = tab_size - (col % tab_size);
reindented_line.extend(&space_cache[spaces_len - 1]);
col += spaces_len;
changed = true;
}
_ => {
// If we dont append before break, the character is consumed
reindented_line.push(ch);
break;
}
}
}
if !changed {
reindented_line.clear();
continue;
}
// Append the rest of the line and replace old reference with new one
reindented_line.extend(chars);
*line = Cow::Owned(reindented_line.clone());
reindented_line.clear();
}
});
}
pub fn convert_indentation_to_tabs(
&mut self,
_: &ConvertIndentationToTabs,
window: &mut Window,
cx: &mut Context<Self>,
) {
let settings = self.buffer.read(cx).language_settings(cx);
let tab_size = settings.tab_size.get() as usize;
self.manipulate_mutable_lines(window, cx, |lines| {
// Allocates a reasonably sized buffer once for the whole loop
let mut reindented_line = String::with_capacity(MAX_LINE_LEN);
// Avoids recomputing spaces that could be inserted many times
let space_cache: Vec<Vec<char>> = (1..=tab_size)
.map(|n| IndentSize::spaces(n as u32).chars().collect())
.collect();
for line in lines.iter_mut().filter(|line| !line.is_empty()) {
let mut chars = line.chars();
let mut spaces_count = 0;
let mut first_non_indent_char = None;
let mut changed = false;
while let Some(ch) = chars.next() {
match ch {
' ' => {
// Keep track of spaces. Append \t when we reach tab_size
spaces_count += 1;
changed = true;
if spaces_count == tab_size {
reindented_line.push('\t');
spaces_count = 0;
}
}
'\t' => {
reindented_line.push('\t');
spaces_count = 0;
}
_ => {
// Dont append it yet, we might have remaining spaces
first_non_indent_char = Some(ch);
break;
}
}
}
if !changed {
reindented_line.clear();
continue;
}
// Remaining spaces that didn't make a full tab stop
if spaces_count > 0 {
reindented_line.extend(&space_cache[spaces_count - 1]);
}
// If we consume an extra character that was not indentation, add it back
if let Some(extra_char) = first_non_indent_char {
reindented_line.push(extra_char);
}
// Append the rest of the line and replace old reference with new one
reindented_line.extend(chars);
*line = Cow::Owned(reindented_line.clone());
reindented_line.clear();
}
});
}
pub fn convert_to_upper_case(
&mut self,
_: &ConvertToUpperCase,
@@ -21157,6 +21318,13 @@ pub struct LineHighlight {
pub type_id: Option<TypeId>,
}
struct LineManipulationResult {
pub new_text: String,
pub line_count_before: usize,
pub line_count_after: usize,
}
fn render_diff_hunk_controls(
row: u32,
status: &DiffHunkStatus,

View File

@@ -10,7 +10,7 @@ use assistant_tool::{
ToolUseStatus,
};
use buffer_diff::{BufferDiff, BufferDiffSnapshot};
use editor::{Editor, EditorMode, MinimapVisibility, MultiBuffer, PathKey, scroll::Autoscroll};
use editor::{Editor, EditorMode, MinimapVisibility, MultiBuffer, PathKey};
use futures::StreamExt;
use gpui::{
Animation, AnimationExt, AnyWindowHandle, App, AppContext, AsyncApp, Entity, Task,
@@ -823,7 +823,7 @@ impl ToolCard for EditFileToolCard {
let first_hunk_start =
first_hunk.multi_buffer_range().start;
editor.change_selections(
Some(Autoscroll::fit()),
Default::default(),
window,
cx,
|selections| {
@@ -1065,7 +1065,7 @@ fn markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
MarkdownStyle {
base_text_style: text_style.clone(),
selection_background_color: cx.theme().players().local().selection,
selection_background_color: cx.theme().colors().element_selection_background,
..Default::default()
}
}

View File

@@ -691,7 +691,7 @@ fn markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
MarkdownStyle {
base_text_style: text_style.clone(),
selection_background_color: cx.theme().players().local().selection,
selection_background_color: cx.theme().colors().element_selection_background,
..Default::default()
}
}

View File

@@ -1,7 +1,7 @@
use auto_update::AutoUpdater;
use client::proto::UpdateNotification;
use editor::{Editor, MultiBuffer};
use gpui::{App, Context, DismissEvent, Entity, SharedString, Window, actions, prelude::*};
use gpui::{App, Context, DismissEvent, Entity, Window, actions, prelude::*};
use http_client::HttpClient;
use markdown_preview::markdown_preview_view::{MarkdownPreviewMode, MarkdownPreviewView};
use release_channel::{AppVersion, ReleaseChannel};
@@ -94,7 +94,6 @@ fn view_release_notes_locally(
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
let tab_content = Some(SharedString::from(body.title.to_string()));
let editor = cx.new(|cx| {
Editor::for_multibuffer(buffer, Some(project), window, cx)
});
@@ -105,7 +104,6 @@ fn view_release_notes_locally(
editor,
workspace_handle,
language_registry,
tab_content,
window,
cx,
);
@@ -132,6 +130,11 @@ pub fn notify_if_app_was_updated(cx: &mut App) {
let Some(updater) = AutoUpdater::get(cx) else {
return;
};
if let ReleaseChannel::Nightly = ReleaseChannel::global(cx) {
return;
}
let should_show_notification = updater.read(cx).should_show_update_notification(cx);
cx.spawn(async move |cx| {
let should_show_notification = should_show_notification.await?;

View File

@@ -11,6 +11,13 @@ pub enum BedrockModelMode {
},
}
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
pub struct BedrockModelCacheConfiguration {
pub max_cache_anchors: usize,
pub min_total_token: u64,
}
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, EnumIter)]
pub enum Model {
@@ -104,6 +111,7 @@ pub enum Model {
display_name: Option<String>,
max_output_tokens: Option<u64>,
default_temperature: Option<f32>,
cache_configuration: Option<BedrockModelCacheConfiguration>,
},
}
@@ -401,6 +409,56 @@ impl Model {
}
}
pub fn supports_caching(&self) -> bool {
match self {
// Only Claude models on Bedrock support caching
// Nova models support only text caching
// https://docs.aws.amazon.com/bedrock/latest/userguide/prompt-caching.html#prompt-caching-models
Self::Claude3_5Haiku
| Self::Claude3_7Sonnet
| Self::Claude3_7SonnetThinking
| Self::ClaudeSonnet4
| Self::ClaudeSonnet4Thinking
| Self::ClaudeOpus4
| Self::ClaudeOpus4Thinking => true,
// Custom models - check if they have cache configuration
Self::Custom {
cache_configuration,
..
} => cache_configuration.is_some(),
// All other models don't support caching
_ => false,
}
}
pub fn cache_configuration(&self) -> Option<BedrockModelCacheConfiguration> {
match self {
Self::Claude3_7Sonnet
| Self::Claude3_7SonnetThinking
| Self::ClaudeSonnet4
| Self::ClaudeSonnet4Thinking
| Self::ClaudeOpus4
| Self::ClaudeOpus4Thinking => Some(BedrockModelCacheConfiguration {
max_cache_anchors: 4,
min_total_token: 1024,
}),
Self::Claude3_5Haiku => Some(BedrockModelCacheConfiguration {
max_cache_anchors: 4,
min_total_token: 2048,
}),
Self::Custom {
cache_configuration,
..
} => cache_configuration.clone(),
_ => None,
}
}
pub fn mode(&self) -> BedrockModelMode {
match self {
Model::Claude3_7SonnetThinking => BedrockModelMode::Thinking {
@@ -660,6 +718,7 @@ mod tests {
display_name: Some("My Custom Model".to_string()),
max_output_tokens: Some(8192),
default_temperature: Some(0.7),
cache_configuration: None,
};
// Custom model should return its name unchanged

View File

@@ -1867,7 +1867,7 @@ mod tests {
let hunk = diff.hunks(&buffer, cx).next().unwrap();
let new_index_text = diff
.stage_or_unstage_hunks(true, &[hunk.clone()], &buffer, true, cx)
.stage_or_unstage_hunks(true, std::slice::from_ref(&hunk), &buffer, true, cx)
.unwrap()
.to_string();
assert_eq!(new_index_text, buffer_text);

View File

@@ -734,8 +734,8 @@ impl Database {
users.push(proto::User {
id: user.id.to_proto(),
avatar_url: format!(
"https://github.com/{}.png?size=128",
user.github_login
"https://avatars.githubusercontent.com/u/{}?s=128&v=4",
user.github_user_id
),
github_login: user.github_login,
name: user.name,

View File

@@ -76,7 +76,10 @@ async fn test_purge_old_embeddings(cx: &mut gpui::TestAppContext) {
db.purge_old_embeddings().await.unwrap();
// Try to retrieve the purged embeddings
let retrieved_embeddings = db.get_embeddings(model, &[digest.clone()]).await.unwrap();
let retrieved_embeddings = db
.get_embeddings(model, std::slice::from_ref(&digest))
.await
.unwrap();
assert!(
retrieved_embeddings.is_empty(),
"Old embeddings should have been purged"

View File

@@ -179,7 +179,7 @@ struct Session {
}
impl Session {
async fn db(&self) -> tokio::sync::MutexGuard<DbHandle> {
async fn db(&self) -> tokio::sync::MutexGuard<'_, DbHandle> {
#[cfg(test)]
tokio::task::yield_now().await;
let guard = self.db.lock().await;
@@ -1037,7 +1037,7 @@ impl Server {
}
}
pub async fn snapshot(self: &Arc<Self>) -> ServerSnapshot {
pub async fn snapshot(self: &Arc<Self>) -> ServerSnapshot<'_> {
ServerSnapshot {
connection_pool: ConnectionPoolGuard {
guard: self.connection_pool.lock(),
@@ -2008,6 +2008,7 @@ async fn join_project(
session.connection_id,
proto::UpdateLanguageServer {
project_id: project_id.to_proto(),
server_name: Some(language_server.name.clone()),
language_server_id: language_server.id,
variant: Some(
proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated(

View File

@@ -16,9 +16,9 @@ use crate::stripe_client::{
StripeCreateCheckoutSessionLineItems, StripeCreateCheckoutSessionParams,
StripeCreateCheckoutSessionSubscriptionData, StripeCreateMeterEventParams,
StripeCreateMeterEventPayload, StripeCreateSubscriptionItems, StripeCreateSubscriptionParams,
StripeCustomerId, StripeMeter, StripePrice, StripePriceId, StripeSubscription,
StripeSubscriptionId, StripeSubscriptionTrialSettings,
StripeSubscriptionTrialSettingsEndBehavior,
StripeCustomerId, StripeCustomerUpdate, StripeCustomerUpdateAddress, StripeCustomerUpdateName,
StripeMeter, StripePrice, StripePriceId, StripeSubscription, StripeSubscriptionId,
StripeSubscriptionTrialSettings, StripeSubscriptionTrialSettingsEndBehavior,
StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod, UpdateSubscriptionItems,
UpdateSubscriptionParams,
};
@@ -247,6 +247,11 @@ impl StripeBilling {
}]);
params.success_url = Some(success_url);
params.billing_address_collection = Some(StripeBillingAddressCollection::Required);
params.customer_update = Some(StripeCustomerUpdate {
address: Some(StripeCustomerUpdateAddress::Auto),
name: Some(StripeCustomerUpdateName::Auto),
shipping: None,
});
let session = self.client.create_checkout_session(params).await?;
Ok(session.url.context("no checkout session URL")?)
@@ -301,6 +306,11 @@ impl StripeBilling {
}]);
params.success_url = Some(success_url);
params.billing_address_collection = Some(StripeBillingAddressCollection::Required);
params.customer_update = Some(StripeCustomerUpdate {
address: Some(StripeCustomerUpdateAddress::Auto),
name: Some(StripeCustomerUpdateName::Auto),
shipping: None,
});
let session = self.client.create_checkout_session(params).await?;
Ok(session.url.context("no checkout session URL")?)

View File

@@ -154,6 +154,31 @@ pub enum StripeBillingAddressCollection {
Required,
}
#[derive(Debug, PartialEq, Clone)]
pub struct StripeCustomerUpdate {
pub address: Option<StripeCustomerUpdateAddress>,
pub name: Option<StripeCustomerUpdateName>,
pub shipping: Option<StripeCustomerUpdateShipping>,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum StripeCustomerUpdateAddress {
Auto,
Never,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum StripeCustomerUpdateName {
Auto,
Never,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum StripeCustomerUpdateShipping {
Auto,
Never,
}
#[derive(Debug, Default)]
pub struct StripeCreateCheckoutSessionParams<'a> {
pub customer: Option<&'a StripeCustomerId>,
@@ -164,6 +189,7 @@ pub struct StripeCreateCheckoutSessionParams<'a> {
pub subscription_data: Option<StripeCreateCheckoutSessionSubscriptionData>,
pub success_url: Option<&'a str>,
pub billing_address_collection: Option<StripeBillingAddressCollection>,
pub customer_update: Option<StripeCustomerUpdate>,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]

View File

@@ -12,9 +12,10 @@ use crate::stripe_client::{
StripeCheckoutSessionMode, StripeCheckoutSessionPaymentMethodCollection, StripeClient,
StripeCreateCheckoutSessionLineItems, StripeCreateCheckoutSessionParams,
StripeCreateCheckoutSessionSubscriptionData, StripeCreateMeterEventParams,
StripeCreateSubscriptionParams, StripeCustomer, StripeCustomerId, StripeMeter, StripeMeterId,
StripePrice, StripePriceId, StripeSubscription, StripeSubscriptionId, StripeSubscriptionItem,
StripeSubscriptionItemId, UpdateCustomerParams, UpdateSubscriptionParams,
StripeCreateSubscriptionParams, StripeCustomer, StripeCustomerId, StripeCustomerUpdate,
StripeMeter, StripeMeterId, StripePrice, StripePriceId, StripeSubscription,
StripeSubscriptionId, StripeSubscriptionItem, StripeSubscriptionItemId, UpdateCustomerParams,
UpdateSubscriptionParams,
};
#[derive(Debug, Clone)]
@@ -36,6 +37,7 @@ pub struct StripeCreateCheckoutSessionCall {
pub subscription_data: Option<StripeCreateCheckoutSessionSubscriptionData>,
pub success_url: Option<String>,
pub billing_address_collection: Option<StripeBillingAddressCollection>,
pub customer_update: Option<StripeCustomerUpdate>,
}
pub struct FakeStripeClient {
@@ -233,6 +235,7 @@ impl StripeClient for FakeStripeClient {
subscription_data: params.subscription_data,
success_url: params.success_url.map(|url| url.to_string()),
billing_address_collection: params.billing_address_collection,
customer_update: params.customer_update,
});
Ok(StripeCheckoutSession {

View File

@@ -22,10 +22,11 @@ use crate::stripe_client::{
StripeCheckoutSessionPaymentMethodCollection, StripeClient,
StripeCreateCheckoutSessionLineItems, StripeCreateCheckoutSessionParams,
StripeCreateCheckoutSessionSubscriptionData, StripeCreateMeterEventParams,
StripeCreateSubscriptionParams, StripeCustomer, StripeCustomerId, StripeMeter, StripePrice,
StripePriceId, StripePriceRecurring, StripeSubscription, StripeSubscriptionId,
StripeSubscriptionItem, StripeSubscriptionItemId, StripeSubscriptionTrialSettings,
StripeSubscriptionTrialSettingsEndBehavior,
StripeCreateSubscriptionParams, StripeCustomer, StripeCustomerId, StripeCustomerUpdate,
StripeCustomerUpdateAddress, StripeCustomerUpdateName, StripeCustomerUpdateShipping,
StripeMeter, StripePrice, StripePriceId, StripePriceRecurring, StripeSubscription,
StripeSubscriptionId, StripeSubscriptionItem, StripeSubscriptionItemId,
StripeSubscriptionTrialSettings, StripeSubscriptionTrialSettingsEndBehavior,
StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod, UpdateCustomerParams,
UpdateSubscriptionParams,
};
@@ -446,6 +447,7 @@ impl<'a> TryFrom<StripeCreateCheckoutSessionParams<'a>> for CreateCheckoutSessio
subscription_data: value.subscription_data.map(Into::into),
success_url: value.success_url,
billing_address_collection: value.billing_address_collection.map(Into::into),
customer_update: value.customer_update.map(Into::into),
..Default::default()
})
}
@@ -541,3 +543,50 @@ impl From<StripeBillingAddressCollection> for stripe::CheckoutSessionBillingAddr
}
}
}
impl From<StripeCustomerUpdateAddress> for stripe::CreateCheckoutSessionCustomerUpdateAddress {
fn from(value: StripeCustomerUpdateAddress) -> Self {
match value {
StripeCustomerUpdateAddress::Auto => {
stripe::CreateCheckoutSessionCustomerUpdateAddress::Auto
}
StripeCustomerUpdateAddress::Never => {
stripe::CreateCheckoutSessionCustomerUpdateAddress::Never
}
}
}
}
impl From<StripeCustomerUpdateName> for stripe::CreateCheckoutSessionCustomerUpdateName {
fn from(value: StripeCustomerUpdateName) -> Self {
match value {
StripeCustomerUpdateName::Auto => stripe::CreateCheckoutSessionCustomerUpdateName::Auto,
StripeCustomerUpdateName::Never => {
stripe::CreateCheckoutSessionCustomerUpdateName::Never
}
}
}
}
impl From<StripeCustomerUpdateShipping> for stripe::CreateCheckoutSessionCustomerUpdateShipping {
fn from(value: StripeCustomerUpdateShipping) -> Self {
match value {
StripeCustomerUpdateShipping::Auto => {
stripe::CreateCheckoutSessionCustomerUpdateShipping::Auto
}
StripeCustomerUpdateShipping::Never => {
stripe::CreateCheckoutSessionCustomerUpdateShipping::Never
}
}
}
}
impl From<StripeCustomerUpdate> for stripe::CreateCheckoutSessionCustomerUpdate {
fn from(value: StripeCustomerUpdate) -> Self {
stripe::CreateCheckoutSessionCustomerUpdate {
address: value.address.map(Into::into),
name: value.name.map(Into::into),
shipping: value.shipping.map(Into::into),
}
}
}

View File

@@ -178,7 +178,7 @@ async fn test_channel_notes_participant_indices(
channel_view_a.update_in(cx_a, |notes, window, cx| {
notes.editor.update(cx, |editor, cx| {
editor.insert("a", window, cx);
editor.change_selections(None, window, cx, |selections| {
editor.change_selections(Default::default(), window, cx, |selections| {
selections.select_ranges(vec![0..1]);
});
});
@@ -188,7 +188,7 @@ async fn test_channel_notes_participant_indices(
notes.editor.update(cx, |editor, cx| {
editor.move_down(&Default::default(), window, cx);
editor.insert("b", window, cx);
editor.change_selections(None, window, cx, |selections| {
editor.change_selections(Default::default(), window, cx, |selections| {
selections.select_ranges(vec![1..2]);
});
});
@@ -198,7 +198,7 @@ async fn test_channel_notes_participant_indices(
notes.editor.update(cx, |editor, cx| {
editor.move_down(&Default::default(), window, cx);
editor.insert("c", window, cx);
editor.change_selections(None, window, cx, |selections| {
editor.change_selections(Default::default(), window, cx, |selections| {
selections.select_ranges(vec![2..3]);
});
});
@@ -273,12 +273,12 @@ async fn test_channel_notes_participant_indices(
.unwrap();
editor_a.update_in(cx_a, |editor, window, cx| {
editor.change_selections(None, window, cx, |selections| {
editor.change_selections(Default::default(), window, cx, |selections| {
selections.select_ranges(vec![0..1]);
});
});
editor_b.update_in(cx_b, |editor, window, cx| {
editor.change_selections(None, window, cx, |selections| {
editor.change_selections(Default::default(), window, cx, |selections| {
selections.select_ranges(vec![2..3]);
});
});

View File

@@ -4,7 +4,7 @@ use crate::{
};
use call::ActiveCall;
use editor::{
DocumentColorsRenderMode, Editor, EditorSettings, RowInfo,
DocumentColorsRenderMode, Editor, EditorSettings, RowInfo, SelectionEffects,
actions::{
ConfirmCodeAction, ConfirmCompletion, ConfirmRename, ContextMenuFirst,
ExpandMacroRecursively, MoveToEnd, Redo, Rename, SelectAll, ToggleCodeActions, Undo,
@@ -348,7 +348,9 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
// Type a completion trigger character as the guest.
editor_b.update_in(cx_b, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([13..13])
});
editor.handle_input(".", window, cx);
});
cx_b.focus(&editor_b);
@@ -461,7 +463,9 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
// Now we do a second completion, this time to ensure that documentation/snippets are
// resolved
editor_b.update_in(cx_b, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| s.select_ranges([46..46]));
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([46..46])
});
editor.handle_input("; a", window, cx);
editor.handle_input(".", window, cx);
});
@@ -613,7 +617,7 @@ async fn test_collaborating_with_code_actions(
// Move cursor to a location that contains code actions.
editor_b.update_in(cx_b, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| {
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([Point::new(1, 31)..Point::new(1, 31)])
});
});
@@ -817,7 +821,9 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
// Move cursor to a location that can be renamed.
let prepare_rename = editor_b.update_in(cx_b, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| s.select_ranges([7..7]));
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([7..7])
});
editor.rename(&Rename, window, cx).unwrap()
});
@@ -863,7 +869,9 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
editor.cancel(&editor::actions::Cancel, window, cx);
});
let prepare_rename = editor_b.update_in(cx_b, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| s.select_ranges([7..8]));
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([7..8])
});
editor.rename(&Rename, window, cx).unwrap()
});
@@ -1364,7 +1372,9 @@ async fn test_on_input_format_from_host_to_guest(
// Type a on type formatting trigger character as the guest.
cx_a.focus(&editor_a);
editor_a.update_in(cx_a, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([13..13])
});
editor.handle_input(">", window, cx);
});
@@ -1460,7 +1470,9 @@ async fn test_on_input_format_from_guest_to_host(
// Type a on type formatting trigger character as the guest.
cx_b.focus(&editor_b);
editor_b.update_in(cx_b, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([13..13])
});
editor.handle_input(":", window, cx);
});
@@ -1697,7 +1709,9 @@ async fn test_mutual_editor_inlay_hint_cache_update(
let after_client_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
editor_b.update_in(cx_b, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| s.select_ranges([13..13].clone()));
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([13..13].clone())
});
editor.handle_input(":", window, cx);
});
cx_b.focus(&editor_b);
@@ -1718,7 +1732,9 @@ async fn test_mutual_editor_inlay_hint_cache_update(
let after_host_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
editor_a.update_in(cx_a, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([13..13])
});
editor.handle_input("a change to increment both buffers' versions", window, cx);
});
cx_a.focus(&editor_a);
@@ -2121,7 +2137,9 @@ async fn test_lsp_document_color(cx_a: &mut TestAppContext, cx_b: &mut TestAppCo
});
editor_a.update_in(cx_a, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| s.select_ranges([13..13].clone()));
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([13..13].clone())
});
editor.handle_input(":", window, cx);
});
color_request_handle.next().await.unwrap();

View File

@@ -6,7 +6,7 @@ use collab_ui::{
channel_view::ChannelView,
notifications::project_shared_notification::ProjectSharedNotification,
};
use editor::{Editor, MultiBuffer, PathKey};
use editor::{Editor, MultiBuffer, PathKey, SelectionEffects};
use gpui::{
AppContext as _, BackgroundExecutor, BorrowAppContext, Entity, SharedString, TestAppContext,
VisualContext, VisualTestContext, point,
@@ -376,7 +376,9 @@ async fn test_basic_following(
// Changes to client A's editor are reflected on client B.
editor_a1.update_in(cx_a, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| s.select_ranges([1..1, 2..2]));
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([1..1, 2..2])
});
});
executor.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
executor.run_until_parked();
@@ -393,7 +395,9 @@ async fn test_basic_following(
editor_b1.update(cx_b, |editor, cx| assert_eq!(editor.text(cx), "TWO"));
editor_a1.update_in(cx_a, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| s.select_ranges([3..3]));
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([3..3])
});
editor.set_scroll_position(point(0., 100.), window, cx);
});
executor.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
@@ -1647,7 +1651,9 @@ async fn test_following_stops_on_unshare(cx_a: &mut TestAppContext, cx_b: &mut T
// b should follow a to position 1
editor_a.update_in(cx_a, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| s.select_ranges([1..1]))
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([1..1])
})
});
cx_a.executor()
.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
@@ -1667,7 +1673,9 @@ async fn test_following_stops_on_unshare(cx_a: &mut TestAppContext, cx_b: &mut T
// b should not follow a to position 2
editor_a.update_in(cx_a, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| s.select_ranges([2..2]))
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([2..2])
})
});
cx_a.executor()
.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
@@ -1968,7 +1976,7 @@ async fn test_following_to_channel_notes_without_a_shared_project(
assert_eq!(notes.channel(cx).unwrap().name, "channel-1");
notes.editor.update(cx, |editor, cx| {
editor.insert("Hello from A.", window, cx);
editor.change_selections(None, window, cx, |selections| {
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
selections.select_ranges(vec![3..4]);
});
});
@@ -2109,7 +2117,7 @@ async fn test_following_after_replacement(cx_a: &mut TestAppContext, cx_b: &mut
workspace.add_item_to_center(Box::new(editor.clone()) as _, window, cx)
});
editor.update_in(cx_a, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| {
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([Point::row_range(4..4)]);
})
});

View File

@@ -8,8 +8,9 @@ use crate::stripe_billing::StripeBilling;
use crate::stripe_client::{
FakeStripeClient, StripeBillingAddressCollection, StripeCheckoutSessionMode,
StripeCheckoutSessionPaymentMethodCollection, StripeCreateCheckoutSessionLineItems,
StripeCreateCheckoutSessionSubscriptionData, StripeCustomerId, StripeMeter, StripeMeterId,
StripePrice, StripePriceId, StripePriceRecurring, StripeSubscription, StripeSubscriptionId,
StripeCreateCheckoutSessionSubscriptionData, StripeCustomerId, StripeCustomerUpdate,
StripeCustomerUpdateAddress, StripeCustomerUpdateName, StripeMeter, StripeMeterId, StripePrice,
StripePriceId, StripePriceRecurring, StripeSubscription, StripeSubscriptionId,
StripeSubscriptionItem, StripeSubscriptionItemId, StripeSubscriptionTrialSettings,
StripeSubscriptionTrialSettingsEndBehavior,
StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod, UpdateSubscriptionItems,
@@ -431,6 +432,14 @@ async fn test_checkout_with_zed_pro() {
call.billing_address_collection,
Some(StripeBillingAddressCollection::Required)
);
assert_eq!(
call.customer_update,
Some(StripeCustomerUpdate {
address: Some(StripeCustomerUpdateAddress::Auto),
name: Some(StripeCustomerUpdateName::Auto),
shipping: None,
})
);
}
}
@@ -516,6 +525,14 @@ async fn test_checkout_with_zed_pro_trial() {
call.billing_address_collection,
Some(StripeBillingAddressCollection::Required)
);
assert_eq!(
call.customer_update,
Some(StripeCustomerUpdate {
address: Some(StripeCustomerUpdateAddress::Auto),
name: Some(StripeCustomerUpdateName::Auto),
shipping: None,
})
);
}
// Successful checkout with extended trial.
@@ -574,5 +591,13 @@ async fn test_checkout_with_zed_pro_trial() {
call.billing_address_collection,
Some(StripeBillingAddressCollection::Required)
);
assert_eq!(
call.customer_update,
Some(StripeCustomerUpdate {
address: Some(StripeCustomerUpdateAddress::Auto),
name: Some(StripeCustomerUpdateName::Auto),
shipping: None,
})
);
}
}

View File

@@ -7,8 +7,8 @@ use client::{
};
use collections::HashMap;
use editor::{
CollaborationHub, DisplayPoint, Editor, EditorEvent, display_map::ToDisplayPoint,
scroll::Autoscroll,
CollaborationHub, DisplayPoint, Editor, EditorEvent, SelectionEffects,
display_map::ToDisplayPoint, scroll::Autoscroll,
};
use gpui::{
AnyView, App, ClipboardItem, Context, Entity, EventEmitter, Focusable, Pixels, Point, Render,
@@ -260,9 +260,16 @@ impl ChannelView {
.find(|item| &Channel::slug(&item.text).to_lowercase() == &position)
{
self.editor.update(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::focused()), window, cx, |s| {
s.replace_cursors_with(|map| vec![item.range.start.to_display_point(map)])
})
editor.change_selections(
SelectionEffects::scroll(Autoscroll::focused()),
window,
cx,
|s| {
s.replace_cursors_with(|map| {
vec![item.range.start.to_display_point(map)]
})
},
)
});
return;
}

View File

@@ -41,7 +41,7 @@ pub struct CommandPalette {
/// Removes subsequent whitespace characters and double colons from the query.
///
/// This improves the likelihood of a match by either humanized name or keymap-style name.
pub fn normalize_action_query(input: &str) -> String {
fn normalize_query(input: &str) -> String {
let mut result = String::with_capacity(input.len());
let mut last_char = None;
@@ -297,7 +297,7 @@ impl PickerDelegate for CommandPaletteDelegate {
let mut commands = self.all_commands.clone();
let hit_counts = self.hit_counts();
let executor = cx.background_executor().clone();
let query = normalize_action_query(query.as_str());
let query = normalize_query(query.as_str());
async move {
commands.sort_by_key(|action| {
(
@@ -311,17 +311,29 @@ impl PickerDelegate for CommandPaletteDelegate {
.enumerate()
.map(|(ix, command)| StringMatchCandidate::new(ix, &command.name))
.collect::<Vec<_>>();
let matches = fuzzy::match_strings(
&candidates,
&query,
true,
true,
10000,
&Default::default(),
executor,
)
.await;
let matches = if query.is_empty() {
candidates
.into_iter()
.enumerate()
.map(|(index, candidate)| StringMatch {
candidate_id: index,
string: candidate.string,
positions: Vec::new(),
score: 0.0,
})
.collect()
} else {
fuzzy::match_strings(
&candidates,
&query,
true,
true,
10000,
&Default::default(),
executor,
)
.await
};
tx.send((commands, matches)).await.log_err();
}
@@ -410,8 +422,8 @@ impl PickerDelegate for CommandPaletteDelegate {
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
let matching_command = self.matches.get(ix)?;
let command = self.commands.get(matching_command.candidate_id)?;
let r#match = self.matches.get(ix)?;
let command = self.commands.get(r#match.candidate_id)?;
Some(
ListItem::new(ix)
.inset(true)
@@ -424,7 +436,7 @@ impl PickerDelegate for CommandPaletteDelegate {
.justify_between()
.child(HighlightedLabel::new(
command.name.clone(),
matching_command.positions.clone(),
r#match.positions.clone(),
))
.children(KeyBinding::for_action_in(
&*command.action,
@@ -500,28 +512,19 @@ mod tests {
#[test]
fn test_normalize_query() {
assert_eq!(normalize_query("editor: backspace"), "editor: backspace");
assert_eq!(normalize_query("editor: backspace"), "editor: backspace");
assert_eq!(normalize_query("editor: backspace"), "editor: backspace");
assert_eq!(
normalize_action_query("editor: backspace"),
"editor: backspace"
);
assert_eq!(
normalize_action_query("editor: backspace"),
"editor: backspace"
);
assert_eq!(
normalize_action_query("editor: backspace"),
"editor: backspace"
);
assert_eq!(
normalize_action_query("editor::GoToDefinition"),
normalize_query("editor::GoToDefinition"),
"editor:GoToDefinition"
);
assert_eq!(
normalize_action_query("editor::::GoToDefinition"),
normalize_query("editor::::GoToDefinition"),
"editor:GoToDefinition"
);
assert_eq!(
normalize_action_query("editor: :GoToDefinition"),
normalize_query("editor: :GoToDefinition"),
"editor: :GoToDefinition"
);
}

View File

@@ -29,6 +29,7 @@ impl Display for ContextServerId {
#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema)]
pub struct ContextServerCommand {
#[serde(rename = "command")]
pub path: String,
pub args: Vec<String>,
pub env: Option<HashMap<String, String>>,

View File

@@ -698,16 +698,16 @@ async fn stream_completion(
completion_url: Arc<str>,
request: Request,
) -> Result<BoxStream<'static, Result<ResponseEvent>>> {
let is_vision_request = request.messages.last().map_or(false, |message| match message {
ChatMessage::User { content }
| ChatMessage::Assistant { content, .. }
| ChatMessage::Tool { content, .. } => {
matches!(content, ChatMessageContent::Multipart(parts) if parts.iter().any(|part| matches!(part, ChatMessagePart::Image { .. })))
}
_ => false,
});
let is_vision_request = request.messages.iter().any(|message| match message {
ChatMessage::User { content }
| ChatMessage::Assistant { content, .. }
| ChatMessage::Tool { content, .. } => {
matches!(content, ChatMessageContent::Multipart(parts) if parts.iter().any(|part| matches!(part, ChatMessagePart::Image { .. })))
}
_ => false,
});
let request_builder = HttpRequest::builder()
let mut request_builder = HttpRequest::builder()
.method(Method::POST)
.uri(completion_url.as_ref())
.header(
@@ -719,8 +719,12 @@ async fn stream_completion(
)
.header("Authorization", format!("Bearer {}", api_key))
.header("Content-Type", "application/json")
.header("Copilot-Integration-Id", "vscode-chat")
.header("Copilot-Vision-Request", is_vision_request.to_string());
.header("Copilot-Integration-Id", "vscode-chat");
if is_vision_request {
request_builder =
request_builder.header("Copilot-Vision-Request", is_vision_request.to_string());
}
let is_streaming = request.stream;

View File

@@ -264,7 +264,8 @@ fn common_prefix<T1: Iterator<Item = char>, T2: Iterator<Item = char>>(a: T1, b:
mod tests {
use super::*;
use editor::{
Editor, ExcerptRange, MultiBuffer, test::editor_lsp_test_context::EditorLspTestContext,
Editor, ExcerptRange, MultiBuffer, SelectionEffects,
test::editor_lsp_test_context::EditorLspTestContext,
};
use fs::FakeFs;
use futures::StreamExt;
@@ -478,7 +479,7 @@ mod tests {
// Reset the editor to verify how suggestions behave when tabbing on leading indentation.
cx.update_editor(|editor, window, cx| {
editor.set_text("fn foo() {\n \n}", window, cx);
editor.change_selections(None, window, cx, |s| {
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([Point::new(1, 2)..Point::new(1, 2)])
});
});
@@ -767,7 +768,7 @@ mod tests {
);
_ = editor.update(cx, |editor, window, cx| {
// Ensure copilot suggestions are shown for the first excerpt.
editor.change_selections(None, window, cx, |s| {
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([Point::new(1, 5)..Point::new(1, 5)])
});
editor.next_edit_prediction(&Default::default(), window, cx);
@@ -793,7 +794,7 @@ mod tests {
);
_ = editor.update(cx, |editor, window, cx| {
// Move to another excerpt, ensuring the suggestion gets cleared.
editor.change_selections(None, window, cx, |s| {
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
});
assert!(!editor.has_active_inline_completion());
@@ -1019,7 +1020,7 @@ mod tests {
);
_ = editor.update(cx, |editor, window, cx| {
editor.change_selections(None, window, cx, |selections| {
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
selections.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
});
editor.refresh_inline_completion(true, false, window, cx);
@@ -1029,7 +1030,7 @@ mod tests {
assert!(copilot_requests.try_next().is_err());
_ = editor.update(cx, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| {
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([Point::new(5, 0)..Point::new(5, 0)])
});
editor.refresh_inline_completion(true, false, window, cx);

View File

@@ -1,5 +1,3 @@
use std::collections::{HashMap, HashSet};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum VariableLookupKind {
Variable,
@@ -20,641 +18,3 @@ pub struct InlineValueLocation {
pub row: usize,
pub column: usize,
}
/// A trait for providing inline values for debugging purposes.
///
/// Implementors of this trait are responsible for analyzing a given node in the
/// source code and extracting variable information, including their names,
/// scopes, and positions. This information is used to display inline values
/// during debugging sessions. Implementors must also handle variable scoping
/// themselves by traversing the syntax tree upwards to determine whether a
/// variable is local or global.
pub trait InlineValueProvider: 'static + Send + Sync {
/// Provides a list of inline value locations based on the given node and source code.
///
/// # Parameters
/// - `node`: The root node of the active debug line. Implementors should traverse
/// upwards from this node to gather variable information and determine their scope.
/// - `source`: The source code as a string slice, used to extract variable names.
/// - `max_row`: The maximum row to consider when collecting variables. Variables
/// declared beyond this row should be ignored.
///
/// # Returns
/// A vector of `InlineValueLocation` instances, each representing a variable's
/// name, scope, and the position of the inline value should be shown.
fn provide(
&self,
node: language::Node,
source: &str,
max_row: usize,
) -> Vec<InlineValueLocation>;
}
pub struct RustInlineValueProvider;
impl InlineValueProvider for RustInlineValueProvider {
fn provide(
&self,
mut node: language::Node,
source: &str,
max_row: usize,
) -> Vec<InlineValueLocation> {
let mut variables = Vec::new();
let mut variable_names = HashSet::new();
let mut scope = VariableScope::Local;
loop {
let mut variable_names_in_scope = HashMap::new();
for child in node.named_children(&mut node.walk()) {
if child.start_position().row >= max_row {
break;
}
if scope == VariableScope::Local && child.kind() == "let_declaration" {
if let Some(identifier) = child.child_by_field_name("pattern") {
let variable_name = source[identifier.byte_range()].to_string();
if variable_names.contains(&variable_name) {
continue;
}
if let Some(index) = variable_names_in_scope.get(&variable_name) {
variables.remove(*index);
}
variable_names_in_scope.insert(variable_name.clone(), variables.len());
variables.push(InlineValueLocation {
variable_name,
scope: VariableScope::Local,
lookup: VariableLookupKind::Variable,
row: identifier.end_position().row,
column: identifier.end_position().column,
});
}
} else if child.kind() == "static_item" {
if let Some(name) = child.child_by_field_name("name") {
let variable_name = source[name.byte_range()].to_string();
variables.push(InlineValueLocation {
variable_name,
scope: scope.clone(),
lookup: VariableLookupKind::Expression,
row: name.end_position().row,
column: name.end_position().column,
});
}
}
}
variable_names.extend(variable_names_in_scope.keys().cloned());
if matches!(node.kind(), "function_item" | "closure_expression") {
scope = VariableScope::Global;
}
if let Some(parent) = node.parent() {
node = parent;
} else {
break;
}
}
variables
}
}
pub struct PythonInlineValueProvider;
impl InlineValueProvider for PythonInlineValueProvider {
fn provide(
&self,
mut node: language::Node,
source: &str,
max_row: usize,
) -> Vec<InlineValueLocation> {
let mut variables = Vec::new();
let mut variable_names = HashSet::new();
let mut scope = VariableScope::Local;
loop {
let mut variable_names_in_scope = HashMap::new();
for child in node.named_children(&mut node.walk()) {
if child.start_position().row >= max_row {
break;
}
if scope == VariableScope::Local {
match child.kind() {
"expression_statement" => {
if let Some(expr) = child.child(0) {
if expr.kind() == "assignment" {
if let Some(param) = expr.child(0) {
let param_identifier = if param.kind() == "identifier" {
Some(param)
} else if param.kind() == "typed_parameter" {
param.child(0)
} else {
None
};
if let Some(identifier) = param_identifier {
if identifier.kind() == "identifier" {
let variable_name =
source[identifier.byte_range()].to_string();
if variable_names.contains(&variable_name) {
continue;
}
if let Some(index) =
variable_names_in_scope.get(&variable_name)
{
variables.remove(*index);
}
variable_names_in_scope
.insert(variable_name.clone(), variables.len());
variables.push(InlineValueLocation {
variable_name,
scope: VariableScope::Local,
lookup: VariableLookupKind::Variable,
row: identifier.end_position().row,
column: identifier.end_position().column,
});
}
}
}
}
}
}
"function_definition" => {
if let Some(params) = child.child_by_field_name("parameters") {
for param in params.named_children(&mut params.walk()) {
let param_identifier = if param.kind() == "identifier" {
Some(param)
} else if param.kind() == "typed_parameter" {
param.child(0)
} else {
None
};
if let Some(identifier) = param_identifier {
if identifier.kind() == "identifier" {
let variable_name =
source[identifier.byte_range()].to_string();
if variable_names.contains(&variable_name) {
continue;
}
if let Some(index) =
variable_names_in_scope.get(&variable_name)
{
variables.remove(*index);
}
variable_names_in_scope
.insert(variable_name.clone(), variables.len());
variables.push(InlineValueLocation {
variable_name,
scope: VariableScope::Local,
lookup: VariableLookupKind::Variable,
row: identifier.end_position().row,
column: identifier.end_position().column,
});
}
}
}
}
}
"for_statement" => {
if let Some(target) = child.child_by_field_name("left") {
if target.kind() == "identifier" {
let variable_name = source[target.byte_range()].to_string();
if variable_names.contains(&variable_name) {
continue;
}
if let Some(index) = variable_names_in_scope.get(&variable_name)
{
variables.remove(*index);
}
variable_names_in_scope
.insert(variable_name.clone(), variables.len());
variables.push(InlineValueLocation {
variable_name,
scope: VariableScope::Local,
lookup: VariableLookupKind::Variable,
row: target.end_position().row,
column: target.end_position().column,
});
}
}
}
_ => {}
}
}
}
variable_names.extend(variable_names_in_scope.keys().cloned());
if matches!(node.kind(), "function_definition" | "module")
&& node.range().end_point.row < max_row
{
scope = VariableScope::Global;
}
if let Some(parent) = node.parent() {
node = parent;
} else {
break;
}
}
variables
}
}
pub struct GoInlineValueProvider;
impl InlineValueProvider for GoInlineValueProvider {
fn provide(
&self,
mut node: language::Node,
source: &str,
max_row: usize,
) -> Vec<InlineValueLocation> {
let mut variables = Vec::new();
let mut variable_names = HashSet::new();
let mut scope = VariableScope::Local;
loop {
let mut variable_names_in_scope = HashMap::new();
for child in node.named_children(&mut node.walk()) {
if child.start_position().row >= max_row {
break;
}
if scope == VariableScope::Local {
match child.kind() {
"var_declaration" => {
for var_spec in child.named_children(&mut child.walk()) {
if var_spec.kind() == "var_spec" {
if let Some(name_node) = var_spec.child_by_field_name("name") {
let variable_name =
source[name_node.byte_range()].to_string();
if variable_names.contains(&variable_name) {
continue;
}
if let Some(index) =
variable_names_in_scope.get(&variable_name)
{
variables.remove(*index);
}
variable_names_in_scope
.insert(variable_name.clone(), variables.len());
variables.push(InlineValueLocation {
variable_name,
scope: VariableScope::Local,
lookup: VariableLookupKind::Variable,
row: name_node.end_position().row,
column: name_node.end_position().column,
});
}
}
}
}
"short_var_declaration" => {
if let Some(left_side) = child.child_by_field_name("left") {
for identifier in left_side.named_children(&mut left_side.walk()) {
if identifier.kind() == "identifier" {
let variable_name =
source[identifier.byte_range()].to_string();
if variable_names.contains(&variable_name) {
continue;
}
if let Some(index) =
variable_names_in_scope.get(&variable_name)
{
variables.remove(*index);
}
variable_names_in_scope
.insert(variable_name.clone(), variables.len());
variables.push(InlineValueLocation {
variable_name,
scope: VariableScope::Local,
lookup: VariableLookupKind::Variable,
row: identifier.end_position().row,
column: identifier.end_position().column,
});
}
}
}
}
"assignment_statement" => {
if let Some(left_side) = child.child_by_field_name("left") {
for identifier in left_side.named_children(&mut left_side.walk()) {
if identifier.kind() == "identifier" {
let variable_name =
source[identifier.byte_range()].to_string();
if variable_names.contains(&variable_name) {
continue;
}
if let Some(index) =
variable_names_in_scope.get(&variable_name)
{
variables.remove(*index);
}
variable_names_in_scope
.insert(variable_name.clone(), variables.len());
variables.push(InlineValueLocation {
variable_name,
scope: VariableScope::Local,
lookup: VariableLookupKind::Variable,
row: identifier.end_position().row,
column: identifier.end_position().column,
});
}
}
}
}
"function_declaration" | "method_declaration" => {
if let Some(params) = child.child_by_field_name("parameters") {
for param in params.named_children(&mut params.walk()) {
if param.kind() == "parameter_declaration" {
if let Some(name_node) = param.child_by_field_name("name") {
let variable_name =
source[name_node.byte_range()].to_string();
if variable_names.contains(&variable_name) {
continue;
}
if let Some(index) =
variable_names_in_scope.get(&variable_name)
{
variables.remove(*index);
}
variable_names_in_scope
.insert(variable_name.clone(), variables.len());
variables.push(InlineValueLocation {
variable_name,
scope: VariableScope::Local,
lookup: VariableLookupKind::Variable,
row: name_node.end_position().row,
column: name_node.end_position().column,
});
}
}
}
}
}
"for_statement" => {
if let Some(clause) = child.named_child(0) {
if clause.kind() == "for_clause" {
if let Some(init) = clause.named_child(0) {
if init.kind() == "short_var_declaration" {
if let Some(left_side) =
init.child_by_field_name("left")
{
if left_side.kind() == "expression_list" {
for identifier in left_side
.named_children(&mut left_side.walk())
{
if identifier.kind() == "identifier" {
let variable_name = source
[identifier.byte_range()]
.to_string();
if variable_names
.contains(&variable_name)
{
continue;
}
if let Some(index) =
variable_names_in_scope
.get(&variable_name)
{
variables.remove(*index);
}
variable_names_in_scope.insert(
variable_name.clone(),
variables.len(),
);
variables.push(InlineValueLocation {
variable_name,
scope: VariableScope::Local,
lookup:
VariableLookupKind::Variable,
row: identifier.end_position().row,
column: identifier
.end_position()
.column,
});
}
}
}
}
}
}
} else if clause.kind() == "range_clause" {
if let Some(left) = clause.child_by_field_name("left") {
if left.kind() == "expression_list" {
for identifier in left.named_children(&mut left.walk())
{
if identifier.kind() == "identifier" {
let variable_name =
source[identifier.byte_range()].to_string();
if variable_name == "_" {
continue;
}
if variable_names.contains(&variable_name) {
continue;
}
if let Some(index) =
variable_names_in_scope.get(&variable_name)
{
variables.remove(*index);
}
variable_names_in_scope.insert(
variable_name.clone(),
variables.len(),
);
variables.push(InlineValueLocation {
variable_name,
scope: VariableScope::Local,
lookup: VariableLookupKind::Variable,
row: identifier.end_position().row,
column: identifier.end_position().column,
});
}
}
}
}
}
}
}
_ => {}
}
} else if child.kind() == "var_declaration" {
for var_spec in child.named_children(&mut child.walk()) {
if var_spec.kind() == "var_spec" {
if let Some(name_node) = var_spec.child_by_field_name("name") {
let variable_name = source[name_node.byte_range()].to_string();
variables.push(InlineValueLocation {
variable_name,
scope: VariableScope::Global,
lookup: VariableLookupKind::Expression,
row: name_node.end_position().row,
column: name_node.end_position().column,
});
}
}
}
}
}
variable_names.extend(variable_names_in_scope.keys().cloned());
if matches!(node.kind(), "function_declaration" | "method_declaration") {
scope = VariableScope::Global;
}
if let Some(parent) = node.parent() {
node = parent;
} else {
break;
}
}
variables
}
}
#[cfg(test)]
mod tests {
use super::*;
use tree_sitter::Parser;
#[test]
fn test_go_inline_value_provider() {
let provider = GoInlineValueProvider;
let source = r#"
package main
func main() {
items := []int{1, 2, 3, 4, 5}
for i, v := range items {
println(i, v)
}
for j := 0; j < 10; j++ {
println(j)
}
}
"#;
let mut parser = Parser::new();
if parser
.set_language(&tree_sitter_go::LANGUAGE.into())
.is_err()
{
return;
}
let Some(tree) = parser.parse(source, None) else {
return;
};
let root_node = tree.root_node();
let mut main_body = None;
for child in root_node.named_children(&mut root_node.walk()) {
if child.kind() == "function_declaration" {
if let Some(name) = child.child_by_field_name("name") {
if &source[name.byte_range()] == "main" {
if let Some(body) = child.child_by_field_name("body") {
main_body = Some(body);
break;
}
}
}
}
}
let Some(main_body) = main_body else {
return;
};
let variables = provider.provide(main_body, source, 100);
assert!(variables.len() >= 2);
let variable_names: Vec<&str> =
variables.iter().map(|v| v.variable_name.as_str()).collect();
assert!(variable_names.contains(&"items"));
assert!(variable_names.contains(&"j"));
}
#[test]
fn test_go_inline_value_provider_counter_pattern() {
let provider = GoInlineValueProvider;
let source = r#"
package main
func main() {
N := 10
for i := range N {
println(i)
}
}
"#;
let mut parser = Parser::new();
if parser
.set_language(&tree_sitter_go::LANGUAGE.into())
.is_err()
{
return;
}
let Some(tree) = parser.parse(source, None) else {
return;
};
let root_node = tree.root_node();
let mut main_body = None;
for child in root_node.named_children(&mut root_node.walk()) {
if child.kind() == "function_declaration" {
if let Some(name) = child.child_by_field_name("name") {
if &source[name.byte_range()] == "main" {
if let Some(body) = child.child_by_field_name("body") {
main_body = Some(body);
break;
}
}
}
}
}
let Some(main_body) = main_body else {
return;
};
let variables = provider.provide(main_body, source, 100);
let variable_names: Vec<&str> =
variables.iter().map(|v| v.variable_name.as_str()).collect();
assert!(variable_names.contains(&"N"));
assert!(variable_names.contains(&"i"));
}
}

View File

@@ -8,10 +8,7 @@ use task::{
AdapterSchema, AdapterSchemas, DebugRequest, DebugScenario, SpawnInTerminal, TaskTemplate,
};
use crate::{
adapters::{DebugAdapter, DebugAdapterName},
inline_value::InlineValueProvider,
};
use crate::adapters::{DebugAdapter, DebugAdapterName};
use std::{collections::BTreeMap, sync::Arc};
/// Given a user build configuration, locator creates a fill-in debug target ([DebugScenario]) on behalf of the user.
@@ -33,7 +30,6 @@ pub trait DapLocator: Send + Sync {
struct DapRegistryState {
adapters: BTreeMap<DebugAdapterName, Arc<dyn DebugAdapter>>,
locators: FxHashMap<SharedString, Arc<dyn DapLocator>>,
inline_value_providers: FxHashMap<String, Arc<dyn InlineValueProvider>>,
}
#[derive(Clone, Default)]
@@ -82,22 +78,6 @@ impl DapRegistry {
schemas
}
pub fn add_inline_value_provider(
&self,
language: String,
provider: Arc<dyn InlineValueProvider>,
) {
let _previous_value = self
.0
.write()
.inline_value_providers
.insert(language, provider);
debug_assert!(
_previous_value.is_none(),
"Attempted to insert a new inline value provider when one is already registered"
);
}
pub fn locators(&self) -> FxHashMap<SharedString, Arc<dyn DapLocator>> {
self.0.read().locators.clone()
}
@@ -106,10 +86,6 @@ impl DapRegistry {
self.0.read().adapters.get(name).cloned()
}
pub fn inline_value_provider(&self, language: &str) -> Option<Arc<dyn InlineValueProvider>> {
self.0.read().inline_value_providers.get(language).cloned()
}
pub fn enumerate_adapters(&self) -> Vec<DebugAdapterName> {
self.0.read().adapters.keys().cloned().collect()
}

View File

@@ -33,6 +33,7 @@ log.workspace = true
paths.workspace = true
serde.workspace = true
serde_json.workspace = true
shlex.workspace = true
task.workspace = true
util.workspace = true
workspace-hack.workspace = true

View File

@@ -4,7 +4,6 @@ mod go;
mod javascript;
mod php;
mod python;
mod ruby;
use std::sync::Arc;
@@ -18,7 +17,6 @@ use dap::{
GithubRepo,
},
configure_tcp_connection,
inline_value::{GoInlineValueProvider, PythonInlineValueProvider, RustInlineValueProvider},
};
use gdb::GdbDebugAdapter;
use go::GoDebugAdapter;
@@ -26,7 +24,6 @@ use gpui::{App, BorrowAppContext};
use javascript::JsDebugAdapter;
use php::PhpDebugAdapter;
use python::PythonDebugAdapter;
use ruby::RubyDebugAdapter;
use serde_json::json;
use task::{DebugScenario, ZedDebugConfig};
@@ -36,7 +33,6 @@ pub fn init(cx: &mut App) {
registry.add_adapter(Arc::from(PythonDebugAdapter::default()));
registry.add_adapter(Arc::from(PhpDebugAdapter::default()));
registry.add_adapter(Arc::from(JsDebugAdapter::default()));
registry.add_adapter(Arc::from(RubyDebugAdapter));
registry.add_adapter(Arc::from(GoDebugAdapter::default()));
registry.add_adapter(Arc::from(GdbDebugAdapter));
@@ -44,10 +40,5 @@ pub fn init(cx: &mut App) {
{
registry.add_adapter(Arc::from(dap::FakeAdapter {}));
}
registry.add_inline_value_provider("Rust".to_string(), Arc::from(RustInlineValueProvider));
registry
.add_inline_value_provider("Python".to_string(), Arc::from(PythonInlineValueProvider));
registry.add_inline_value_provider("Go".to_string(), Arc::from(GoInlineValueProvider));
})
}

View File

@@ -5,7 +5,7 @@ use gpui::AsyncApp;
use serde_json::Value;
use std::{collections::HashMap, path::PathBuf, sync::OnceLock};
use task::DebugRequest;
use util::ResultExt;
use util::{ResultExt, maybe};
use crate::*;
@@ -72,6 +72,24 @@ impl JsDebugAdapter {
let mut configuration = task_definition.config.clone();
if let Some(configuration) = configuration.as_object_mut() {
maybe!({
configuration
.get("type")
.filter(|value| value == &"node-terminal")?;
let command = configuration.get("command")?.as_str()?.to_owned();
let mut args = shlex::split(&command)?.into_iter();
let program = args.next()?;
configuration.insert("program".to_owned(), program.into());
configuration.insert(
"args".to_owned(),
args.map(Value::from).collect::<Vec<_>>().into(),
);
configuration.insert("console".to_owned(), "externalTerminal".into());
Some(())
});
configuration.entry("type").and_modify(normalize_task_type);
if let Some(program) = configuration
.get("program")
.cloned()
@@ -96,7 +114,6 @@ impl JsDebugAdapter {
.entry("cwd")
.or_insert(delegate.worktree_root_path().to_string_lossy().into());
configuration.entry("type").and_modify(normalize_task_type);
configuration
.entry("console")
.or_insert("externalTerminal".into());
@@ -512,7 +529,7 @@ fn normalize_task_type(task_type: &mut Value) {
};
let new_name = match task_type_str {
"node" | "pwa-node" => "pwa-node",
"node" | "pwa-node" | "node-terminal" => "pwa-node",
"chrome" | "pwa-chrome" => "pwa-chrome",
"edge" | "msedge" | "pwa-edge" | "pwa-msedge" => "pwa-msedge",
_ => task_type_str,

View File

@@ -1,208 +0,0 @@
use anyhow::{Result, bail};
use async_trait::async_trait;
use collections::FxHashMap;
use dap::{
DebugRequest, StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest,
adapters::{
DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName, DebugTaskDefinition,
},
};
use gpui::{AsyncApp, SharedString};
use language::LanguageName;
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::path::PathBuf;
use std::{ffi::OsStr, sync::Arc};
use task::{DebugScenario, ZedDebugConfig};
use util::command::new_smol_command;
#[derive(Default)]
pub(crate) struct RubyDebugAdapter;
impl RubyDebugAdapter {
const ADAPTER_NAME: &'static str = "Ruby";
}
#[derive(Serialize, Deserialize)]
struct RubyDebugConfig {
script_or_command: Option<String>,
script: Option<String>,
command: Option<String>,
#[serde(default)]
args: Vec<String>,
#[serde(default)]
env: FxHashMap<String, String>,
cwd: Option<PathBuf>,
}
#[async_trait(?Send)]
impl DebugAdapter for RubyDebugAdapter {
fn name(&self) -> DebugAdapterName {
DebugAdapterName(Self::ADAPTER_NAME.into())
}
fn adapter_language_name(&self) -> Option<LanguageName> {
Some(SharedString::new_static("Ruby").into())
}
async fn request_kind(
&self,
_: &serde_json::Value,
) -> Result<StartDebuggingRequestArgumentsRequest> {
Ok(StartDebuggingRequestArgumentsRequest::Launch)
}
fn dap_schema(&self) -> serde_json::Value {
json!({
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "Command name (ruby, rake, bin/rails, bundle exec ruby, etc)",
},
"script": {
"type": "string",
"description": "Absolute path to a Ruby file."
},
"cwd": {
"type": "string",
"description": "Directory to execute the program in",
"default": "${ZED_WORKTREE_ROOT}"
},
"args": {
"type": "array",
"description": "Command line arguments passed to the program",
"items": {
"type": "string"
},
"default": []
},
"env": {
"type": "object",
"description": "Additional environment variables to pass to the debugging (and debugged) process",
"default": {}
},
}
})
}
async fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
match zed_scenario.request {
DebugRequest::Launch(launch) => {
let config = RubyDebugConfig {
script_or_command: Some(launch.program),
script: None,
command: None,
args: launch.args,
env: launch.env,
cwd: launch.cwd.clone(),
};
let config = serde_json::to_value(config)?;
Ok(DebugScenario {
adapter: zed_scenario.adapter,
label: zed_scenario.label,
config,
tcp_connection: None,
build: None,
})
}
DebugRequest::Attach(_) => {
anyhow::bail!("Attach requests are unsupported");
}
}
}
async fn get_binary(
&self,
delegate: &Arc<dyn DapDelegate>,
definition: &DebugTaskDefinition,
_user_installed_path: Option<PathBuf>,
_user_args: Option<Vec<String>>,
_cx: &mut AsyncApp,
) -> Result<DebugAdapterBinary> {
let adapter_path = paths::debug_adapters_dir().join(self.name().as_ref());
let mut rdbg_path = adapter_path.join("rdbg");
if !delegate.fs().is_file(&rdbg_path).await {
match delegate.which("rdbg".as_ref()).await {
Some(path) => rdbg_path = path,
None => {
delegate.output_to_console(
"rdbg not found on path, trying `gem install debug`".to_string(),
);
let output = new_smol_command("gem")
.arg("install")
.arg("--no-document")
.arg("--bindir")
.arg(adapter_path)
.arg("debug")
.output()
.await?;
anyhow::ensure!(
output.status.success(),
"Failed to install rdbg:\n{}",
String::from_utf8_lossy(&output.stderr).to_string()
);
}
}
}
let tcp_connection = definition.tcp_connection.clone().unwrap_or_default();
let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
let ruby_config = serde_json::from_value::<RubyDebugConfig>(definition.config.clone())?;
let mut arguments = vec![
"--open".to_string(),
format!("--port={}", port),
format!("--host={}", host),
];
if let Some(script) = &ruby_config.script {
arguments.push(script.clone());
} else if let Some(command) = &ruby_config.command {
arguments.push("--command".to_string());
arguments.push(command.clone());
} else if let Some(command_or_script) = &ruby_config.script_or_command {
if delegate
.which(OsStr::new(&command_or_script))
.await
.is_some()
{
arguments.push("--command".to_string());
}
arguments.push(command_or_script.clone());
} else {
bail!("Ruby debug config must have 'script' or 'command' args");
}
arguments.extend(ruby_config.args);
let mut configuration = definition.config.clone();
if let Some(configuration) = configuration.as_object_mut() {
configuration
.entry("cwd")
.or_insert_with(|| delegate.worktree_root_path().to_string_lossy().into());
}
Ok(DebugAdapterBinary {
command: Some(rdbg_path.to_string_lossy().to_string()),
arguments,
connection: Some(dap::adapters::TcpArguments {
host,
port,
timeout,
}),
cwd: Some(
ruby_config
.cwd
.unwrap_or(delegate.worktree_root_path().to_owned()),
),
envs: ruby_config.env.into_iter().collect(),
request_args: StartDebuggingRequestArguments {
request: self.request_kind(&definition.config).await?,
configuration,
},
})
}
}

View File

@@ -21,7 +21,7 @@ use project::{
use settings::Settings as _;
use std::{
borrow::Cow,
collections::{HashMap, VecDeque},
collections::{BTreeMap, HashMap, VecDeque},
sync::Arc,
};
use util::maybe;
@@ -32,13 +32,6 @@ use workspace::{
ui::{Button, Clickable, ContextMenu, Label, LabelCommon, PopoverMenu, h_flex},
};
// TODO:
// - [x] stop sorting by session ID
// - [x] pick the most recent session by default (logs if available, RPC messages otherwise)
// - [ ] dump the launch/attach request somewhere (logs?)
const MAX_SESSIONS: usize = 10;
struct DapLogView {
editor: Entity<Editor>,
focus_handle: FocusHandle,
@@ -49,14 +42,34 @@ struct DapLogView {
_subscriptions: Vec<Subscription>,
}
struct LogStoreEntryIdentifier<'a> {
session_id: SessionId,
project: Cow<'a, WeakEntity<Project>>,
}
impl LogStoreEntryIdentifier<'_> {
fn to_owned(&self) -> LogStoreEntryIdentifier<'static> {
LogStoreEntryIdentifier {
session_id: self.session_id,
project: Cow::Owned(self.project.as_ref().clone()),
}
}
}
struct LogStoreMessage {
id: LogStoreEntryIdentifier<'static>,
kind: IoKind,
command: Option<SharedString>,
message: SharedString,
}
pub struct LogStore {
projects: HashMap<WeakEntity<Project>, ProjectState>,
debug_sessions: VecDeque<DebugAdapterState>,
rpc_tx: UnboundedSender<(SessionId, IoKind, Option<SharedString>, SharedString)>,
adapter_log_tx: UnboundedSender<(SessionId, IoKind, Option<SharedString>, SharedString)>,
rpc_tx: UnboundedSender<LogStoreMessage>,
adapter_log_tx: UnboundedSender<LogStoreMessage>,
}
struct ProjectState {
debug_sessions: BTreeMap<SessionId, DebugAdapterState>,
_subscriptions: [gpui::Subscription; 2],
}
@@ -122,13 +135,12 @@ impl DebugAdapterState {
impl LogStore {
pub fn new(cx: &Context<Self>) -> Self {
let (rpc_tx, mut rpc_rx) =
unbounded::<(SessionId, IoKind, Option<SharedString>, SharedString)>();
let (rpc_tx, mut rpc_rx) = unbounded::<LogStoreMessage>();
cx.spawn(async move |this, cx| {
while let Some((session_id, io_kind, command, message)) = rpc_rx.next().await {
while let Some(message) = rpc_rx.next().await {
if let Some(this) = this.upgrade() {
this.update(cx, |this, cx| {
this.add_debug_adapter_message(session_id, io_kind, command, message, cx);
this.add_debug_adapter_message(message, cx);
})?;
}
@@ -138,13 +150,12 @@ impl LogStore {
})
.detach_and_log_err(cx);
let (adapter_log_tx, mut adapter_log_rx) =
unbounded::<(SessionId, IoKind, Option<SharedString>, SharedString)>();
let (adapter_log_tx, mut adapter_log_rx) = unbounded::<LogStoreMessage>();
cx.spawn(async move |this, cx| {
while let Some((session_id, io_kind, _, message)) = adapter_log_rx.next().await {
while let Some(message) = adapter_log_rx.next().await {
if let Some(this) = this.upgrade() {
this.update(cx, |this, cx| {
this.add_debug_adapter_log(session_id, io_kind, message, cx);
this.add_debug_adapter_log(message, cx);
})?;
}
@@ -157,57 +168,76 @@ impl LogStore {
rpc_tx,
adapter_log_tx,
projects: HashMap::new(),
debug_sessions: Default::default(),
}
}
pub fn add_project(&mut self, project: &Entity<Project>, cx: &mut Context<Self>) {
let weak_project = project.downgrade();
self.projects.insert(
project.downgrade(),
ProjectState {
_subscriptions: [
cx.observe_release(project, move |this, _, _| {
this.projects.remove(&weak_project);
cx.observe_release(project, {
let weak_project = project.downgrade();
move |this, _, _| {
this.projects.remove(&weak_project);
}
}),
cx.subscribe(
&project.read(cx).dap_store(),
|this, dap_store, event, cx| match event {
cx.subscribe(&project.read(cx).dap_store(), {
let weak_project = project.downgrade();
move |this, dap_store, event, cx| match event {
dap_store::DapStoreEvent::DebugClientStarted(session_id) => {
let session = dap_store.read(cx).session_by_id(session_id);
if let Some(session) = session {
this.add_debug_session(*session_id, session, cx);
this.add_debug_session(
LogStoreEntryIdentifier {
project: Cow::Owned(weak_project.clone()),
session_id: *session_id,
},
session,
cx,
);
}
}
dap_store::DapStoreEvent::DebugClientShutdown(session_id) => {
this.get_debug_adapter_state(*session_id)
.iter_mut()
.for_each(|state| state.is_terminated = true);
let id = LogStoreEntryIdentifier {
project: Cow::Borrowed(&weak_project),
session_id: *session_id,
};
if let Some(state) = this.get_debug_adapter_state(&id) {
state.is_terminated = true;
}
this.clean_sessions(cx);
}
_ => {}
},
),
}
}),
],
debug_sessions: Default::default(),
},
);
}
fn get_debug_adapter_state(&mut self, id: SessionId) -> Option<&mut DebugAdapterState> {
self.debug_sessions
.iter_mut()
.find(|adapter_state| adapter_state.id == id)
fn get_debug_adapter_state(
&mut self,
id: &LogStoreEntryIdentifier<'_>,
) -> Option<&mut DebugAdapterState> {
self.projects
.get_mut(&id.project)
.and_then(|state| state.debug_sessions.get_mut(&id.session_id))
}
fn add_debug_adapter_message(
&mut self,
id: SessionId,
io_kind: IoKind,
command: Option<SharedString>,
message: SharedString,
LogStoreMessage {
id,
kind: io_kind,
command,
message,
}: LogStoreMessage,
cx: &mut Context<Self>,
) {
let Some(debug_client_state) = self.get_debug_adapter_state(id) else {
let Some(debug_client_state) = self.get_debug_adapter_state(&id) else {
return;
};
@@ -229,7 +259,7 @@ impl LogStore {
if rpc_messages.last_message_kind != Some(kind) {
Self::get_debug_adapter_entry(
&mut rpc_messages.messages,
id,
id.to_owned(),
kind.label().into(),
LogKind::Rpc,
cx,
@@ -239,7 +269,7 @@ impl LogStore {
let entry = Self::get_debug_adapter_entry(
&mut rpc_messages.messages,
id,
id.to_owned(),
message,
LogKind::Rpc,
cx,
@@ -260,12 +290,15 @@ impl LogStore {
fn add_debug_adapter_log(
&mut self,
id: SessionId,
io_kind: IoKind,
message: SharedString,
LogStoreMessage {
id,
kind: io_kind,
message,
..
}: LogStoreMessage,
cx: &mut Context<Self>,
) {
let Some(debug_adapter_state) = self.get_debug_adapter_state(id) else {
let Some(debug_adapter_state) = self.get_debug_adapter_state(&id) else {
return;
};
@@ -276,7 +309,7 @@ impl LogStore {
Self::get_debug_adapter_entry(
&mut debug_adapter_state.log_messages,
id,
id.to_owned(),
message,
LogKind::Adapter,
cx,
@@ -286,13 +319,17 @@ impl LogStore {
fn get_debug_adapter_entry(
log_lines: &mut VecDeque<SharedString>,
id: SessionId,
id: LogStoreEntryIdentifier<'static>,
message: SharedString,
kind: LogKind,
cx: &mut Context<Self>,
) -> SharedString {
while log_lines.len() >= RpcMessages::MESSAGE_QUEUE_LIMIT {
log_lines.pop_front();
if let Some(excess) = log_lines
.len()
.checked_sub(RpcMessages::MESSAGE_QUEUE_LIMIT)
&& excess > 0
{
log_lines.drain(..excess);
}
let format_messages = DebuggerSettings::get_global(cx).format_dap_log_messages;
@@ -322,118 +359,111 @@ impl LogStore {
fn add_debug_session(
&mut self,
session_id: SessionId,
id: LogStoreEntryIdentifier<'static>,
session: Entity<Session>,
cx: &mut Context<Self>,
) {
if self
.debug_sessions
.iter_mut()
.any(|adapter_state| adapter_state.id == session_id)
{
return;
}
maybe!({
let project_entry = self.projects.get_mut(&id.project)?;
let std::collections::btree_map::Entry::Vacant(state) =
project_entry.debug_sessions.entry(id.session_id)
else {
return None;
};
let (adapter_name, has_adapter_logs) = session.read_with(cx, |session, _| {
(
session.adapter(),
session
.adapter_client()
.map(|client| client.has_adapter_logs())
.unwrap_or(false),
)
let (adapter_name, has_adapter_logs) = session.read_with(cx, |session, _| {
(
session.adapter(),
session
.adapter_client()
.map_or(false, |client| client.has_adapter_logs()),
)
});
state.insert(DebugAdapterState::new(
id.session_id,
adapter_name,
has_adapter_logs,
));
self.clean_sessions(cx);
let io_tx = self.rpc_tx.clone();
let client = session.read(cx).adapter_client()?;
let project = id.project.clone();
let session_id = id.session_id;
client.add_log_handler(
move |kind, command, message| {
io_tx
.unbounded_send(LogStoreMessage {
id: LogStoreEntryIdentifier {
session_id,
project: project.clone(),
},
kind,
command: command.map(|command| command.to_owned().into()),
message: message.to_owned().into(),
})
.ok();
},
LogKind::Rpc,
);
let log_io_tx = self.adapter_log_tx.clone();
let project = id.project;
client.add_log_handler(
move |kind, command, message| {
log_io_tx
.unbounded_send(LogStoreMessage {
id: LogStoreEntryIdentifier {
session_id,
project: project.clone(),
},
kind,
command: command.map(|command| command.to_owned().into()),
message: message.to_owned().into(),
})
.ok();
},
LogKind::Adapter,
);
Some(())
});
self.debug_sessions.push_back(DebugAdapterState::new(
session_id,
adapter_name,
has_adapter_logs,
));
self.clean_sessions(cx);
let io_tx = self.rpc_tx.clone();
let Some(client) = session.read(cx).adapter_client() else {
return;
};
client.add_log_handler(
move |io_kind, command, message| {
io_tx
.unbounded_send((
session_id,
io_kind,
command.map(|command| command.to_owned().into()),
message.to_owned().into(),
))
.ok();
},
LogKind::Rpc,
);
let log_io_tx = self.adapter_log_tx.clone();
client.add_log_handler(
move |io_kind, command, message| {
log_io_tx
.unbounded_send((
session_id,
io_kind,
command.map(|command| command.to_owned().into()),
message.to_owned().into(),
))
.ok();
},
LogKind::Adapter,
);
}
fn clean_sessions(&mut self, cx: &mut Context<Self>) {
let mut to_remove = self.debug_sessions.len().saturating_sub(MAX_SESSIONS);
self.debug_sessions.retain(|session| {
if to_remove > 0 && session.is_terminated {
to_remove -= 1;
return false;
}
true
self.projects.values_mut().for_each(|project| {
project
.debug_sessions
.retain(|_, session| !session.is_terminated);
});
cx.notify();
}
fn log_messages_for_session(
&mut self,
session_id: SessionId,
id: &LogStoreEntryIdentifier<'_>,
) -> Option<&mut VecDeque<SharedString>> {
self.debug_sessions
.iter_mut()
.find(|session| session.id == session_id)
self.get_debug_adapter_state(id)
.map(|state| &mut state.log_messages)
}
fn rpc_messages_for_session(
&mut self,
session_id: SessionId,
id: &LogStoreEntryIdentifier<'_>,
) -> Option<&mut VecDeque<SharedString>> {
self.debug_sessions.iter_mut().find_map(|state| {
if state.id == session_id {
Some(&mut state.rpc_messages.messages)
} else {
None
}
})
self.get_debug_adapter_state(id)
.map(|state| &mut state.rpc_messages.messages)
}
fn initialization_sequence_for_session(
&mut self,
session_id: SessionId,
) -> Option<&mut Vec<SharedString>> {
self.debug_sessions.iter_mut().find_map(|state| {
if state.id == session_id {
Some(&mut state.rpc_messages.initialization_sequence)
} else {
None
}
})
id: &LogStoreEntryIdentifier<'_>,
) -> Option<&Vec<SharedString>> {
self.get_debug_adapter_state(&id)
.map(|state| &state.rpc_messages.initialization_sequence)
}
}
@@ -453,10 +483,11 @@ impl Render for DapLogToolbarItemView {
return Empty.into_any_element();
};
let (menu_rows, current_session_id) = log_view.update(cx, |log_view, cx| {
let (menu_rows, current_session_id, project) = log_view.update(cx, |log_view, cx| {
(
log_view.menu_items(cx),
log_view.current_view.map(|(session_id, _)| session_id),
log_view.project.downgrade(),
)
});
@@ -484,6 +515,7 @@ impl Render for DapLogToolbarItemView {
.menu(move |mut window, cx| {
let log_view = log_view.clone();
let menu_rows = menu_rows.clone();
let project = project.clone();
ContextMenu::build(&mut window, cx, move |mut menu, window, _cx| {
for row in menu_rows.into_iter() {
menu = menu.custom_row(move |_window, _cx| {
@@ -509,8 +541,15 @@ impl Render for DapLogToolbarItemView {
.child(Label::new(ADAPTER_LOGS))
.into_any_element()
},
window.handler_for(&log_view, move |view, window, cx| {
view.show_log_messages_for_adapter(row.session_id, window, cx);
window.handler_for(&log_view, {
let project = project.clone();
let id = LogStoreEntryIdentifier {
project: Cow::Owned(project),
session_id: row.session_id,
};
move |view, window, cx| {
view.show_log_messages_for_adapter(&id, window, cx);
}
}),
);
}
@@ -524,8 +563,15 @@ impl Render for DapLogToolbarItemView {
.child(Label::new(RPC_MESSAGES))
.into_any_element()
},
window.handler_for(&log_view, move |view, window, cx| {
view.show_rpc_trace_for_server(row.session_id, window, cx);
window.handler_for(&log_view, {
let project = project.clone();
let id = LogStoreEntryIdentifier {
project: Cow::Owned(project),
session_id: row.session_id,
};
move |view, window, cx| {
view.show_rpc_trace_for_server(&id, window, cx);
}
}),
)
.custom_entry(
@@ -536,12 +582,17 @@ impl Render for DapLogToolbarItemView {
.child(Label::new(INITIALIZATION_SEQUENCE))
.into_any_element()
},
window.handler_for(&log_view, move |view, window, cx| {
view.show_initialization_sequence_for_server(
row.session_id,
window,
cx,
);
window.handler_for(&log_view, {
let project = project.clone();
let id = LogStoreEntryIdentifier {
project: Cow::Owned(project),
session_id: row.session_id,
};
move |view, window, cx| {
view.show_initialization_sequence_for_server(
&id, window, cx,
);
}
}),
);
}
@@ -613,7 +664,9 @@ impl DapLogView {
let events_subscriptions = cx.subscribe(&log_store, |log_view, _, event, cx| match event {
Event::NewLogEntry { id, entry, kind } => {
if log_view.current_view == Some((*id, *kind)) {
if log_view.current_view == Some((id.session_id, *kind))
&& log_view.project == *id.project
{
log_view.editor.update(cx, |editor, cx| {
editor.set_read_only(false);
let last_point = editor.buffer().read(cx).len(cx);
@@ -629,12 +682,18 @@ impl DapLogView {
}
}
});
let weak_project = project.downgrade();
let state_info = log_store
.read(cx)
.debug_sessions
.back()
.map(|session| (session.id, session.has_adapter_logs));
.projects
.get(&weak_project)
.and_then(|project| {
project
.debug_sessions
.values()
.next_back()
.map(|session| (session.id, session.has_adapter_logs))
});
let mut this = Self {
editor,
@@ -647,10 +706,14 @@ impl DapLogView {
};
if let Some((session_id, have_adapter_logs)) = state_info {
let id = LogStoreEntryIdentifier {
session_id,
project: Cow::Owned(weak_project),
};
if have_adapter_logs {
this.show_log_messages_for_adapter(session_id, window, cx);
this.show_log_messages_for_adapter(&id, window, cx);
} else {
this.show_rpc_trace_for_server(session_id, window, cx);
this.show_rpc_trace_for_server(&id, window, cx);
}
}
@@ -690,31 +753,38 @@ impl DapLogView {
fn menu_items(&self, cx: &App) -> Vec<DapMenuItem> {
self.log_store
.read(cx)
.debug_sessions
.iter()
.rev()
.map(|state| DapMenuItem {
session_id: state.id,
adapter_name: state.adapter_name.clone(),
has_adapter_logs: state.has_adapter_logs,
selected_entry: self.current_view.map_or(LogKind::Adapter, |(_, kind)| kind),
.projects
.get(&self.project.downgrade())
.map_or_else(Vec::new, |state| {
state
.debug_sessions
.values()
.rev()
.map(|state| DapMenuItem {
session_id: state.id,
adapter_name: state.adapter_name.clone(),
has_adapter_logs: state.has_adapter_logs,
selected_entry: self
.current_view
.map_or(LogKind::Adapter, |(_, kind)| kind),
})
.collect::<Vec<_>>()
})
.collect::<Vec<_>>()
}
fn show_rpc_trace_for_server(
&mut self,
session_id: SessionId,
id: &LogStoreEntryIdentifier<'_>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let rpc_log = self.log_store.update(cx, |log_store, _| {
log_store
.rpc_messages_for_session(session_id)
.rpc_messages_for_session(id)
.map(|state| log_contents(state.iter().cloned()))
});
if let Some(rpc_log) = rpc_log {
self.current_view = Some((session_id, LogKind::Rpc));
self.current_view = Some((id.session_id, LogKind::Rpc));
let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, window, cx);
let language = self.project.read(cx).languages().language_for_name("JSON");
editor
@@ -725,8 +795,7 @@ impl DapLogView {
.expect("log buffer should be a singleton")
.update(cx, |_, cx| {
cx.spawn({
let buffer = cx.entity();
async move |_, cx| {
async move |buffer, cx| {
let language = language.await.ok();
buffer.update(cx, |buffer, cx| {
buffer.set_language(language, cx);
@@ -746,17 +815,17 @@ impl DapLogView {
fn show_log_messages_for_adapter(
&mut self,
session_id: SessionId,
id: &LogStoreEntryIdentifier<'_>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let message_log = self.log_store.update(cx, |log_store, _| {
log_store
.log_messages_for_session(session_id)
.log_messages_for_session(id)
.map(|state| log_contents(state.iter().cloned()))
});
if let Some(message_log) = message_log {
self.current_view = Some((session_id, LogKind::Adapter));
self.current_view = Some((id.session_id, LogKind::Adapter));
let (editor, editor_subscriptions) = Self::editor_for_logs(message_log, window, cx);
editor
.read(cx)
@@ -775,17 +844,17 @@ impl DapLogView {
fn show_initialization_sequence_for_server(
&mut self,
session_id: SessionId,
id: &LogStoreEntryIdentifier<'_>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let rpc_log = self.log_store.update(cx, |log_store, _| {
log_store
.initialization_sequence_for_session(session_id)
.initialization_sequence_for_session(id)
.map(|state| log_contents(state.iter().cloned()))
});
if let Some(rpc_log) = rpc_log {
self.current_view = Some((session_id, LogKind::Rpc));
self.current_view = Some((id.session_id, LogKind::Rpc));
let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, window, cx);
let language = self.project.read(cx).languages().language_for_name("JSON");
editor
@@ -993,9 +1062,9 @@ impl Focusable for DapLogView {
}
}
pub enum Event {
enum Event {
NewLogEntry {
id: SessionId,
id: LogStoreEntryIdentifier<'static>,
entry: SharedString,
kind: LogKind,
},
@@ -1008,31 +1077,30 @@ impl EventEmitter<SearchEvent> for DapLogView {}
#[cfg(any(test, feature = "test-support"))]
impl LogStore {
pub fn contained_session_ids(&self) -> Vec<SessionId> {
self.debug_sessions
.iter()
.map(|session| session.id)
.collect()
pub fn has_projects(&self) -> bool {
!self.projects.is_empty()
}
pub fn rpc_messages_for_session_id(&self, session_id: SessionId) -> Vec<SharedString> {
self.debug_sessions
.iter()
.find(|adapter_state| adapter_state.id == session_id)
.expect("This session should exist if a test is calling")
.rpc_messages
.messages
.clone()
.into()
pub fn contained_session_ids(&self, project: &WeakEntity<Project>) -> Vec<SessionId> {
self.projects.get(project).map_or(vec![], |state| {
state.debug_sessions.keys().copied().collect()
})
}
pub fn log_messages_for_session_id(&self, session_id: SessionId) -> Vec<SharedString> {
self.debug_sessions
.iter()
.find(|adapter_state| adapter_state.id == session_id)
.expect("This session should exist if a test is calling")
.log_messages
.clone()
.into()
pub fn rpc_messages_for_session_id(
&self,
project: &WeakEntity<Project>,
session_id: SessionId,
) -> Vec<SharedString> {
self.projects.get(&project).map_or(vec![], |state| {
state
.debug_sessions
.get(&session_id)
.expect("This session should exist if a test is calling")
.rpc_messages
.messages
.clone()
.into()
})
}
}

View File

@@ -28,6 +28,7 @@ test-support = [
[dependencies]
alacritty_terminal.workspace = true
anyhow.workspace = true
bitflags.workspace = true
client.workspace = true
collections.workspace = true
command_palette_hooks.workspace = true
@@ -81,3 +82,4 @@ unindent.workspace = true
util = { workspace = true, features = ["test-support"] }
workspace = { workspace = true, features = ["test-support"] }
zlog.workspace = true
tree-sitter-go.workspace = true

View File

@@ -100,7 +100,13 @@ impl DebugPanel {
sessions: vec![],
active_session: None,
focus_handle,
breakpoint_list: BreakpointList::new(None, workspace.weak_handle(), &project, cx),
breakpoint_list: BreakpointList::new(
None,
workspace.weak_handle(),
&project,
window,
cx,
),
project,
workspace: workspace.weak_handle(),
context_menu: None,
@@ -695,30 +701,6 @@ impl DebugPanel {
}
}),
)
.child(
IconButton::new("debug-step-out", IconName::ArrowUpRight)
.icon_size(IconSize::XSmall)
.shape(ui::IconButtonShape::Square)
.on_click(window.listener_for(
&running_state,
|this, _, _window, cx| {
this.step_out(cx);
},
))
.disabled(thread_status != ThreadStatus::Stopped)
.tooltip({
let focus_handle = focus_handle.clone();
move |window, cx| {
Tooltip::for_action_in(
"Step out",
&StepOut,
&focus_handle,
window,
cx,
)
}
}),
)
.child(
IconButton::new(
"debug-step-into",
@@ -746,6 +728,30 @@ impl DebugPanel {
}
}),
)
.child(
IconButton::new("debug-step-out", IconName::ArrowUpRight)
.icon_size(IconSize::XSmall)
.shape(ui::IconButtonShape::Square)
.on_click(window.listener_for(
&running_state,
|this, _, _window, cx| {
this.step_out(cx);
},
))
.disabled(thread_status != ThreadStatus::Stopped)
.tooltip({
let focus_handle = focus_handle.clone();
move |window, cx| {
Tooltip::for_action_in(
"Step out",
&StepOut,
&focus_handle,
window,
cx,
)
}
}),
)
.child(Divider::vertical())
.child(
IconButton::new("debug-restart", IconName::DebugRestart)
@@ -862,7 +868,7 @@ impl DebugPanel {
let threads =
running_state.update(cx, |running_state, cx| {
let session = running_state.session();
session.read(cx).is_running().then(|| {
session.read(cx).is_started().then(|| {
session.update(cx, |session, cx| {
session.threads(cx)
})
@@ -1292,6 +1298,11 @@ impl Render for DebugPanel {
}
v_flex()
.when_else(
self.position(window, cx) == DockPosition::Bottom,
|this| this.max_h(self.size),
|this| this.max_w(self.size),
)
.size_full()
.key_context("DebugPanel")
.child(h_flex().children(self.top_controls_strip(window, cx)))
@@ -1462,6 +1473,94 @@ impl Render for DebugPanel {
if has_sessions {
this.children(self.active_session.clone())
} else {
let docked_to_bottom = self.position(window, cx) == DockPosition::Bottom;
let welcome_experience = v_flex()
.when_else(
docked_to_bottom,
|this| this.w_2_3().h_full().pr_8(),
|this| this.w_full().h_1_3(),
)
.items_center()
.justify_center()
.gap_2()
.child(
Button::new("spawn-new-session-empty-state", "New Session")
.icon(IconName::Plus)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.icon_position(IconPosition::Start)
.on_click(|_, window, cx| {
window.dispatch_action(crate::Start.boxed_clone(), cx);
}),
)
.child(
Button::new("edit-debug-settings", "Edit debug.json")
.icon(IconName::Code)
.icon_size(IconSize::XSmall)
.color(Color::Muted)
.icon_color(Color::Muted)
.icon_position(IconPosition::Start)
.on_click(|_, window, cx| {
window.dispatch_action(
zed_actions::OpenProjectDebugTasks.boxed_clone(),
cx,
);
}),
)
.child(
Button::new("open-debugger-docs", "Debugger Docs")
.icon(IconName::Book)
.color(Color::Muted)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.icon_position(IconPosition::Start)
.on_click(|_, _, cx| cx.open_url("https://zed.dev/docs/debugger")),
)
.child(
Button::new(
"spawn-new-session-install-extensions",
"Debugger Extensions",
)
.icon(IconName::Blocks)
.color(Color::Muted)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.icon_position(IconPosition::Start)
.on_click(|_, window, cx| {
window.dispatch_action(
zed_actions::Extensions {
category_filter: Some(
zed_actions::ExtensionCategoryFilter::DebugAdapters,
),
}
.boxed_clone(),
cx,
);
}),
);
let breakpoint_list =
v_flex()
.group("base-breakpoint-list")
.items_start()
.when_else(
docked_to_bottom,
|this| this.min_w_1_3().h_full(),
|this| this.w_full().h_2_3(),
)
.p_1()
.child(
h_flex()
.pl_1()
.w_full()
.justify_between()
.child(Label::new("Breakpoints").size(LabelSize::Small))
.child(h_flex().visible_on_hover("base-breakpoint-list").child(
self.breakpoint_list.read(cx).render_control_strip(),
))
.track_focus(&self.breakpoint_list.focus_handle(cx)),
)
.child(Divider::horizontal())
.child(self.breakpoint_list.clone());
this.child(
v_flex()
.h_full()
@@ -1469,65 +1568,23 @@ impl Render for DebugPanel {
.items_center()
.justify_center()
.child(
h_flex().size_full()
.items_start()
.child(v_flex().group("base-breakpoint-list").items_start().min_w_1_3().h_full().p_1()
.child(h_flex().pl_1().w_full().justify_between()
.child(Label::new("Breakpoints").size(LabelSize::Small))
.child(h_flex().visible_on_hover("base-breakpoint-list").child(self.breakpoint_list.read(cx).render_control_strip())))
.child(Divider::horizontal())
.child(self.breakpoint_list.clone()))
.child(Divider::vertical())
.child(
v_flex().w_2_3().h_full().items_center().justify_center()
.gap_2()
.pr_8()
.child(
Button::new("spawn-new-session-empty-state", "New Session")
.icon(IconName::Plus)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.icon_position(IconPosition::Start)
.on_click(|_, window, cx| {
window.dispatch_action(crate::Start.boxed_clone(), cx);
})
)
.child(
Button::new("edit-debug-settings", "Edit debug.json")
.icon(IconName::Code)
.icon_size(IconSize::XSmall)
.color(Color::Muted)
.icon_color(Color::Muted)
.icon_position(IconPosition::Start)
.on_click(|_, window, cx| {
window.dispatch_action(zed_actions::OpenProjectDebugTasks.boxed_clone(), cx);
})
)
.child(
Button::new("open-debugger-docs", "Debugger Docs")
.icon(IconName::Book)
.color(Color::Muted)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.icon_position(IconPosition::Start)
.on_click(|_, _, cx| {
cx.open_url("https://zed.dev/docs/debugger")
})
)
.child(
Button::new("spawn-new-session-install-extensions", "Debugger Extensions")
.icon(IconName::Blocks)
.color(Color::Muted)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.icon_position(IconPosition::Start)
.on_click(|_, window, cx| {
window.dispatch_action(zed_actions::Extensions { category_filter: Some(zed_actions::ExtensionCategoryFilter::DebugAdapters)}.boxed_clone(), cx);
})
)
)
)
div()
.when_else(docked_to_bottom, Div::h_flex, Div::v_flex)
.size_full()
.map(|this| {
if docked_to_bottom {
this.items_start()
.child(breakpoint_list)
.child(Divider::vertical())
.child(welcome_experience)
} else {
this.items_end()
.child(welcome_experience)
.child(Divider::horizontal())
.child(breakpoint_list)
}
}),
),
)
}
})

View File

@@ -11,7 +11,7 @@ use project::worktree_store::WorktreeStore;
use rpc::proto;
use running::RunningState;
use std::{cell::OnceCell, sync::OnceLock};
use ui::{Indicator, prelude::*};
use ui::{Indicator, Tooltip, prelude::*};
use workspace::{
CollaboratorId, FollowableItem, ViewId, Workspace,
item::{self, Item},
@@ -153,6 +153,8 @@ impl DebugSession {
};
h_flex()
.id("session-label")
.tooltip(Tooltip::text(format!("Session {}", self.session_id(cx).0,)))
.ml(depth * px(16.0))
.gap_2()
.when_some(icon, |this, indicator| this.child(indicator))

View File

@@ -697,8 +697,13 @@ impl RunningState {
)
});
let breakpoint_list =
BreakpointList::new(Some(session.clone()), workspace.clone(), &project, cx);
let breakpoint_list = BreakpointList::new(
Some(session.clone()),
workspace.clone(),
&project,
window,
cx,
);
let _subscriptions = vec![
cx.on_app_quit(move |this, cx| {

View File

@@ -5,11 +5,11 @@ use std::{
time::Duration,
};
use dap::ExceptionBreakpointsFilter;
use dap::{Capabilities, ExceptionBreakpointsFilter};
use editor::Editor;
use gpui::{
Action, AppContext, Entity, FocusHandle, Focusable, MouseButton, ScrollStrategy, Stateful,
Task, UniformListScrollHandle, WeakEntity, uniform_list,
Action, AppContext, ClickEvent, Entity, FocusHandle, Focusable, MouseButton, ScrollStrategy,
Stateful, Task, UniformListScrollHandle, WeakEntity, actions, uniform_list,
};
use language::Point;
use project::{
@@ -21,16 +21,20 @@ use project::{
worktree_store::WorktreeStore,
};
use ui::{
AnyElement, App, ButtonCommon, Clickable, Color, Context, Disableable, Div, FluentBuilder as _,
Icon, IconButton, IconName, IconSize, Indicator, InteractiveElement, IntoElement, Label,
LabelCommon, LabelSize, ListItem, ParentElement, Render, Scrollbar, ScrollbarState,
SharedString, StatefulInteractiveElement, Styled, Toggleable, Tooltip, Window, div, h_flex, px,
v_flex,
ActiveTheme, AnyElement, App, ButtonCommon, Clickable, Color, Context, Disableable, Div,
Divider, FluentBuilder as _, Icon, IconButton, IconName, IconSize, Indicator,
InteractiveElement, IntoElement, Label, LabelCommon, LabelSize, ListItem, ParentElement,
Render, RenderOnce, Scrollbar, ScrollbarState, SharedString, StatefulInteractiveElement,
Styled, Toggleable, Tooltip, Window, div, h_flex, px, v_flex,
};
use util::ResultExt;
use workspace::Workspace;
use zed_actions::{ToggleEnableBreakpoint, UnsetBreakpoint};
actions!(
debugger,
[PreviousBreakpointProperty, NextBreakpointProperty]
);
#[derive(Clone, Copy, PartialEq)]
pub(crate) enum SelectedBreakpointKind {
Source,
@@ -48,6 +52,8 @@ pub(crate) struct BreakpointList {
focus_handle: FocusHandle,
scroll_handle: UniformListScrollHandle,
selected_ix: Option<usize>,
input: Entity<Editor>,
strip_mode: Option<ActiveBreakpointStripMode>,
}
impl Focusable for BreakpointList {
@@ -56,11 +62,19 @@ impl Focusable for BreakpointList {
}
}
#[derive(Clone, Copy, PartialEq)]
enum ActiveBreakpointStripMode {
Log,
Condition,
HitCondition,
}
impl BreakpointList {
pub(crate) fn new(
session: Option<Entity<Session>>,
workspace: WeakEntity<Workspace>,
project: &Entity<Project>,
window: &mut Window,
cx: &mut App,
) -> Entity<Self> {
let project = project.read(cx);
@@ -70,7 +84,7 @@ impl BreakpointList {
let scroll_handle = UniformListScrollHandle::new();
let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
cx.new(|_| Self {
cx.new(|cx| Self {
breakpoint_store,
worktree_store,
scrollbar_state,
@@ -82,17 +96,28 @@ impl BreakpointList {
focus_handle,
scroll_handle,
selected_ix: None,
input: cx.new(|cx| Editor::single_line(window, cx)),
strip_mode: None,
})
}
fn edit_line_breakpoint(
&mut self,
&self,
path: Arc<Path>,
row: u32,
action: BreakpointEditAction,
cx: &mut Context<Self>,
cx: &mut App,
) {
self.breakpoint_store.update(cx, |breakpoint_store, cx| {
Self::edit_line_breakpoint_inner(&self.breakpoint_store, path, row, action, cx);
}
fn edit_line_breakpoint_inner(
breakpoint_store: &Entity<BreakpointStore>,
path: Arc<Path>,
row: u32,
action: BreakpointEditAction,
cx: &mut App,
) {
breakpoint_store.update(cx, |breakpoint_store, cx| {
if let Some((buffer, breakpoint)) = breakpoint_store.breakpoint_at_row(&path, row, cx) {
breakpoint_store.toggle_breakpoint(buffer, breakpoint, action, cx);
} else {
@@ -148,16 +173,63 @@ impl BreakpointList {
})
}
fn select_ix(&mut self, ix: Option<usize>, cx: &mut Context<Self>) {
fn set_active_breakpoint_property(
&mut self,
prop: ActiveBreakpointStripMode,
window: &mut Window,
cx: &mut App,
) {
self.strip_mode = Some(prop);
let placeholder = match prop {
ActiveBreakpointStripMode::Log => "Set Log Message",
ActiveBreakpointStripMode::Condition => "Set Condition",
ActiveBreakpointStripMode::HitCondition => "Set Hit Condition",
};
let mut is_exception_breakpoint = true;
let active_value = self.selected_ix.and_then(|ix| {
self.breakpoints.get(ix).and_then(|bp| {
if let BreakpointEntryKind::LineBreakpoint(bp) = &bp.kind {
is_exception_breakpoint = false;
match prop {
ActiveBreakpointStripMode::Log => bp.breakpoint.message.clone(),
ActiveBreakpointStripMode::Condition => bp.breakpoint.condition.clone(),
ActiveBreakpointStripMode::HitCondition => {
bp.breakpoint.hit_condition.clone()
}
}
} else {
None
}
})
});
self.input.update(cx, |this, cx| {
this.set_placeholder_text(placeholder, cx);
this.set_read_only(is_exception_breakpoint);
this.set_text(active_value.as_deref().unwrap_or(""), window, cx);
});
}
fn select_ix(&mut self, ix: Option<usize>, window: &mut Window, cx: &mut Context<Self>) {
self.selected_ix = ix;
if let Some(ix) = ix {
self.scroll_handle
.scroll_to_item(ix, ScrollStrategy::Center);
}
if let Some(mode) = self.strip_mode {
self.set_active_breakpoint_property(mode, window, cx);
}
cx.notify();
}
fn select_next(&mut self, _: &menu::SelectNext, _window: &mut Window, cx: &mut Context<Self>) {
fn select_next(&mut self, _: &menu::SelectNext, window: &mut Window, cx: &mut Context<Self>) {
if self.strip_mode.is_some() {
if self.input.focus_handle(cx).contains_focused(window, cx) {
cx.propagate();
return;
}
}
let ix = match self.selected_ix {
_ if self.breakpoints.len() == 0 => None,
None => Some(0),
@@ -169,15 +241,21 @@ impl BreakpointList {
}
}
};
self.select_ix(ix, cx);
self.select_ix(ix, window, cx);
}
fn select_previous(
&mut self,
_: &menu::SelectPrevious,
_window: &mut Window,
window: &mut Window,
cx: &mut Context<Self>,
) {
if self.strip_mode.is_some() {
if self.input.focus_handle(cx).contains_focused(window, cx) {
cx.propagate();
return;
}
}
let ix = match self.selected_ix {
_ if self.breakpoints.len() == 0 => None,
None => Some(self.breakpoints.len() - 1),
@@ -189,37 +267,105 @@ impl BreakpointList {
}
}
};
self.select_ix(ix, cx);
self.select_ix(ix, window, cx);
}
fn select_first(
&mut self,
_: &menu::SelectFirst,
_window: &mut Window,
cx: &mut Context<Self>,
) {
fn select_first(&mut self, _: &menu::SelectFirst, window: &mut Window, cx: &mut Context<Self>) {
if self.strip_mode.is_some() {
if self.input.focus_handle(cx).contains_focused(window, cx) {
cx.propagate();
return;
}
}
let ix = if self.breakpoints.len() > 0 {
Some(0)
} else {
None
};
self.select_ix(ix, cx);
self.select_ix(ix, window, cx);
}
fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
fn select_last(&mut self, _: &menu::SelectLast, window: &mut Window, cx: &mut Context<Self>) {
if self.strip_mode.is_some() {
if self.input.focus_handle(cx).contains_focused(window, cx) {
cx.propagate();
return;
}
}
let ix = if self.breakpoints.len() > 0 {
Some(self.breakpoints.len() - 1)
} else {
None
};
self.select_ix(ix, cx);
self.select_ix(ix, window, cx);
}
fn dismiss(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context<Self>) {
if self.input.focus_handle(cx).contains_focused(window, cx) {
self.focus_handle.focus(window);
} else if self.strip_mode.is_some() {
self.strip_mode.take();
cx.notify();
} else {
cx.propagate();
}
}
fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
let Some(entry) = self.selected_ix.and_then(|ix| self.breakpoints.get_mut(ix)) else {
return;
};
if let Some(mode) = self.strip_mode {
let handle = self.input.focus_handle(cx);
if handle.is_focused(window) {
// Go back to the main strip. Save the result as well.
let text = self.input.read(cx).text(cx);
match mode {
ActiveBreakpointStripMode::Log => match &entry.kind {
BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
Self::edit_line_breakpoint_inner(
&self.breakpoint_store,
line_breakpoint.breakpoint.path.clone(),
line_breakpoint.breakpoint.row,
BreakpointEditAction::EditLogMessage(Arc::from(text)),
cx,
);
}
_ => {}
},
ActiveBreakpointStripMode::Condition => match &entry.kind {
BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
Self::edit_line_breakpoint_inner(
&self.breakpoint_store,
line_breakpoint.breakpoint.path.clone(),
line_breakpoint.breakpoint.row,
BreakpointEditAction::EditCondition(Arc::from(text)),
cx,
);
}
_ => {}
},
ActiveBreakpointStripMode::HitCondition => match &entry.kind {
BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
Self::edit_line_breakpoint_inner(
&self.breakpoint_store,
line_breakpoint.breakpoint.path.clone(),
line_breakpoint.breakpoint.row,
BreakpointEditAction::EditHitCondition(Arc::from(text)),
cx,
);
}
_ => {}
},
}
self.focus_handle.focus(window);
} else {
handle.focus(window);
}
return;
}
match &mut entry.kind {
BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
let path = line_breakpoint.breakpoint.path.clone();
@@ -233,12 +379,18 @@ impl BreakpointList {
fn toggle_enable_breakpoint(
&mut self,
_: &ToggleEnableBreakpoint,
_window: &mut Window,
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(entry) = self.selected_ix.and_then(|ix| self.breakpoints.get_mut(ix)) else {
return;
};
if self.strip_mode.is_some() {
if self.input.focus_handle(cx).contains_focused(window, cx) {
cx.propagate();
return;
}
}
match &mut entry.kind {
BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
@@ -279,6 +431,50 @@ impl BreakpointList {
cx.notify();
}
fn previous_breakpoint_property(
&mut self,
_: &PreviousBreakpointProperty,
window: &mut Window,
cx: &mut Context<Self>,
) {
let next_mode = match self.strip_mode {
Some(ActiveBreakpointStripMode::Log) => None,
Some(ActiveBreakpointStripMode::Condition) => Some(ActiveBreakpointStripMode::Log),
Some(ActiveBreakpointStripMode::HitCondition) => {
Some(ActiveBreakpointStripMode::Condition)
}
None => Some(ActiveBreakpointStripMode::HitCondition),
};
if let Some(mode) = next_mode {
self.set_active_breakpoint_property(mode, window, cx);
} else {
self.strip_mode.take();
}
cx.notify();
}
fn next_breakpoint_property(
&mut self,
_: &NextBreakpointProperty,
window: &mut Window,
cx: &mut Context<Self>,
) {
let next_mode = match self.strip_mode {
Some(ActiveBreakpointStripMode::Log) => Some(ActiveBreakpointStripMode::Condition),
Some(ActiveBreakpointStripMode::Condition) => {
Some(ActiveBreakpointStripMode::HitCondition)
}
Some(ActiveBreakpointStripMode::HitCondition) => None,
None => Some(ActiveBreakpointStripMode::Log),
};
if let Some(mode) = next_mode {
self.set_active_breakpoint_property(mode, window, cx);
} else {
self.strip_mode.take();
}
cx.notify();
}
fn hide_scrollbar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |panel, cx| {
@@ -294,20 +490,31 @@ impl BreakpointList {
}))
}
fn render_list(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
fn render_list(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
let selected_ix = self.selected_ix;
let focus_handle = self.focus_handle.clone();
let supported_breakpoint_properties = self
.session
.as_ref()
.map(|session| SupportedBreakpointProperties::from(session.read(cx).capabilities()))
.unwrap_or_else(SupportedBreakpointProperties::empty);
let strip_mode = self.strip_mode;
uniform_list(
"breakpoint-list",
self.breakpoints.len(),
cx.processor(move |this, range: Range<usize>, window, cx| {
cx.processor(move |this, range: Range<usize>, _, _| {
range
.clone()
.zip(&mut this.breakpoints[range])
.map(|(ix, breakpoint)| {
breakpoint
.render(ix, focus_handle.clone(), window, cx)
.toggle_state(Some(ix) == selected_ix)
.render(
strip_mode,
supported_breakpoint_properties,
ix,
Some(ix) == selected_ix,
focus_handle.clone(),
)
.into_any_element()
})
.collect()
@@ -443,7 +650,6 @@ impl BreakpointList {
impl Render for BreakpointList {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
// let old_len = self.breakpoints.len();
let breakpoints = self.breakpoint_store.read(cx).all_source_breakpoints(cx);
self.breakpoints.clear();
let weak = cx.weak_entity();
@@ -523,15 +729,46 @@ impl Render for BreakpointList {
.on_action(cx.listener(Self::select_previous))
.on_action(cx.listener(Self::select_first))
.on_action(cx.listener(Self::select_last))
.on_action(cx.listener(Self::dismiss))
.on_action(cx.listener(Self::confirm))
.on_action(cx.listener(Self::toggle_enable_breakpoint))
.on_action(cx.listener(Self::unset_breakpoint))
.on_action(cx.listener(Self::next_breakpoint_property))
.on_action(cx.listener(Self::previous_breakpoint_property))
.size_full()
.m_0p5()
.child(self.render_list(window, cx))
.children(self.render_vertical_scrollbar(cx))
.child(
v_flex()
.size_full()
.child(self.render_list(cx))
.children(self.render_vertical_scrollbar(cx)),
)
.when_some(self.strip_mode, |this, _| {
this.child(Divider::horizontal()).child(
h_flex()
// .w_full()
.m_0p5()
.p_0p5()
.border_1()
.rounded_sm()
.when(
self.input.focus_handle(cx).contains_focused(window, cx),
|this| {
let colors = cx.theme().colors();
let border = if self.input.read(cx).read_only(cx) {
colors.border_disabled
} else {
colors.border_focused
};
this.border_color(border)
},
)
.child(self.input.clone()),
)
})
}
}
#[derive(Clone, Debug)]
struct LineBreakpoint {
name: SharedString,
@@ -543,7 +780,10 @@ struct LineBreakpoint {
impl LineBreakpoint {
fn render(
&mut self,
props: SupportedBreakpointProperties,
strip_mode: Option<ActiveBreakpointStripMode>,
ix: usize,
is_selected: bool,
focus_handle: FocusHandle,
weak: WeakEntity<BreakpointList>,
) -> ListItem {
@@ -594,15 +834,16 @@ impl LineBreakpoint {
})
.child(Indicator::icon(Icon::new(icon_name)).color(Color::Debugger))
.on_mouse_down(MouseButton::Left, move |_, _, _| {});
ListItem::new(SharedString::from(format!(
"breakpoint-ui-item-{:?}/{}:{}",
self.dir, self.name, self.line
)))
.on_click({
let weak = weak.clone();
move |_, _, cx| {
move |_, window, cx| {
weak.update(cx, |breakpoint_list, cx| {
breakpoint_list.select_ix(Some(ix), cx);
breakpoint_list.select_ix(Some(ix), window, cx);
})
.ok();
}
@@ -613,39 +854,67 @@ impl LineBreakpoint {
cx.stop_propagation();
})
.child(
v_flex()
.py_1()
h_flex()
.w_full()
.mr_4()
.py_0p5()
.gap_1()
.min_h(px(26.))
.justify_center()
.justify_between()
.id(SharedString::from(format!(
"breakpoint-ui-on-click-go-to-line-{:?}/{}:{}",
self.dir, self.name, self.line
)))
.on_click(move |_, window, cx| {
weak.update(cx, |breakpoint_list, cx| {
breakpoint_list.select_ix(Some(ix), cx);
breakpoint_list.go_to_line_breakpoint(path.clone(), row, window, cx);
})
.ok();
.on_click({
let weak = weak.clone();
move |_, window, cx| {
weak.update(cx, |breakpoint_list, cx| {
breakpoint_list.select_ix(Some(ix), window, cx);
breakpoint_list.go_to_line_breakpoint(path.clone(), row, window, cx);
})
.ok();
}
})
.cursor_pointer()
.child(
h_flex()
.gap_1()
.gap_0p5()
.child(
Label::new(format!("{}:{}", self.name, self.line))
.size(LabelSize::Small)
.line_height_style(ui::LineHeightStyle::UiLabel),
)
.children(self.dir.clone().map(|dir| {
Label::new(dir)
.color(Color::Muted)
.size(LabelSize::Small)
.line_height_style(ui::LineHeightStyle::UiLabel)
.children(self.dir.as_ref().and_then(|dir| {
let path_without_root = Path::new(dir.as_ref())
.components()
.skip(1)
.collect::<PathBuf>();
path_without_root.components().next()?;
Some(
Label::new(path_without_root.to_string_lossy().into_owned())
.color(Color::Muted)
.size(LabelSize::Small)
.line_height_style(ui::LineHeightStyle::UiLabel)
.truncate(),
)
})),
),
)
.when_some(self.dir.as_ref(), |this, parent_dir| {
this.tooltip(Tooltip::text(format!("Worktree parent path: {parent_dir}")))
})
.child(BreakpointOptionsStrip {
props,
breakpoint: BreakpointEntry {
kind: BreakpointEntryKind::LineBreakpoint(self.clone()),
weak: weak,
},
is_selected,
focus_handle,
strip_mode,
index: ix,
}),
)
.toggle_state(is_selected)
}
}
#[derive(Clone, Debug)]
@@ -658,7 +927,10 @@ struct ExceptionBreakpoint {
impl ExceptionBreakpoint {
fn render(
&mut self,
props: SupportedBreakpointProperties,
strip_mode: Option<ActiveBreakpointStripMode>,
ix: usize,
is_selected: bool,
focus_handle: FocusHandle,
list: WeakEntity<BreakpointList>,
) -> ListItem {
@@ -669,15 +941,15 @@ impl ExceptionBreakpoint {
};
let id = SharedString::from(&self.id);
let is_enabled = self.is_enabled;
let weak = list.clone();
ListItem::new(SharedString::from(format!(
"exception-breakpoint-ui-item-{}",
self.id
)))
.on_click({
let list = list.clone();
move |_, _, cx| {
list.update(cx, |list, cx| list.select_ix(Some(ix), cx))
move |_, window, cx| {
list.update(cx, |list, cx| list.select_ix(Some(ix), window, cx))
.ok();
}
})
@@ -691,18 +963,21 @@ impl ExceptionBreakpoint {
"exception-breakpoint-ui-item-{}-click-handler",
self.id
)))
.tooltip(move |window, cx| {
Tooltip::for_action_in(
if is_enabled {
"Disable Exception Breakpoint"
} else {
"Enable Exception Breakpoint"
},
&ToggleEnableBreakpoint,
&focus_handle,
window,
cx,
)
.tooltip({
let focus_handle = focus_handle.clone();
move |window, cx| {
Tooltip::for_action_in(
if is_enabled {
"Disable Exception Breakpoint"
} else {
"Enable Exception Breakpoint"
},
&ToggleEnableBreakpoint,
&focus_handle,
window,
cx,
)
}
})
.on_click({
let list = list.clone();
@@ -722,21 +997,40 @@ impl ExceptionBreakpoint {
.child(Indicator::icon(Icon::new(IconName::Flame)).color(color)),
)
.child(
v_flex()
.py_1()
.gap_1()
.min_h(px(26.))
.justify_center()
.id(("exception-breakpoint-label", ix))
h_flex()
.w_full()
.mr_4()
.py_0p5()
.justify_between()
.child(
Label::new(self.data.label.clone())
.size(LabelSize::Small)
.line_height_style(ui::LineHeightStyle::UiLabel),
v_flex()
.py_1()
.gap_1()
.min_h(px(26.))
.justify_center()
.id(("exception-breakpoint-label", ix))
.child(
Label::new(self.data.label.clone())
.size(LabelSize::Small)
.line_height_style(ui::LineHeightStyle::UiLabel),
)
.when_some(self.data.description.clone(), |el, description| {
el.tooltip(Tooltip::text(description))
}),
)
.when_some(self.data.description.clone(), |el, description| {
el.tooltip(Tooltip::text(description))
.child(BreakpointOptionsStrip {
props,
breakpoint: BreakpointEntry {
kind: BreakpointEntryKind::ExceptionBreakpoint(self.clone()),
weak: weak,
},
is_selected,
focus_handle,
strip_mode,
index: ix,
}),
)
.toggle_state(is_selected)
}
}
#[derive(Clone, Debug)]
@@ -754,18 +1048,267 @@ struct BreakpointEntry {
impl BreakpointEntry {
fn render(
&mut self,
strip_mode: Option<ActiveBreakpointStripMode>,
props: SupportedBreakpointProperties,
ix: usize,
is_selected: bool,
focus_handle: FocusHandle,
_: &mut Window,
_: &mut App,
) -> ListItem {
match &mut self.kind {
BreakpointEntryKind::LineBreakpoint(line_breakpoint) => line_breakpoint.render(
props,
strip_mode,
ix,
is_selected,
focus_handle,
self.weak.clone(),
),
BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => exception_breakpoint
.render(
props.for_exception_breakpoints(),
strip_mode,
ix,
is_selected,
focus_handle,
self.weak.clone(),
),
}
}
fn id(&self) -> SharedString {
match &self.kind {
BreakpointEntryKind::LineBreakpoint(line_breakpoint) => format!(
"source-breakpoint-control-strip-{:?}:{}",
line_breakpoint.breakpoint.path, line_breakpoint.breakpoint.row
)
.into(),
BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => format!(
"exception-breakpoint-control-strip--{}",
exception_breakpoint.id
)
.into(),
}
}
fn has_log(&self) -> bool {
match &self.kind {
BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
line_breakpoint.render(ix, focus_handle, self.weak.clone())
line_breakpoint.breakpoint.message.is_some()
}
BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => {
exception_breakpoint.render(ix, focus_handle, self.weak.clone())
_ => false,
}
}
fn has_condition(&self) -> bool {
match &self.kind {
BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
line_breakpoint.breakpoint.condition.is_some()
}
// We don't support conditions on exception breakpoints
BreakpointEntryKind::ExceptionBreakpoint(_) => false,
}
}
fn has_hit_condition(&self) -> bool {
match &self.kind {
BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
line_breakpoint.breakpoint.hit_condition.is_some()
}
_ => false,
}
}
}
bitflags::bitflags! {
#[derive(Clone, Copy)]
pub struct SupportedBreakpointProperties: u32 {
const LOG = 1 << 0;
const CONDITION = 1 << 1;
const HIT_CONDITION = 1 << 2;
// Conditions for exceptions can be set only when exception filters are supported.
const EXCEPTION_FILTER_OPTIONS = 1 << 3;
}
}
impl From<&Capabilities> for SupportedBreakpointProperties {
fn from(caps: &Capabilities) -> Self {
let mut this = Self::empty();
for (prop, offset) in [
(caps.supports_log_points, Self::LOG),
(caps.supports_conditional_breakpoints, Self::CONDITION),
(
caps.supports_hit_conditional_breakpoints,
Self::HIT_CONDITION,
),
(
caps.supports_exception_options,
Self::EXCEPTION_FILTER_OPTIONS,
),
] {
if prop.unwrap_or_default() {
this.insert(offset);
}
}
this
}
}
impl SupportedBreakpointProperties {
fn for_exception_breakpoints(self) -> Self {
// TODO: we don't yet support conditions for exception breakpoints at the data layer, hence all props are disabled here.
Self::empty()
}
}
#[derive(IntoElement)]
struct BreakpointOptionsStrip {
props: SupportedBreakpointProperties,
breakpoint: BreakpointEntry,
is_selected: bool,
focus_handle: FocusHandle,
strip_mode: Option<ActiveBreakpointStripMode>,
index: usize,
}
impl BreakpointOptionsStrip {
fn is_toggled(&self, expected_mode: ActiveBreakpointStripMode) -> bool {
self.is_selected && self.strip_mode == Some(expected_mode)
}
fn on_click_callback(
&self,
mode: ActiveBreakpointStripMode,
) -> impl for<'a> Fn(&ClickEvent, &mut Window, &'a mut App) + use<> {
let list = self.breakpoint.weak.clone();
let ix = self.index;
move |_, window, cx| {
list.update(cx, |this, cx| {
if this.strip_mode != Some(mode) {
this.set_active_breakpoint_property(mode, window, cx);
} else if this.selected_ix == Some(ix) {
this.strip_mode.take();
} else {
cx.propagate();
}
})
.ok();
}
}
fn add_border(
&self,
kind: ActiveBreakpointStripMode,
available: bool,
window: &Window,
cx: &App,
) -> impl Fn(Div) -> Div {
move |this: Div| {
// Avoid layout shifts in case there's no colored border
let this = this.border_2().rounded_sm();
if self.is_selected && self.strip_mode == Some(kind) {
let theme = cx.theme().colors();
if self.focus_handle.is_focused(window) {
this.border_color(theme.border_selected)
} else {
this.border_color(theme.border_disabled)
}
} else if !available {
this.border_color(cx.theme().colors().border_disabled)
} else {
this
}
}
}
}
impl RenderOnce for BreakpointOptionsStrip {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let id = self.breakpoint.id();
let supports_logs = self.props.contains(SupportedBreakpointProperties::LOG);
let supports_condition = self
.props
.contains(SupportedBreakpointProperties::CONDITION);
let supports_hit_condition = self
.props
.contains(SupportedBreakpointProperties::HIT_CONDITION);
let has_logs = self.breakpoint.has_log();
let has_condition = self.breakpoint.has_condition();
let has_hit_condition = self.breakpoint.has_hit_condition();
let style_for_toggle = |mode, is_enabled| {
if is_enabled && self.strip_mode == Some(mode) && self.is_selected {
ui::ButtonStyle::Filled
} else {
ui::ButtonStyle::Subtle
}
};
let color_for_toggle = |is_enabled| {
if is_enabled {
ui::Color::Default
} else {
ui::Color::Muted
}
};
h_flex()
.gap_1()
.child(
div().map(self.add_border(ActiveBreakpointStripMode::Log, supports_logs, window, cx))
.child(
IconButton::new(
SharedString::from(format!("{id}-log-toggle")),
IconName::ScrollText,
)
.icon_size(IconSize::XSmall)
.style(style_for_toggle(ActiveBreakpointStripMode::Log, has_logs))
.icon_color(color_for_toggle(has_logs))
.disabled(!supports_logs)
.toggle_state(self.is_toggled(ActiveBreakpointStripMode::Log))
.on_click(self.on_click_callback(ActiveBreakpointStripMode::Log)).tooltip(|window, cx| Tooltip::with_meta("Set Log Message", None, "Set log message to display (instead of stopping) when a breakpoint is hit", window, cx))
)
.when(!has_logs && !self.is_selected, |this| this.invisible()),
)
.child(
div().map(self.add_border(
ActiveBreakpointStripMode::Condition,
supports_condition,
window, cx
))
.child(
IconButton::new(
SharedString::from(format!("{id}-condition-toggle")),
IconName::SplitAlt,
)
.icon_size(IconSize::XSmall)
.style(style_for_toggle(
ActiveBreakpointStripMode::Condition,
has_condition
))
.icon_color(color_for_toggle(has_condition))
.disabled(!supports_condition)
.toggle_state(self.is_toggled(ActiveBreakpointStripMode::Condition))
.on_click(self.on_click_callback(ActiveBreakpointStripMode::Condition))
.tooltip(|window, cx| Tooltip::with_meta("Set Condition", None, "Set condition to evaluate when a breakpoint is hit. Program execution will stop only when the condition is met", window, cx))
)
.when(!has_condition && !self.is_selected, |this| this.invisible()),
)
.child(
div().map(self.add_border(
ActiveBreakpointStripMode::HitCondition,
supports_hit_condition,window, cx
))
.child(
IconButton::new(
SharedString::from(format!("{id}-hit-condition-toggle")),
IconName::ArrowDown10,
)
.icon_size(IconSize::XSmall)
.style(style_for_toggle(
ActiveBreakpointStripMode::HitCondition,
has_hit_condition,
))
.icon_color(color_for_toggle(has_hit_condition))
.disabled(!supports_hit_condition)
.toggle_state(self.is_toggled(ActiveBreakpointStripMode::HitCondition))
.on_click(self.on_click_callback(ActiveBreakpointStripMode::HitCondition)).tooltip(|window, cx| Tooltip::with_meta("Set Hit Condition", None, "Set expression that controls how many hits of the breakpoint are ignored.", window, cx))
)
.when(!has_hit_condition && !self.is_selected, |this| {
this.invisible()
}),
)
}
}

View File

@@ -114,7 +114,7 @@ impl Console {
}
fn is_running(&self, cx: &Context<Self>) -> bool {
self.session.read(cx).is_running()
self.session.read(cx).is_started()
}
fn handle_stack_frame_list_events(
@@ -582,14 +582,31 @@ impl CompletionProvider for ConsoleQueryBarCompletionProvider {
fn is_completion_trigger(
&self,
_buffer: &Entity<Buffer>,
_position: language::Anchor,
_text: &str,
buffer: &Entity<Buffer>,
position: language::Anchor,
text: &str,
_trigger_in_words: bool,
_menu_is_open: bool,
_cx: &mut Context<Editor>,
menu_is_open: bool,
cx: &mut Context<Editor>,
) -> bool {
true
let snapshot = buffer.read(cx).snapshot();
if !menu_is_open && !snapshot.settings_at(position, cx).show_completions_on_input {
return false;
}
self.0
.read_with(cx, |console, cx| {
console
.session
.read(cx)
.capabilities()
.completion_trigger_characters
.as_ref()
.map(|triggers| triggers.contains(&text.to_string()))
})
.ok()
.flatten()
.unwrap_or(true)
}
}
@@ -629,8 +646,23 @@ impl ConsoleQueryBarCompletionProvider {
(variables, string_matches)
});
let query = buffer.read(cx).text();
let snapshot = buffer.read(cx).text_snapshot();
let query = snapshot.text();
let replace_range = {
let buffer_offset = buffer_position.to_offset(&snapshot);
let reversed_chars = snapshot.reversed_chars_for_range(0..buffer_offset);
let mut word_len = 0;
for ch in reversed_chars {
if ch.is_alphanumeric() || ch == '_' {
word_len += 1;
} else {
break;
}
}
let word_start_offset = buffer_offset - word_len;
let start_anchor = snapshot.anchor_at(word_start_offset, Bias::Left);
start_anchor..buffer_position
};
cx.spawn(async move |_, cx| {
const LIMIT: usize = 10;
let matches = fuzzy::match_strings(
@@ -650,7 +682,7 @@ impl ConsoleQueryBarCompletionProvider {
let variable_value = variables.get(&string_match.string)?;
Some(project::Completion {
replace_range: buffer_position..buffer_position,
replace_range: replace_range.clone(),
new_text: string_match.string.clone(),
label: CodeLabel {
filter_range: 0..string_match.string.len(),

View File

@@ -4,7 +4,7 @@ use collections::HashMap;
use dap::StackFrameId;
use editor::{
Anchor, Bias, DebugStackFrameLine, Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer,
RowHighlightOptions, ToPoint, scroll::Autoscroll,
RowHighlightOptions, SelectionEffects, ToPoint, scroll::Autoscroll,
};
use gpui::{
AnyView, App, AppContext, Entity, EventEmitter, Focusable, IntoElement, Render, SharedString,
@@ -99,10 +99,11 @@ impl StackTraceView {
if frame_anchor.excerpt_id
!= editor.selections.newest_anchor().head().excerpt_id
{
let auto_scroll =
Some(Autoscroll::center().for_anchor(frame_anchor));
let effects = SelectionEffects::scroll(
Autoscroll::center().for_anchor(frame_anchor),
);
editor.change_selections(auto_scroll, window, cx, |selections| {
editor.change_selections(effects, window, cx, |selections| {
let selection_id = selections.new_selection_id();
let selection = Selection {

View File

@@ -37,15 +37,23 @@ async fn test_dap_logger_captures_all_session_rpc_messages(
.await;
assert!(
log_store.read_with(cx, |log_store, _| log_store
.contained_session_ids()
.is_empty()),
"log_store shouldn't contain any session IDs before any sessions were created"
log_store.read_with(cx, |log_store, _| !log_store.has_projects()),
"log_store shouldn't contain any projects before any projects were created"
);
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
let workspace = init_test_workspace(&project, cx).await;
assert!(
log_store.read_with(cx, |log_store, _| log_store.has_projects()),
"log_store shouldn't contain any projects before any projects were created"
);
assert!(
log_store.read_with(cx, |log_store, _| log_store
.contained_session_ids(&project.downgrade())
.is_empty()),
"log_store shouldn't contain any projects before any projects were created"
);
let cx = &mut VisualTestContext::from_window(*workspace, cx);
// Start a debug session
@@ -54,20 +62,22 @@ async fn test_dap_logger_captures_all_session_rpc_messages(
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
assert_eq!(
log_store.read_with(cx, |log_store, _| log_store.contained_session_ids().len()),
log_store.read_with(cx, |log_store, _| log_store
.contained_session_ids(&project.downgrade())
.len()),
1,
);
assert!(
log_store.read_with(cx, |log_store, _| log_store
.contained_session_ids()
.contained_session_ids(&project.downgrade())
.contains(&session_id)),
"log_store should contain the session IDs of the started session"
);
assert!(
!log_store.read_with(cx, |log_store, _| log_store
.rpc_messages_for_session_id(session_id)
.rpc_messages_for_session_id(&project.downgrade(), session_id)
.is_empty()),
"We should have the initialization sequence in the log store"
);

View File

@@ -246,10 +246,10 @@ fn main() {
editor.update_in(cx, |editor, window, cx| {
pretty_assertions::assert_eq!(
r#"
static mut GLOBAL: 1: usize = 1;
static mut GLOBAL: usize = 1;
fn main() {
let x = 10;
let x: 10 = 10;
let value = 42;
let y = 4;
let tester = {
@@ -303,11 +303,11 @@ fn main() {
editor.update_in(cx, |editor, window, cx| {
pretty_assertions::assert_eq!(
r#"
static mut GLOBAL: 1: usize = 1;
static mut GLOBAL: usize = 1;
fn main() {
let x: 10 = 10;
let value = 42;
let value: 42 = 42;
let y = 4;
let tester = {
let y = 10;
@@ -360,12 +360,12 @@ fn main() {
editor.update_in(cx, |editor, window, cx| {
pretty_assertions::assert_eq!(
r#"
static mut GLOBAL: 1: usize = 1;
static mut GLOBAL: usize = 1;
fn main() {
let x: 10 = 10;
let value: 42 = 42;
let y = 4;
let y: 4 = 4;
let tester = {
let y = 10;
let y = 5;
@@ -417,7 +417,7 @@ fn main() {
editor.update_in(cx, |editor, window, cx| {
pretty_assertions::assert_eq!(
r#"
static mut GLOBAL: 1: usize = 1;
static mut GLOBAL: usize = 1;
fn main() {
let x: 10 = 10;
@@ -474,14 +474,14 @@ fn main() {
editor.update_in(cx, |editor, window, cx| {
pretty_assertions::assert_eq!(
r#"
static mut GLOBAL: 1: usize = 1;
static mut GLOBAL: usize = 1;
fn main() {
let x: 10 = 10;
let value: 42 = 42;
let y: 4 = 4;
let tester = {
let y = 10;
let y: 4 = 10;
let y = 5;
let b = 3;
vec![y, 20, 30]
@@ -581,15 +581,15 @@ fn main() {
editor.update_in(cx, |editor, window, cx| {
pretty_assertions::assert_eq!(
r#"
static mut GLOBAL: 1: usize = 1;
static mut GLOBAL: usize = 1;
fn main() {
let x: 10 = 10;
let value: 42 = 42;
let y = 4;
let y: 10 = 4;
let tester = {
let y: 10 = 10;
let y = 5;
let y: 10 = 5;
let b = 3;
vec![y, 20, 30]
};
@@ -688,14 +688,14 @@ fn main() {
editor.update_in(cx, |editor, window, cx| {
pretty_assertions::assert_eq!(
r#"
static mut GLOBAL: 1: usize = 1;
static mut GLOBAL: usize = 1;
fn main() {
let x: 10 = 10;
let value: 42 = 42;
let y = 4;
let y: 5 = 4;
let tester = {
let y = 10;
let y: 5 = 10;
let y: 5 = 5;
let b = 3;
vec![y, 20, 30]
@@ -807,17 +807,17 @@ fn main() {
editor.update_in(cx, |editor, window, cx| {
pretty_assertions::assert_eq!(
r#"
static mut GLOBAL: 1: usize = 1;
static mut GLOBAL: usize = 1;
fn main() {
let x: 10 = 10;
let value: 42 = 42;
let y = 4;
let y: 5 = 4;
let tester = {
let y = 10;
let y: 5 = 10;
let y: 5 = 5;
let b: 3 = 3;
vec![y, 20, 30]
vec![y: 5, 20, 30]
};
let caller = || {
@@ -926,7 +926,7 @@ fn main() {
editor.update_in(cx, |editor, window, cx| {
pretty_assertions::assert_eq!(
r#"
static mut GLOBAL: 1: usize = 1;
static mut GLOBAL: usize = 1;
fn main() {
let x: 10 = 10;
@@ -1058,7 +1058,7 @@ fn main() {
editor.update_in(cx, |editor, window, cx| {
pretty_assertions::assert_eq!(
r#"
static mut GLOBAL: 1: usize = 1;
static mut GLOBAL: usize = 1;
fn main() {
let x: 10 = 10;
@@ -1115,21 +1115,21 @@ fn main() {
editor.update_in(cx, |editor, window, cx| {
pretty_assertions::assert_eq!(
r#"
static mut GLOBAL: 1: usize = 1;
static mut GLOBAL: usize = 1;
fn main() {
let x = 10;
let value = 42;
let y = 4;
let tester = {
let x: 10 = 10;
let value: 42 = 42;
let y: 4 = 4;
let tester: size=3 = {
let y = 10;
let y = 5;
let b = 3;
vec![y, 20, 30]
};
let caller = || {
let x = 3;
let caller: <not available> = || {
let x: 10 = 3;
println!("x={}", x);
};
@@ -1193,10 +1193,10 @@ fn main() {
editor.update_in(cx, |editor, window, cx| {
pretty_assertions::assert_eq!(
r#"
static mut GLOBAL: 1: usize = 1;
static mut GLOBAL: usize = 1;
fn main() {
let x = 10;
let x: 3 = 10;
let value = 42;
let y = 4;
let tester = {
@@ -1208,7 +1208,7 @@ fn main() {
let caller = || {
let x: 3 = 3;
println!("x={}", x);
println!("x={}", x: 3);
};
caller();
@@ -1338,7 +1338,7 @@ fn main() {
editor.update_in(cx, |editor, window, cx| {
pretty_assertions::assert_eq!(
r#"
static mut GLOBAL: 2: usize = 1;
static mut GLOBAL: usize = 1;
fn main() {
let x: 10 = 10;
@@ -1362,7 +1362,7 @@ fn main() {
GLOBAL = 2;
}
let result = value * 2 * x;
let result = value: 42 * 2 * x: 10;
println!("Simple test executed: value={}, result={}", value, result);
assert!(true);
}
@@ -1483,7 +1483,7 @@ fn main() {
editor.update_in(cx, |editor, window, cx| {
pretty_assertions::assert_eq!(
r#"
static mut GLOBAL: 2: usize = 1;
static mut GLOBAL: usize = 1;
fn main() {
let x: 10 = 10;
@@ -1507,8 +1507,8 @@ fn main() {
GLOBAL = 2;
}
let result: 840 = value * 2 * x;
println!("Simple test executed: value={}, result={}", value, result);
let result: 840 = value: 42 * 2 * x: 10;
println!("Simple test executed: value={}, result={}", value: 42, result: 840);
assert!(true);
}
"#
@@ -1519,6 +1519,7 @@ fn main() {
}
fn rust_lang() -> Language {
let debug_variables_query = include_str!("../../../languages/src/rust/debugger.scm");
Language::new(
LanguageConfig {
name: "Rust".into(),
@@ -1530,6 +1531,8 @@ fn rust_lang() -> Language {
},
Some(tree_sitter_rust::LANGUAGE.into()),
)
.with_debug_variables_query(debug_variables_query)
.unwrap()
}
#[gpui::test]
@@ -1818,8 +1821,8 @@ def process_data(untyped_param, typed_param: int, another_typed: str):
def process_data(untyped_param: test_value, typed_param: 42: int, another_typed: world: str):
# Local variables
x: 10 = 10
result: 84 = typed_param * 2
text: Hello, world = "Hello, " + another_typed
result: 84 = typed_param: 42 * 2
text: Hello, world = "Hello, " + another_typed: world
# For loop with range
sum_value: 10 = 0
@@ -1837,6 +1840,7 @@ def process_data(untyped_param, typed_param: int, another_typed: str):
}
fn python_lang() -> Language {
let debug_variables_query = include_str!("../../../languages/src/python/debugger.scm");
Language::new(
LanguageConfig {
name: "Python".into(),
@@ -1848,4 +1852,392 @@ fn python_lang() -> Language {
},
Some(tree_sitter_python::LANGUAGE.into()),
)
.with_debug_variables_query(debug_variables_query)
.unwrap()
}
fn go_lang() -> Language {
let debug_variables_query = include_str!("../../../languages/src/go/debugger.scm");
Language::new(
LanguageConfig {
name: "Go".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["go".to_string()],
..Default::default()
},
..Default::default()
},
Some(tree_sitter_go::LANGUAGE.into()),
)
.with_debug_variables_query(debug_variables_query)
.unwrap()
}
/// Test utility function for inline values testing
///
/// # Arguments
/// * `variables` - List of tuples containing (variable_name, variable_value)
/// * `before` - Source code before inline values are applied
/// * `after` - Expected source code after inline values are applied
/// * `language` - Language configuration to use for parsing
/// * `executor` - Background executor for async operations
/// * `cx` - Test app context
async fn test_inline_values_util(
local_variables: &[(&str, &str)],
global_variables: &[(&str, &str)],
before: &str,
after: &str,
active_debug_line: Option<usize>,
language: Language,
executor: BackgroundExecutor,
cx: &mut TestAppContext,
) {
init_test(cx);
let lines_count = before.lines().count();
let stop_line =
active_debug_line.unwrap_or_else(|| if lines_count > 6 { 6 } else { lines_count - 1 });
let fs = FakeFs::new(executor.clone());
fs.insert_tree(path!("/project"), json!({ "main.rs": before.to_string() }))
.await;
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
let workspace = init_test_workspace(&project, cx).await;
workspace
.update(cx, |workspace, window, cx| {
workspace.focus_panel::<DebugPanel>(window, cx);
})
.unwrap();
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::<dap::requests::Threads, _>(|_, _| {
Ok(dap::ThreadsResponse {
threads: vec![dap::Thread {
id: 1,
name: "main".into(),
}],
})
});
client.on_request::<dap::requests::StackTrace, _>(move |_, _| {
Ok(dap::StackTraceResponse {
stack_frames: vec![dap::StackFrame {
id: 1,
name: "main".into(),
source: Some(dap::Source {
name: Some("main.rs".into()),
path: Some(path!("/project/main.rs").into()),
source_reference: None,
presentation_hint: None,
origin: None,
sources: None,
adapter_data: None,
checksums: None,
}),
line: stop_line as u64,
column: 1,
end_line: None,
end_column: None,
can_restart: None,
instruction_pointer_reference: None,
module_id: None,
presentation_hint: None,
}],
total_frames: None,
})
});
let local_vars: Vec<Variable> = local_variables
.iter()
.map(|(name, value)| Variable {
name: (*name).into(),
value: (*value).into(),
type_: None,
presentation_hint: None,
evaluate_name: None,
variables_reference: 0,
named_variables: None,
indexed_variables: None,
memory_reference: None,
declaration_location_reference: None,
value_location_reference: None,
})
.collect();
let global_vars: Vec<Variable> = global_variables
.iter()
.map(|(name, value)| Variable {
name: (*name).into(),
value: (*value).into(),
type_: None,
presentation_hint: None,
evaluate_name: None,
variables_reference: 0,
named_variables: None,
indexed_variables: None,
memory_reference: None,
declaration_location_reference: None,
value_location_reference: None,
})
.collect();
client.on_request::<Variables, _>({
let local_vars = Arc::new(local_vars.clone());
let global_vars = Arc::new(global_vars.clone());
move |_, args| {
let variables = match args.variables_reference {
2 => (*local_vars).clone(),
3 => (*global_vars).clone(),
_ => vec![],
};
Ok(dap::VariablesResponse { variables })
}
});
client.on_request::<dap::requests::Scopes, _>(move |_, _| {
Ok(dap::ScopesResponse {
scopes: vec![
Scope {
name: "Local".into(),
presentation_hint: None,
variables_reference: 2,
named_variables: None,
indexed_variables: None,
expensive: false,
source: None,
line: None,
column: None,
end_line: None,
end_column: None,
},
Scope {
name: "Global".into(),
presentation_hint: None,
variables_reference: 3,
named_variables: None,
indexed_variables: None,
expensive: false,
source: None,
line: None,
column: None,
end_line: None,
end_column: None,
},
],
})
});
if !global_variables.is_empty() {
let global_evaluate_map: std::collections::HashMap<String, String> = global_variables
.iter()
.map(|(name, value)| (name.to_string(), value.to_string()))
.collect();
client.on_request::<dap::requests::Evaluate, _>(move |_, args| {
let value = global_evaluate_map
.get(&args.expression)
.unwrap_or(&"undefined".to_string())
.clone();
Ok(dap::EvaluateResponse {
result: value,
type_: None,
presentation_hint: None,
variables_reference: 0,
named_variables: None,
indexed_variables: None,
memory_reference: None,
value_location_reference: 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();
let project_path = Path::new(path!("/project"));
let worktree = project
.update(cx, |project, cx| project.find_worktree(project_path, cx))
.expect("This worktree should exist in project")
.0;
let worktree_id = workspace
.update(cx, |_, _, cx| worktree.read(cx).id())
.unwrap();
let buffer = project
.update(cx, |project, cx| {
project.open_buffer((worktree_id, "main.rs"), cx)
})
.await
.unwrap();
buffer.update(cx, |buffer, cx| {
buffer.set_language(Some(Arc::new(language)), cx);
});
let (editor, cx) = cx.add_window_view(|window, cx| {
Editor::new(
EditorMode::full(),
MultiBuffer::build_from_buffer(buffer, cx),
Some(project),
window,
cx,
)
});
active_debug_session_panel(workspace, cx).update_in(cx, |_, window, cx| {
cx.focus_self(window);
});
cx.run_until_parked();
editor.update(cx, |editor, cx| editor.refresh_inline_values(cx));
cx.run_until_parked();
editor.update_in(cx, |editor, window, cx| {
pretty_assertions::assert_eq!(after, editor.snapshot(window, cx).text());
});
}
#[gpui::test]
async fn test_inline_values_example(executor: BackgroundExecutor, cx: &mut TestAppContext) {
let variables = [("x", "10"), ("y", "20"), ("result", "30")];
let before = r#"
fn main() {
let x = 10;
let y = 20;
let result = x + y;
println!("Result: {}", result);
}
"#
.unindent();
let after = r#"
fn main() {
let x: 10 = 10;
let y: 20 = 20;
let result: 30 = x: 10 + y: 20;
println!("Result: {}", result: 30);
}
"#
.unindent();
test_inline_values_util(
&variables,
&[],
&before,
&after,
None,
rust_lang(),
executor,
cx,
)
.await;
}
#[gpui::test]
async fn test_inline_values_with_globals(executor: BackgroundExecutor, cx: &mut TestAppContext) {
let variables = [("x", "5"), ("y", "10")];
let before = r#"
static mut GLOBAL_COUNTER: usize = 42;
fn main() {
let x = 5;
let y = 10;
unsafe {
GLOBAL_COUNTER += 1;
}
println!("x={}, y={}, global={}", x, y, unsafe { GLOBAL_COUNTER });
}
"#
.unindent();
let after = r#"
static mut GLOBAL_COUNTER: 42: usize = 42;
fn main() {
let x: 5 = 5;
let y: 10 = 10;
unsafe {
GLOBAL_COUNTER += 1;
}
println!("x={}, y={}, global={}", x, y, unsafe { GLOBAL_COUNTER });
}
"#
.unindent();
test_inline_values_util(
&variables,
&[("GLOBAL_COUNTER", "42")],
&before,
&after,
None,
rust_lang(),
executor,
cx,
)
.await;
}
#[gpui::test]
async fn test_go_inline_values(executor: BackgroundExecutor, cx: &mut TestAppContext) {
let variables = [("x", "42"), ("y", "hello")];
let before = r#"
package main
var globalCounter int = 100
func main() {
x := 42
y := "hello"
z := x + 10
println(x, y, z)
}
"#
.unindent();
let after = r#"
package main
var globalCounter: 100 int = 100
func main() {
x: 42 := 42
y := "hello"
z := x + 10
println(x, y, z)
}
"#
.unindent();
test_inline_values_util(
&variables,
&[("globalCounter", "100")],
&before,
&after,
None,
go_lang(),
executor,
cx,
)
.await;
}

View File

@@ -267,7 +267,6 @@ async fn test_dap_adapter_config_conversion_and_validation(cx: &mut TestAppConte
"Debugpy",
"PHP",
"JavaScript",
"Ruby",
"Delve",
"GDB",
"fake-adapter",

View File

@@ -201,13 +201,13 @@ pub struct Response {
#[derive(Serialize, Deserialize, Debug)]
pub struct Usage {
pub prompt_tokens: u32,
pub completion_tokens: u32,
pub total_tokens: u32,
pub prompt_tokens: u64,
pub completion_tokens: u64,
pub total_tokens: u64,
#[serde(default)]
pub prompt_cache_hit_tokens: u32,
pub prompt_cache_hit_tokens: u64,
#[serde(default)]
pub prompt_cache_miss_tokens: u32,
pub prompt_cache_miss_tokens: u64,
}
#[derive(Serialize, Deserialize, Debug)]
@@ -224,6 +224,7 @@ pub struct StreamResponse {
pub created: u64,
pub model: String,
pub choices: Vec<StreamChoice>,
pub usage: Option<Usage>,
}
#[derive(Serialize, Deserialize, Debug)]

View File

@@ -4,7 +4,6 @@ use editor::{
Anchor, Editor, EditorSnapshot, ToOffset,
display_map::{BlockContext, BlockPlacement, BlockProperties, BlockStyle},
hover_popover::diagnostics_markdown_style,
scroll::Autoscroll,
};
use gpui::{AppContext, Entity, Focusable, WeakEntity};
use language::{BufferId, Diagnostic, DiagnosticEntry};
@@ -311,7 +310,7 @@ impl DiagnosticBlock {
let range = range.start.to_offset(&snapshot)..range.end.to_offset(&snapshot);
editor.unfold_ranges(&[range.start..range.end], true, false, cx);
editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
editor.change_selections(Default::default(), window, cx, |s| {
s.select_ranges([range.start..range.start]);
});
window.focus(&editor.focus_handle(cx));

View File

@@ -12,7 +12,6 @@ use diagnostic_renderer::DiagnosticBlock;
use editor::{
DEFAULT_MULTIBUFFER_CONTEXT, Editor, EditorEvent, ExcerptRange, MultiBuffer, PathKey,
display_map::{BlockPlacement, BlockProperties, BlockStyle, CustomBlockId},
scroll::Autoscroll,
};
use futures::future::join_all;
use gpui::{
@@ -626,7 +625,7 @@ impl ProjectDiagnosticsEditor {
if let Some(anchor_range) = anchor_ranges.first() {
let range_to_select = anchor_range.start..anchor_range.start;
this.editor.update(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
editor.change_selections(Default::default(), window, cx, |s| {
s.select_anchor_ranges([range_to_select]);
})
});

View File

@@ -270,6 +270,8 @@ actions!(
ContextMenuLast,
ContextMenuNext,
ContextMenuPrevious,
ConvertIndentationToSpaces,
ConvertIndentationToTabs,
ConvertToKebabCase,
ConvertToLowerCamelCase,
ConvertToLowerCase,
@@ -386,7 +388,6 @@ actions!(
RestartLanguageServer,
RevealInFileManager,
ReverseLines,
RevertFile,
ReloadFile,
Rewrap,
RunFlycheck,

View File

@@ -1205,7 +1205,7 @@ impl CodeActionContents {
tasks_len + code_actions_len + self.debug_scenarios.len()
}
fn is_empty(&self) -> bool {
pub fn is_empty(&self) -> bool {
self.len() == 0
}

View File

@@ -37,7 +37,9 @@ pub use block_map::{
use block_map::{BlockRow, BlockSnapshot};
use collections::{HashMap, HashSet};
pub use crease_map::*;
pub use fold_map::{ChunkRenderer, ChunkRendererContext, Fold, FoldId, FoldPlaceholder, FoldPoint};
pub use fold_map::{
ChunkRenderer, ChunkRendererContext, ChunkRendererId, Fold, FoldId, FoldPlaceholder, FoldPoint,
};
use fold_map::{FoldMap, FoldSnapshot};
use gpui::{App, Context, Entity, Font, HighlightStyle, LineLayout, Pixels, UnderlineStyle};
pub use inlay_map::Inlay;
@@ -538,7 +540,7 @@ impl DisplayMap {
pub fn update_fold_widths(
&mut self,
widths: impl IntoIterator<Item = (FoldId, Pixels)>,
widths: impl IntoIterator<Item = (ChunkRendererId, Pixels)>,
cx: &mut Context<Self>,
) -> bool {
let snapshot = self.buffer.read(cx).snapshot(cx);
@@ -966,10 +968,22 @@ impl DisplaySnapshot {
.and_then(|id| id.style(&editor_style.syntax));
if let Some(chunk_highlight) = chunk.highlight_style {
// For color inlays, blend the color with the editor background
let mut processed_highlight = chunk_highlight;
if chunk.is_inlay {
if let Some(inlay_color) = chunk_highlight.color {
// Only blend if the color has transparency (alpha < 1.0)
if inlay_color.a < 1.0 {
let blended_color = editor_style.background.blend(inlay_color);
processed_highlight.color = Some(blended_color);
}
}
}
if let Some(highlight_style) = highlight_style.as_mut() {
highlight_style.highlight(chunk_highlight);
highlight_style.highlight(processed_highlight);
} else {
highlight_style = Some(chunk_highlight);
highlight_style = Some(processed_highlight);
}
}

View File

@@ -1,3 +1,5 @@
use crate::{InlayId, display_map::inlay_map::InlayChunk};
use super::{
Highlights,
inlay_map::{InlayBufferRows, InlayChunks, InlayEdit, InlayOffset, InlayPoint, InlaySnapshot},
@@ -275,13 +277,16 @@ impl FoldMapWriter<'_> {
pub(crate) fn update_fold_widths(
&mut self,
new_widths: impl IntoIterator<Item = (FoldId, Pixels)>,
new_widths: impl IntoIterator<Item = (ChunkRendererId, Pixels)>,
) -> (FoldSnapshot, Vec<FoldEdit>) {
let mut edits = Vec::new();
let inlay_snapshot = self.0.snapshot.inlay_snapshot.clone();
let buffer = &inlay_snapshot.buffer;
for (id, new_width) in new_widths {
let ChunkRendererId::Fold(id) = id else {
continue;
};
if let Some(metadata) = self.0.snapshot.fold_metadata_by_id.get(&id).cloned() {
if Some(new_width) != metadata.width {
let buffer_start = metadata.range.start.to_offset(buffer);
@@ -527,7 +532,7 @@ impl FoldMap {
placeholder: Some(TransformPlaceholder {
text: ELLIPSIS,
renderer: ChunkRenderer {
id: fold.id,
id: ChunkRendererId::Fold(fold.id),
render: Arc::new(move |cx| {
(fold.placeholder.render)(
fold_id,
@@ -1060,7 +1065,7 @@ impl sum_tree::Summary for TransformSummary {
}
#[derive(Copy, Clone, Eq, PartialEq, Debug, Default, Ord, PartialOrd, Hash)]
pub struct FoldId(usize);
pub struct FoldId(pub(super) usize);
impl From<FoldId> for ElementId {
fn from(val: FoldId) -> Self {
@@ -1265,11 +1270,17 @@ pub struct Chunk<'a> {
pub renderer: Option<ChunkRenderer>,
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum ChunkRendererId {
Fold(FoldId),
Inlay(InlayId),
}
/// A recipe for how the chunk should be presented.
#[derive(Clone)]
pub struct ChunkRenderer {
/// The id of the fold associated with this chunk.
pub id: FoldId,
/// The id of the renderer associated with this chunk.
pub id: ChunkRendererId,
/// Creates a custom element to represent this chunk.
pub render: Arc<dyn Send + Sync + Fn(&mut ChunkRendererContext) -> AnyElement>,
/// If true, the element is constrained to the shaped width of the text.
@@ -1311,7 +1322,7 @@ impl DerefMut for ChunkRendererContext<'_, '_> {
pub struct FoldChunks<'a> {
transform_cursor: Cursor<'a, Transform, (FoldOffset, InlayOffset)>,
inlay_chunks: InlayChunks<'a>,
inlay_chunk: Option<(InlayOffset, language::Chunk<'a>)>,
inlay_chunk: Option<(InlayOffset, InlayChunk<'a>)>,
inlay_offset: InlayOffset,
output_offset: FoldOffset,
max_output_offset: FoldOffset,
@@ -1403,7 +1414,8 @@ impl<'a> Iterator for FoldChunks<'a> {
}
// Otherwise, take a chunk from the buffer's text.
if let Some((buffer_chunk_start, mut chunk)) = self.inlay_chunk.clone() {
if let Some((buffer_chunk_start, mut inlay_chunk)) = self.inlay_chunk.clone() {
let chunk = &mut inlay_chunk.chunk;
let buffer_chunk_end = buffer_chunk_start + InlayOffset(chunk.text.len());
let transform_end = self.transform_cursor.end(&()).1;
let chunk_end = buffer_chunk_end.min(transform_end);
@@ -1428,7 +1440,7 @@ impl<'a> Iterator for FoldChunks<'a> {
is_tab: chunk.is_tab,
is_inlay: chunk.is_inlay,
underline: chunk.underline,
renderer: None,
renderer: inlay_chunk.renderer,
});
}

View File

@@ -1,4 +1,4 @@
use crate::{HighlightStyles, InlayId};
use crate::{ChunkRenderer, HighlightStyles, InlayId};
use collections::BTreeSet;
use gpui::{Hsla, Rgba};
use language::{Chunk, Edit, Point, TextSummary};
@@ -8,11 +8,13 @@ use multi_buffer::{
use std::{
cmp,
ops::{Add, AddAssign, Range, Sub, SubAssign},
sync::Arc,
};
use sum_tree::{Bias, Cursor, SumTree};
use text::{Patch, Rope};
use ui::{ActiveTheme, IntoElement as _, ParentElement as _, Styled as _, div};
use super::{Highlights, custom_highlights::CustomHighlightsChunks};
use super::{Highlights, custom_highlights::CustomHighlightsChunks, fold_map::ChunkRendererId};
/// Decides where the [`Inlay`]s should be displayed.
///
@@ -252,6 +254,13 @@ pub struct InlayChunks<'a> {
snapshot: &'a InlaySnapshot,
}
#[derive(Clone)]
pub struct InlayChunk<'a> {
pub chunk: Chunk<'a>,
/// Whether the inlay should be customly rendered.
pub renderer: Option<ChunkRenderer>,
}
impl InlayChunks<'_> {
pub fn seek(&mut self, new_range: Range<InlayOffset>) {
self.transforms.seek(&new_range.start, Bias::Right, &());
@@ -271,7 +280,7 @@ impl InlayChunks<'_> {
}
impl<'a> Iterator for InlayChunks<'a> {
type Item = Chunk<'a>;
type Item = InlayChunk<'a>;
fn next(&mut self) -> Option<Self::Item> {
if self.output_offset == self.max_output_offset {
@@ -296,9 +305,12 @@ impl<'a> Iterator for InlayChunks<'a> {
chunk.text = suffix;
self.output_offset.0 += prefix.len();
Chunk {
text: prefix,
..chunk.clone()
InlayChunk {
chunk: Chunk {
text: prefix,
..chunk.clone()
},
renderer: None,
}
}
Transform::Inlay(inlay) => {
@@ -313,6 +325,7 @@ impl<'a> Iterator for InlayChunks<'a> {
}
}
let mut renderer = None;
let mut highlight_style = match inlay.id {
InlayId::InlineCompletion(_) => {
self.highlight_styles.inline_completion.map(|s| {
@@ -325,14 +338,31 @@ impl<'a> Iterator for InlayChunks<'a> {
}
InlayId::Hint(_) => self.highlight_styles.inlay_hint,
InlayId::DebuggerValue(_) => self.highlight_styles.inlay_hint,
InlayId::Color(_) => match inlay.color {
Some(color) => {
let style = self.highlight_styles.inlay_hint.get_or_insert_default();
style.color = Some(color);
Some(*style)
InlayId::Color(_) => {
if let Some(color) = inlay.color {
renderer = Some(ChunkRenderer {
id: ChunkRendererId::Inlay(inlay.id),
render: Arc::new(move |cx| {
div()
.relative()
.size_3p5()
.child(
div()
.absolute()
.right_1()
.size_3()
.border_1()
.border_color(cx.theme().colors().border)
.bg(color),
)
.into_any_element()
}),
constrain_width: false,
measured_width: None,
});
}
None => self.highlight_styles.inlay_hint,
},
self.highlight_styles.inlay_hint
}
};
let next_inlay_highlight_endpoint;
let offset_in_inlay = self.output_offset - self.transforms.start().0;
@@ -370,11 +400,14 @@ impl<'a> Iterator for InlayChunks<'a> {
self.output_offset.0 += chunk.len();
Chunk {
text: chunk,
highlight_style,
is_inlay: true,
..Default::default()
InlayChunk {
chunk: Chunk {
text: chunk,
highlight_style,
is_inlay: true,
..Chunk::default()
},
renderer,
}
}
};
@@ -1066,7 +1099,7 @@ impl InlaySnapshot {
#[cfg(test)]
pub fn text(&self) -> String {
self.chunks(Default::default()..self.len(), false, Highlights::default())
.map(|chunk| chunk.text)
.map(|chunk| chunk.chunk.text)
.collect()
}
@@ -1704,7 +1737,7 @@ mod tests {
..Highlights::default()
},
)
.map(|chunk| chunk.text)
.map(|chunk| chunk.chunk.text)
.collect::<String>();
assert_eq!(
actual_text,

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