Compare commits

..

150 Commits

Author SHA1 Message Date
Conrad Irwin
1cc23e8cf2 Moar 2025-07-03 16:16:44 -06:00
Conrad Irwin
09628506df TEMP 2025-07-03 14:55:44 -06:00
Conrad Irwin
e6be959fda More margin 2025-07-03 11:48:31 -06:00
Conrad Irwin
34a5e90f27 Don't max height 2025-07-03 11:47:18 -06:00
Mikayla Maki
0924a1b6bb Clear errors after auth
Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Agus Zubiaga <agus@zed.dev>
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-07-03 10:25:16 -07:00
Nathan Sobo
31f3a02631 Implement FakeAcpServer and test thinking coalescing
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-07-03 11:19:10 -06:00
Nathan Sobo
1f284df002 WIP 2025-07-03 11:19:10 -06:00
Nathan Sobo
08fead27fb WIP 2025-07-03 11:19:10 -06:00
Agus Zubiaga
0e67f5313e Fix cancel test
Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
2025-07-03 13:15:09 -03:00
Agus Zubiaga
996fbc0aa5 Make tests compile 2025-07-03 13:05:21 -03:00
Agus Zubiaga
c74fc0bf43 Merge branch 'acp' of github.com:zed-industries/zed into acp
Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
2025-07-03 13:01:24 -03:00
Agus Zubiaga
5cba826395 Properly cancel requests
Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
2025-07-03 12:59:43 -03:00
Nathan Sobo
ab8c9b7edc Coalesce acp thinking chunks and improve thinking styling
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-07-03 09:38:05 -06:00
Nathan Sobo
fb1b761021 Render thinking blocks
For now, we style them the same as messages. Next up we'll improve the
styling.

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-07-03 09:22:30 -06:00
Ben Brandt
733e5dd8f2 Add path to the top of the diff 2025-07-03 14:34:33 +02:00
Ben Brandt
fdc365ecf1 Fix clippy lints 2025-07-03 12:16:29 +02:00
Ben Brandt
0770ae2612 ToolCallContent is always available on the ToolCall
Co-authored-by: Antonio Scandurra <me@as-cii.com>
2025-07-03 11:50:30 +02:00
Ben Brandt
a1ad858fb6 Initialize Gemini CLI in tests
Co-authored-by: Antonio Scandurra <me@as-cii.com>
2025-07-03 10:53:10 +02:00
Ben Brandt
ea197d4e6d Remove unused ACP methods
Co-authored-by: Antonio Scandurra <me@as-cii.com>
2025-07-03 10:42:10 +02:00
Ben Brandt
1ab78a7544 Send size for entry as well 2025-07-03 10:07:29 +02:00
Agus Zubiaga
df3d956e63 Only show hunk ranges 2025-07-02 21:16:26 -03:00
Mikayla Maki
4e3f66c5c2 Merge commit 'd6c76d8d33' into acp 2025-07-02 16:53:14 -07:00
Mikayla Maki
d6c76d8d33 Resolve merge conflicts 2025-07-02 16:52:41 -07:00
Agus Zubiaga
f1613afb22 Render confirmation diffs and description as markdown 2025-07-02 19:56:18 -03:00
Conrad Irwin
405f7cf64f Hack in authentication
Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
2025-07-02 16:49:37 -06:00
Conrad Irwin
73ac553316 Show errors from ACP when requests error
Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
2025-07-02 16:48:22 -06:00
Agus Zubiaga
136423da94 Remove some todo! 2025-07-02 18:43:17 -03:00
Agus Zubiaga
28baedd935 Show tool output diffs 2025-07-02 18:39:43 -03:00
Conrad Irwin
756358b9c7 Handle loading outside of a project 2025-07-02 14:16:27 -06:00
Conrad Irwin
54040188bb Show a bit of a better error if gemini cli exits
I considered dumping stderr to the screen, but for now it's useful to
see stderr when developing...
2025-07-02 14:06:00 -06:00
Agus Zubiaga
4755d6fa9d Display tool icons
Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Nathan Sobo <nathan@zed.dev>
2025-07-02 13:48:57 -03:00
Agus Zubiaga
135143d51b Rename display_name to label
Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Nathan Sobo <nathan@zed.dev>
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-07-02 13:16:30 -03:00
Agus Zubiaga
450604b4a1 Add tool call with confirmation test 2025-07-02 12:13:20 -03:00
Agus Zubiaga
348bc52a3f Merge branch 'acp' of github.com:zed-industries/zed into acp 2025-07-02 11:33:22 -03:00
Agus Zubiaga
d16c595d57 Fix always allow, and update acp confirmation types 2025-07-02 11:31:51 -03:00
Antonio Scandurra
975a7e6f7f Fix clicking on tool confirmation buttons
Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
2025-07-02 14:54:24 +02:00
Antonio Scandurra
7d2f7cb70e Replace title with display_name for tool calls
Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
2025-07-02 14:40:16 +02:00
Ben Brandt
5f9afdf7ba Add buttons for more outcomes and handle tools that don't need
authorization

Co-authored-by: Antonio Scandurra <me@as-cii.com>
2025-07-02 12:56:03 +02:00
Ben Brandt
7a3105b0c6 Wire up push_tool_call
Co-authored-by: Antonio Scandurra <me@as-cii.com>
2025-07-02 12:03:35 +02:00
Ben Brandt
ab0b16939d Update tool call confirmation 2025-07-02 11:32:03 +02:00
Agus Zubiaga
28d992487d Better temporary title 2025-07-02 00:58:05 -03:00
Agus Zubiaga
fde15a5a68 Update tool calls via ACP 2025-07-02 00:47:28 -03:00
Agus Zubiaga
780db30e0b Handle waiting for tool confirmation in UI 2025-07-01 23:48:09 -03:00
Agus Zubiaga
7c992adfe1 Improve spacing even more 2025-07-01 23:35:29 -03:00
Agus Zubiaga
825aecfd28 Fix spacing and list scrolling 2025-07-01 23:27:12 -03:00
Agus Zubiaga
f2f32fb3bd Proper allow/reject UI 2025-07-01 23:13:56 -03:00
Agus Zubiaga
d9fd8d5eee Improve spacing 2025-07-01 21:50:14 -03:00
Agus Zubiaga
8137b3318f Remove ReadFile entry and test tool call 2025-07-01 21:37:31 -03:00
Agus Zubiaga
3ceeefe460 Tool authorization 2025-07-01 20:32:21 -03:00
Agus Zubiaga
6f768aefa2 Copy
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-07-01 17:15:57 -03:00
Agus Zubiaga
28ac84ed01 Jump to gemini thread view immediately
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-07-01 17:15:20 -03:00
Agus Zubiaga
4d803fa628 message markdown
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-07-01 16:57:22 -03:00
Agus Zubiaga
17b2dd9a93 Update list incrementally
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-07-01 16:13:16 -03:00
Mikayla Maki
7abf635e20 Use a list to render items
Co-authored-by: Agus Zubiaga <agus@zed.dev>
2025-07-01 11:48:03 -07:00
Antonio Scandurra
92adcb6e63 WIP 2025-07-01 19:01:02 +02:00
Antonio Scandurra
5ed001e0df Merge remote-tracking branch 'origin/main' into agent2
# Conflicts:
#	Cargo.lock
2025-07-01 18:30:08 +02:00
Antonio Scandurra
f12fffd1ba WIP 2025-07-01 18:23:21 +02:00
Julia Ryan
0068de0386 debugger: Handle the envFile setting for Go (#33666)
Fixes #32984

Release Notes:

- The Go debugger now respects the `envFile` setting.
2025-07-01 09:14:59 -07:00
Julia Ryan
a11647d07f ci: Block PRs on Nix build failures (#33688)
Closes #17458

For now we're being conservative and only running CI on changes to the
following files:
- `flake.{nix,lock}`
- `Cargo.{lock,toml}`
- `nix/*`
- `.cargo/config.toml`
- `rust-toolchain.toml`

Release Notes:

- N/A
2025-07-01 09:14:25 -07:00
Peter Tripp
274f2e90da Add support for more python operators (#33720)
Closes: https://github.com/zed-industries/zed/issues/33683

| Before | After |
| - | - |
| <img width="571" alt="Screenshot 2025-07-01 at 11 42 56"
src="https://github.com/user-attachments/assets/5ef79304-37bb-42a1-8891-d19a55a5095e"
/> | <img width="592" alt="Screenshot 2025-07-01 at 11 44 45"
src="https://github.com/user-attachments/assets/f28aa2a8-6306-4294-86e1-8f089f57b825"
/> |

Release Notes:

- python: Properly highlight additional operators ("&=", "<<=", ">>=",
"@=", "^=" and "|=")
2025-07-01 12:12:46 -04:00
Alex Shi
31b7786be7 Fix IndentGuides story (#32781)
This PR updates the `Model` to `Entity` also fixes the
`IndentGuidesStory`. In this
[commit](6fca1d2b0b),
`Entity<T>` replaces `View<T>`/`Model<T>`.

Other than this, I noticed the storybook fails on my MacOS and Ubuntu,
see error below

```
thread 'main' panicked at crates/gpui/src/colors.rs:99:15:
called `Result::unwrap()` on an `Err` value: no state of type gpui::colors::GlobalColors exists
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
```

This was resolved by explicitly specifying `GlobalColors` in Storybook.

Release Notes:

- N/A
2025-07-01 15:43:39 +00:00
G36maid
351ba5023b docs: Add FreeBSD build instructions and current status (#33617)
This adds documentation for building Zed on FreeBSD.
Notice WebRTC/LiveKit remains unsupported on this platform for now.

Follow-up to:
- #33162
- #30981

Release Notes:

- N/A

---------

Co-authored-by: Peter Tripp <peter@zed.dev>
2025-07-01 15:18:34 +00:00
Abdelhakim Qbaich
3041de0cdf Suggest Typst extension for .typ files (#33632)
Release Notes:

- N/A
2025-07-01 17:54:53 +03:00
Marshall Bowers
52c42125a7 language_models: Fix casing of ZedAiConfiguration (#33712)
This PR fixes the casing of the `ZedAiConfiguration` identifier.

Release Notes:

- N/A
2025-07-01 13:29:43 +00:00
Bennet Bo Fenner
62e8f45304 settings: Remove version field migration (#33711)
This reverts some parts of #33372, as it will break the settings for
users running stable and preview at the same time. We can add it back
once the changes make it to stable.

Release Notes:

- N/A
2025-07-01 13:17:36 +00:00
Vitaly Slobodin
0fe73a99e5 ruby: Add basic documentation about debugging (#33572)
Hi, this pull request adds basic documentation about debugging feature
available in the Ruby extension.


Release Notes:

- N/A
2025-07-01 09:12:08 -04:00
Umesh Yadav
6e9c6c5684 git_ui: Fix list in git commit message (#33409)
Follow up: #32114

Closes #33274

Use the new support for language-specific rewrap_prefixes added in
https://github.com/zed-industries/zed/pull/33702.

Release Notes:

- Fix git commit message line break getting stripped after committing.

---------

Signed-off-by: Umesh Yadav <git@umesh.dev>
2025-07-01 08:05:08 -04:00
Danilo Leal
42f788185a agent: Use callout for displaying errors instead of toasts (#33680)
This PR makes all errors in the agent panel to use the `Callout`
component instead of toasts. Reason for that is because the toasts
obscured part of the panel's UI, which wasn't ideal. We can also be more
expressive here with a background color, which I think helps with
parsing the message.

Release Notes:

- agent: Improved how we display errors in the panel.
2025-07-01 09:00:20 -03:00
Cole Miller
a5b2428897 debugger: Fix Go locator for subtests (#33694)
Closes #33054 

Release Notes:

- Fixed debugging Go subtests.
2025-07-01 11:34:50 +00:00
Bennet Bo Fenner
0629804390 agent: Clarify upgrade path when starting trial (#33706)
Release Notes:

- N/A
2025-07-01 11:32:14 +00:00
Smit Barmase
3151b5efc1 languages: Fix ( wouldn’t autocomplete when . is preceded by it in Markdown (#33705)
Closes #5092

Release Notes:

- Fixed issue where `(` wouldn’t autocomplete when `.` is preceded by it
in Markdown.
2025-07-01 16:51:46 +05:30
Bennet Bo Fenner
782fbfad90 agent: Add component preview for Zed AI configuration (#33704)
As we are in the process of improving our Onboarding UX for Zed AI, I
added component previews for the Zed AI Configuration section. This
should make it easier to inspect the different states we can run into.

<img width="1198" alt="image"
src="https://github.com/user-attachments/assets/eb774f27-9091-450d-bfae-c688d533c25e"
/>


Release Notes:

- N/A
2025-07-01 11:12:51 +00:00
Piotr Osiewicz
2caa19214b debugger: Do not include Rust in default value for sourceLanguages (CodeLLDB config) (#33670)
- **debugger: Update exception breakpoints list on capability update**
- **Do not prefill codelldb sourcelanguages by default**

Release Notes:

- debugger: CodeLLDB no longer enables pretty-printers for Rust by
default. This fixes pretty-printers for C++. This is a breaking change
for user-defined debug scenarios from debug.json; in order to enable
Rust pretty printing when using CodeLLDB, add `"sourceLanguages":
["rust"]` to your debug configuration. This change does not affect
scenarios automatically inferred by Zed.

---------

Co-authored-by: Anthony Eid <anthony@zed.dev>
2025-07-01 11:03:40 +00:00
Sunli
bff5d85ff4 gpui: Add the windows-manifest feature to embed manifest, enable by default (#32440)
Gpui's build.rs will embed a manifest file into the Windows binary, but
sometimes we want to customize it, so I added a feature called
`no-windows-manifest` to disable this behavior.

Release Notes:

- N/A
2025-07-01 13:00:14 +02:00
Bedis Nbiba
abe5d523e1 dap_adapters: Add attachSimplePort to JS DAP schema (#31412)
taken from
https://github.com/microsoft/vscode-js-debug/blob/main/OPTIONS.md?plain=1

Release Notes:

- debugger: Added attachSimplePort to JavaScript DAP schema

---------

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
2025-07-01 12:28:01 +02:00
Smit Barmase
8fb3199a84 editor: Improve rewrap of markdown lists, todos, and block quotes (#33702)
Closes #19644 #18151

Now, rewrapping markdown lists (unordered, ordered, and to-do lists) and
block quotes wrap them separately, without merging them together.
Additionally, it correctly indents subsequent lines.

With this input: 

```md
1. This is a list item that is short.
2. This list item is a bit longer because I want to see if it wraps correctly after a rewrap operation in Zed. What do you think?
3. another short item
```

Output would be:

```md
1. This is a list item that is short.
2. This list item is a bit longer because I want to see if it wraps correctly
   after a rewrap operation in Zed. What do you think?
3. another short item
```

Instead of:

```md
1. This is a list item that is short. 2. This list item is a bit longer because 
I want to see if it wraps correctly after a rewrap operation in Zed. What 
do you think? 3. another short item
```

Release Notes:

- Improved rewrap for markdown lists, todos, and block quotes.
2025-07-01 15:34:39 +05:30
Shardul Vaidya
0d809c21ba bedrock: Fix bedrock not streaming (#28281)
Closes #26030 

Release Notes:

- Fixed Bedrock bug causing streaming responses to return as one big
chunk

---------

Co-authored-by: Peter Tripp <peter@zed.dev>
2025-07-01 12:51:09 +03:00
Michael Sloan
93b1e95a5d agent: Make AgentSettings::default_model optional (#33695)
It's already effectively optional and the the old default of gpt-4
doesn't really get used in practice

Release Notes:

- N/A
2025-07-01 00:46:01 -06:00
maan2003
49bc2e61da gpui: Fix slow scrolling in lists (#33608)
matches editor element's behavior


https://github.com/user-attachments/assets/f70912e1-5adb-403b-a98c-63e2e89929ac


- in first version editor scrolls like 1.5 pages, but agent panel only
scrolls half a page.
- in second version, agent panel also scrolls like 1.5 pages.

Release Notes:

- Fixed skipping of some scroll events in the non-uniform list UI element, which fixes slow scrolling of the agent panel.
2025-07-01 00:44:19 -06:00
mslzed
9a4bcd11a2 Remove callout for hiring (#33674)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-06-30 23:35:00 -07:00
Michael Sloan
2ee5bedfa9 agent: Only consider zed provider authenticated if TOS is accepted (#33693)
Also now auto-expands the zed provider section when TOS is not accepted

Release Notes:

- N/A
2025-07-01 04:51:32 +00:00
Michael Sloan
d497f52e17 agent: Improve error handling and retry for zed-provided models (#33565)
* Updates to `zed_llm_client-0.8.5` which adds support for `retry_after`
when anthropic provides it.

* Distinguishes upstream provider errors and rate limits from errors
that originate from zed's servers

* Moves `LanguageModelCompletionError::BadInputJson` to
`LanguageModelCompletionEvent::ToolUseJsonParseError`. While arguably
this is an error case, the logic in thread is cleaner with this move.
There is also precedent for inclusion of errors in the event type -
`CompletionRequestStatus::Failed` is how cloud errors arrive.

* Updates `PROVIDER_ID` / `PROVIDER_NAME` constants to use proper types
instead of `&str`, since they can be constructed in a const fashion.

* Removes use of `CLIENT_SUPPORTS_EXA_WEB_SEARCH_PROVIDER_HEADER_NAME`
as the server no longer reads this header and just defaults to that
behavior.

Release notes for this is covered by #33275

Release Notes:

- N/A

---------

Co-authored-by: Richard Feldman <oss@rtfeldman.com>
Co-authored-by: Richard <richard@zed.dev>
2025-06-30 21:01:32 -06:00
Michael Sloan
f022a13091 Add #[serde(deny_unknown_fields)] to action structs that didn't have it (#33679)
Release Notes:

- N/A
2025-07-01 00:20:02 +00:00
Michael Sloan
c74ecb4654 Warn about unknown fields when editing settings json (#33678)
Closes #30017

* While generating the settings JSON schema, defaults all schema
definitions to reject unknown fields via `additionalProperties: false`.

* Uses `unevaluatedProperties: false` at the top level to check fields
that remain after the settings field names + release stage override
field names.

* Changes json schema version from `draft07` to `draft_2019_09` to have
support for `unevaluatedProperties`.

Release Notes:

- Added warnings for unknown fields when editing `settings.json`.
2025-06-30 23:34:25 +00:00
Mikayla Maki
7609ca7a8d Sketch in a table for the keybindings UI (#32436)
Adds the initial semblance of a keymap UI. It is currently gated behind the `settings-ui` feature flag. Follow up PRs will add polish and missing features.

Release Notes:

- N/A

---------

Co-authored-by: Ben Kunkle <ben@zed.dev>
Co-authored-by: Anthony <anthony@zed.dev>
2025-06-30 19:25:11 -04:00
Umesh Yadav
32906bfa7c Update Cargo.lock (#33667)
Followup to: https://github.com/zed-industries/zed/pull/32208

Release Notes:

- N/A
2025-06-30 15:12:02 -06:00
Michael Sloan
5fafab6e52 Migrate to schemars version 1.0 (#33635)
The major change in schemars 1.0 is that now schemas are represented as
plain json values instead of specialized datatypes. This allows for more
concise construction and manipulation.

This change also improves how settings schemas are generated. Each top
level settings type was being generated as a full root schema including
the definitions it references, and then these were merged. This meant
generating all shared definitions multiple times, and might have bugs in
cases where there are two types with the same names.

Now instead the schemar generator's `definitions` are built up as they
normally are and the `Settings` trait no longer has a special
`json_schema` method. To handle types that have schema that vary at
runtime (`FontFamilyName`, `ThemeName`, etc), values of
`ParameterizedJsonSchema` are collected by `inventory`, and the schema
definitions for these types are replaced.

To help check that this doesn't break anything, I tried to minimize the
overall [schema
diff](https://gist.github.com/mgsloan/1de549def20399d6f37943a3c1583ee7)
with some patches to make the order more consistent + schemas also
sorted with `jq -S .`. A skim of the diff shows that the diffs come
from:

* `enum: ["value"]` turning into `const: "value"`
* Differences in handling of newlines for "description"
* Schemas for generic types no longer including the parameter name, now
all disambiguation is with numeric suffixes
* Enums now using `oneOf` instead of `anyOf`.

Release Notes:

- N/A
2025-06-30 21:07:28 +00: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
Agus Zubiaga
991ba08711 Stop button 2025-06-26 14:37:22 -03:00
Agus Zubiaga
c728731099 Merge last chunk 2025-06-26 14:30:59 -03:00
Agus Zubiaga
ddab1cbd71 Fix notify and margin
Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
2025-06-26 14:23:39 -03:00
Agus Zubiaga
f383a7626f Improve user message
Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
2025-06-26 14:16:30 -03:00
Agus Zubiaga
ee1df65569 Start displaying messages in new thread element
Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
2025-06-26 14:05:59 -03:00
Agus Zubiaga
3be45822be agent2 basic message editor
Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
2025-06-26 13:37:23 -03:00
Agus Zubiaga
3b6f30a6fd Add ThreadElement and render it when active
Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
2025-06-26 13:07:02 -03:00
Agus Zubiaga
779a68f868 Merge branch 'main' into agent2
Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
2025-06-26 12:50:36 -03:00
Agus Zubiaga
79c37284e0 Move ActiveThread into ActiveView::Thread
Co-authored-by: Antonio Scandurra <me@as-cii.com>
2025-06-26 11:36:05 -03:00
Ben Brandt
0a053cf55d Merge branch 'main' into agent2 2025-06-26 14:36:39 +02:00
Ben Brandt
fc59d9cbf3 Clean up tests
Co-authored-by: Agus Zubiaga <agus@zed.dev>
Co-authored-by: Antonio Scandurra <me@as-cii.com>
2025-06-26 14:22:13 +02:00
Ben Brandt
678a42e920 Fix missing variant 2025-06-26 14:00:21 +02:00
Ben Brandt
75bcaf743c Put user messages into thread 2025-06-26 13:59:41 +02:00
Ben Brandt
47c875f6b5 Pass GEMINI_API_KEY to agent process if available 2025-06-26 12:25:23 +02:00
Max Brunsfeld
81b4d7e35a Start on using agent2 from agent_ui 2025-06-25 20:23:41 -07:00
Max Brunsfeld
33ee0c3093 Return an Arc from AcpAgent::stdio 2025-06-25 20:23:18 -07:00
Max Brunsfeld
d68f86052f Merge branch 'main' into agent2 2025-06-25 15:57:59 -07:00
Max Brunsfeld
a74ffd9ee4 In test, start gemini in the right directory
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-06-25 14:59:07 -07:00
Conrad Irwin
8b9ad1cfae passing roundtrip test
Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Co-authored-by: Agus Zubiaga <agus@zed.dev>
2025-06-25 15:18:42 -06:00
Max Brunsfeld
adbccb1ad0 Get agent2 compiling
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
Co-authored-by: Antonio Scandurra <me@as-cii.com>
2025-06-25 10:30:52 -07:00
Agus Zubiaga
f4e2d38c29 --wip-- 2025-06-25 13:54:31 -03:00
Ben Brandt
5f10be7791 Start implementing send
Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Agus Zubiaga <agus@zed.dev>
2025-06-25 14:40:33 +02:00
Ben Brandt
d47a920c05 Implement ACP threads
The `create_thread` and `get_threads` methods are now implemented for
the ACP agent. A test is added to verify the file reading flow.
2025-06-25 13:10:43 +02:00
Ben Brandt
24b72be154 Add debug/clone to structs for testing 2025-06-25 10:11:50 +02:00
Max Brunsfeld
de779a45ce Get one test passing w/ gemini cli 2025-06-24 20:07:41 -07:00
Agus Zubiaga
b094a636cf Checkpoint: Wiring up acp crate
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com> Co-authored-by:
Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
Co-authored-by: Max <max@zed.dev>
2025-06-24 18:27:25 -03:00
Agus Zubiaga
318709b60d Fix typo
Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
2025-06-24 16:51:43 -03:00
Agus Zubiaga
f1bd531a32 Handle pending requests
Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
2025-06-24 16:30:29 -03:00
Ben Brandt
549eb4d826 wip: request / response in send loop
Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Agus Zubiaga <agus@zed.dev>
2025-06-24 14:50:48 +02:00
Ben Brandt
c1e53b7fa5 wip: test
Co-authored-by: Antonio Scandurra <me@as-cii.com>
2025-06-24 12:31:04 +02:00
Ben Brandt
ec376e0b61 Sketch out new Agent traits
Co-authored-by: Antonio Scandurra <me@as-cii.com>
2025-06-24 12:26:40 +02:00
189 changed files with 15431 additions and 7510 deletions

View File

@@ -30,6 +30,7 @@ jobs:
run_tests: ${{ steps.filter.outputs.run_tests }}
run_license: ${{ steps.filter.outputs.run_license }}
run_docs: ${{ steps.filter.outputs.run_docs }}
run_nix: ${{ steps.filter.outputs.run_nix }}
runs-on:
- ubuntu-latest
steps:
@@ -69,6 +70,12 @@ jobs:
else
echo "run_license=false" >> $GITHUB_OUTPUT
fi
NIX_REGEX='^(nix/|flake\.|Cargo\.|rust-toolchain.toml|\.cargo/config.toml)'
if [[ $(git diff --name-only $COMPARE_REV ${{ github.sha }} | grep "$NIX_REGEX") ]]; then
echo "run_nix=true" >> $GITHUB_OUTPUT
else
echo "run_nix=false" >> $GITHUB_OUTPUT
fi
migration_checks:
name: Check Postgres and Protobuf migrations, mergability
@@ -746,7 +753,10 @@ jobs:
nix-build:
name: Build with Nix
uses: ./.github/workflows/nix.yml
if: github.repository_owner == 'zed-industries' && contains(github.event.pull_request.labels.*.name, 'run-nix')
needs: [job_spec]
if: github.repository_owner == 'zed-industries' &&
(contains(github.event.pull_request.labels.*.name, 'run-nix') ||
needs.job_spec.outputs.run_nix == 'true')
secrets: inherit
with:
flake-output: debug

92
Cargo.lock generated
View File

@@ -2,6 +2,40 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "acp"
version = "0.1.0"
dependencies = [
"agentic-coding-protocol",
"anyhow",
"async-pipe",
"async-trait",
"base64 0.22.1",
"buffer_diff",
"chrono",
"collections",
"editor",
"env_logger 0.11.8",
"futures 0.3.31",
"gpui",
"indoc",
"language",
"log",
"markdown",
"parking_lot",
"project",
"proto",
"serde_json",
"settings",
"smol",
"theme",
"ui",
"util",
"uuid",
"workspace-hack",
"zed_actions",
]
[[package]]
name = "activity_indicator"
version = "0.1.0"
@@ -78,7 +112,6 @@ dependencies = [
"language",
"language_model",
"log",
"markdown",
"parking_lot",
"paths",
"postage",
@@ -131,6 +164,7 @@ dependencies = [
name = "agent_ui"
version = "0.1.0"
dependencies = [
"acp",
"agent",
"agent_settings",
"anyhow",
@@ -213,6 +247,21 @@ dependencies = [
"zed_llm_client",
]
[[package]]
name = "agentic-coding-protocol"
version = "0.0.1"
dependencies = [
"anyhow",
"async-trait",
"chrono",
"futures 0.3.31",
"log",
"parking_lot",
"schemars",
"serde",
"serde_json",
]
[[package]]
name = "ahash"
version = "0.7.8"
@@ -1912,7 +1961,6 @@ dependencies = [
"serde_json",
"strum 0.27.1",
"thiserror 2.0.12",
"tokio",
"workspace-hack",
]
@@ -4134,7 +4182,7 @@ dependencies = [
[[package]]
name = "dap-types"
version = "0.0.1"
source = "git+https://github.com/zed-industries/dap-types?rev=b40956a7f4d1939da67429d941389ee306a3a308#b40956a7f4d1939da67429d941389ee306a3a308"
source = "git+https://github.com/zed-industries/dap-types?rev=7f39295b441614ca9dbf44293e53c32f666897f9#7f39295b441614ca9dbf44293e53c32f666897f9"
dependencies = [
"schemars",
"serde",
@@ -4149,6 +4197,8 @@ dependencies = [
"async-trait",
"collections",
"dap",
"dotenvy",
"fs",
"futures 0.3.31",
"gpui",
"json_dotpath",
@@ -4677,12 +4727,6 @@ dependencies = [
"syn 2.0.101",
]
[[package]]
name = "dotenv"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f"
[[package]]
name = "dotenvy"
version = "0.15.7"
@@ -4815,6 +4859,7 @@ dependencies = [
"pretty_assertions",
"project",
"rand 0.8.5",
"regex",
"release_channel",
"rpc",
"schemars",
@@ -5115,7 +5160,7 @@ dependencies = [
"collections",
"debug_adapter_extension",
"dirs 4.0.0",
"dotenv",
"dotenvy",
"env_logger 0.11.8",
"extension",
"fs",
@@ -8848,6 +8893,7 @@ dependencies = [
"http_client",
"imara-diff",
"indoc",
"inventory",
"itertools 0.14.0",
"log",
"lsp",
@@ -8946,8 +8992,10 @@ dependencies = [
"aws-credential-types",
"aws_http_client",
"bedrock",
"chrono",
"client",
"collections",
"component",
"copilot",
"credentials_provider",
"deepseek",
@@ -14054,12 +14102,14 @@ dependencies = [
[[package]]
name = "schemars"
version = "0.8.22"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615"
checksum = "fe8c9d1c68d67dd9f97ecbc6f932b60eb289c5dbddd8aa1405484a8fd2fcd984"
dependencies = [
"chrono",
"dyn-clone",
"indexmap",
"ref-cast",
"schemars_derive",
"serde",
"serde_json",
@@ -14067,9 +14117,9 @@ dependencies = [
[[package]]
name = "schemars_derive"
version = "0.8.22"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d"
checksum = "6ca9fcb757952f8e8629b9ab066fc62da523c46c2b247b1708a3be06dd82530b"
dependencies = [
"proc-macro2",
"quote",
@@ -14568,13 +14618,22 @@ dependencies = [
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",
@@ -16011,6 +16070,7 @@ dependencies = [
"futures 0.3.31",
"gpui",
"indexmap",
"inventory",
"log",
"palette",
"parking_lot",
@@ -20128,9 +20188,9 @@ dependencies = [
[[package]]
name = "zed_llm_client"
version = "0.8.4"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de7d9523255f4e00ee3d0918e5407bd252d798a4a8e71f6d37f23317a1588203"
checksum = "c740e29260b8797ad252c202ea09a255b3cbc13f30faaf92fb6b2490336106e0"
dependencies = [
"anyhow",
"serde",

View File

@@ -2,6 +2,7 @@
resolver = "2"
members = [
"crates/activity_indicator",
"crates/acp",
"crates/agent_ui",
"crates/agent",
"crates/agent_settings",
@@ -215,8 +216,9 @@ edition = "2024"
# Workspace member crates
#
activity_indicator = { path = "crates/activity_indicator" }
acp = { path = "crates/acp" }
agent = { path = "crates/agent" }
activity_indicator = { path = "crates/activity_indicator" }
agent_ui = { path = "crates/agent_ui" }
agent_settings = { path = "crates/agent_settings" }
ai = { path = "crates/ai" }
@@ -398,6 +400,7 @@ zlog_settings = { path = "crates/zlog_settings" }
# External crates
#
agentic-coding-protocol = { path = "../agentic-coding-protocol" }
aho-corasick = "1.1"
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" }
any_vec = "0.14"
@@ -444,12 +447,12 @@ core-video = { version = "0.4.3", features = ["metal"] }
cpal = "0.16"
criterion = { version = "0.5", features = ["html_reports"] }
ctor = "0.4.0"
dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "b40956a7f4d1939da67429d941389ee306a3a308" }
dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "7f39295b441614ca9dbf44293e53c32f666897f9" }
dashmap = "6.0"
derive_more = "0.99.17"
dirs = "4.0"
documented = "0.9.1"
dotenv = "0.15.0"
dotenvy = "0.15.0"
ec4rs = "1.1"
emojis = "0.6.1"
env_logger = "0.11"
@@ -480,7 +483,7 @@ json_dotpath = "1.1"
jsonschema = "0.30.0"
jsonwebtoken = "9.3"
jupyter-protocol = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
jupyter-websocket-client = { git = "https://github.com/ConradIrwin/runtimed" ,rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
jupyter-websocket-client = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
libc = "0.2"
libsqlite3-sys = { version = "0.30.1", features = ["bundled"] }
linkify = "0.10.0"
@@ -491,7 +494,7 @@ metal = "0.29"
moka = { version = "0.12.10", features = ["sync"] }
naga = { version = "25.0", features = ["wgsl-in"] }
nanoid = "0.4"
nbformat = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
nbformat = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
nix = "0.29"
num-format = "0.4.4"
objc = "0.2"
@@ -531,7 +534,7 @@ reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "951c77
"stream",
] }
rsa = "0.9.6"
runtimelib = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734", default-features = false, features = [
runtimelib = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734", default-features = false, features = [
"async-dispatcher-runtime",
] }
rust-embed = { version = "8.4", features = ["include-exclude"] }
@@ -540,7 +543,7 @@ rustc-hash = "2.1.0"
rustls = { version = "0.23.26" }
rustls-platform-verifier = "0.5.0"
scap = { git = "https://github.com/zed-industries/scap", rev = "08f0a01417505cc0990b9931a37e5120db92e0d0", default-features = false }
schemars = { version = "0.8", features = ["impl_json_schema", "indexmap2"] }
schemars = { version = "1.0", features = ["indexmap2"] }
semver = "1.0"
serde = { version = "1.0", features = ["derive", "rc"] }
serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
@@ -625,7 +628,7 @@ wasmtime = { version = "29", default-features = false, features = [
wasmtime-wasi = "29"
which = "6.0.0"
workspace-hack = "0.1.0"
zed_llm_client = "0.8.4"
zed_llm_client = "0.8.5"
zstd = "0.11"
[workspace.dependencies.async-stripe]

View File

@@ -1067,5 +1067,12 @@
"ctrl-tab": "pane::ActivateNextItem",
"ctrl-shift-tab": "pane::ActivatePreviousItem"
}
},
{
"context": "KeymapEditor",
"use_key_equivalents": true,
"bindings": {
"ctrl-f": "search::FocusSearch"
}
}
]

View File

@@ -1167,5 +1167,12 @@
"ctrl-tab": "pane::ActivateNextItem",
"ctrl-shift-tab": "pane::ActivatePreviousItem"
}
},
{
"context": "KeymapEditor",
"use_key_equivalents": true,
"bindings": {
"cmd-f": "search::FocusSearch"
}
}
]

View File

@@ -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",
@@ -849,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": {
@@ -860,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"
}
},
{

52
crates/acp/Cargo.toml Normal file
View File

@@ -0,0 +1,52 @@
[package]
name = "acp"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/acp.rs"
doctest = false
[features]
test-support = ["gpui/test-support", "project/test-support"]
[dependencies]
agentic-coding-protocol = { path = "../../../agentic-coding-protocol" }
anyhow.workspace = true
async-trait.workspace = true
base64.workspace = true
buffer_diff.workspace = true
chrono.workspace = true
collections.workspace = true
editor.workspace = true
futures.workspace = true
gpui.workspace = true
language.workspace = true
log.workspace = true
markdown.workspace = true
parking_lot.workspace = true
project.workspace = true
proto.workspace = true
settings.workspace = true
smol.workspace = true
theme.workspace = true
ui.workspace = true
util.workspace = true
uuid.workspace = true
workspace-hack.workspace = true
zed_actions.workspace = true
[dev-dependencies]
async-pipe.workspace = true
env_logger.workspace = true
gpui = { workspace = true, "features" = ["test-support"] }
indoc.workspace = true
project = { workspace = true, "features" = ["test-support"] }
serde_json.workspace = true
util.workspace = true
settings.workspace = true

1
crates/acp/LICENSE-GPL Symbolic link
View File

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

1438
crates/acp/src/acp.rs Normal file

File diff suppressed because it is too large Load Diff

125
crates/acp/src/server.rs Normal file
View File

@@ -0,0 +1,125 @@
use crate::{AcpThread, ThreadEntryId, ToolCallId, ToolCallRequest};
use agentic_coding_protocol as acp;
use anyhow::{Context as _, Result};
use async_trait::async_trait;
use gpui::{App, AppContext, AsyncApp, Entity, Task, WeakEntity};
use parking_lot::Mutex;
use project::Project;
use smol::process::Child;
use std::{process::ExitStatus, sync::Arc};
use util::ResultExt;
pub struct AcpServer {
thread: WeakEntity<AcpThread>,
project: Entity<Project>,
connection: Arc<acp::AgentConnection>,
exit_status: Arc<Mutex<Option<ExitStatus>>>,
_handler_task: Task<()>,
_io_task: Task<()>,
}
struct AcpClientDelegate {
thread: WeakEntity<AcpThread>,
cx: AsyncApp,
// sent_buffer_versions: HashMap<Entity<Buffer>, HashMap<u64, BufferSnapshot>>,
}
impl AcpClientDelegate {
fn new(thread: WeakEntity<AcpThread>, cx: AsyncApp) -> Self {
Self { thread, cx }
}
}
#[async_trait(?Send)]
impl acp::Client for AcpClientDelegate {
async fn stream_assistant_message_chunk(
&self,
params: acp::StreamAssistantMessageChunkParams,
) -> Result<acp::StreamAssistantMessageChunkResponse> {
let cx = &mut self.cx.clone();
cx.update(|cx| {
self.thread.update(cx, |thread, cx| {
thread.push_assistant_chunk(params.chunk, cx)
});
})?;
Ok(acp::StreamAssistantMessageChunkResponse)
}
async fn request_tool_call_confirmation(
&self,
request: acp::RequestToolCallConfirmationParams,
) -> Result<acp::RequestToolCallConfirmationResponse> {
let cx = &mut self.cx.clone();
let ToolCallRequest { id, outcome } = cx
.update(|cx| {
self.thread.update(cx, |thread, cx| {
thread.request_tool_call(
request.label,
request.icon,
request.content,
request.confirmation,
cx,
)
})
})?
.context("Failed to update thread")?;
Ok(acp::RequestToolCallConfirmationResponse {
id: id.into(),
outcome: outcome.await?,
})
}
async fn push_tool_call(
&self,
request: acp::PushToolCallParams,
) -> Result<acp::PushToolCallResponse> {
let cx = &mut self.cx.clone();
let entry_id = cx
.update(|cx| {
self.thread.update(cx, |thread, cx| {
thread.push_tool_call(request.label, request.icon, request.content, cx)
})
})?
.context("Failed to update thread")?;
Ok(acp::PushToolCallResponse {
id: entry_id.into(),
})
}
async fn update_tool_call(
&self,
request: acp::UpdateToolCallParams,
) -> Result<acp::UpdateToolCallResponse> {
let cx = &mut self.cx.clone();
cx.update(|cx| {
self.thread.update(cx, |thread, cx| {
thread.update_tool_call(
request.tool_call_id.into(),
request.status,
request.content,
cx,
)
})
})?
.context("Failed to update thread")??;
Ok(acp::UpdateToolCallResponse)
}
}
impl From<acp::ToolCallId> for ToolCallId {
fn from(tool_call_id: acp::ToolCallId) -> Self {
Self(ThreadEntryId(tool_call_id.0))
}
}
impl From<ToolCallId> for acp::ToolCallId {
fn from(tool_call_id: ToolCallId) -> Self {
acp::ToolCallId(tool_call_id.as_u64())
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -42,7 +42,6 @@ itertools.workspace = true
language.workspace = true
language_model.workspace = true
log.workspace = true
markdown.workspace = true
paths.workspace = true
postage.workspace = true
project.workspace = true

View File

@@ -1,4 +1,3 @@
mod agent2;
pub mod agent_profile;
pub mod context;
pub mod context_server_tool;
@@ -6,17 +5,15 @@ pub mod context_store;
pub mod history_store;
pub mod thread;
pub mod thread_store;
mod zed_agent;
pub mod tool_use;
pub use agent2::*;
pub use context::{AgentContext, ContextId, ContextLoadResult};
pub use context_store::ContextStore;
pub use thread::{
LastRestoreCheckpoint, Message, MessageCrease, MessageSegment, Thread, ThreadError,
ThreadEvent, ThreadFeedback, ThreadTitle, TokenUsageRatio,
LastRestoreCheckpoint, Message, MessageCrease, MessageId, MessageSegment, Thread, ThreadError,
ThreadEvent, ThreadFeedback, ThreadId, ThreadSummary, TokenUsageRatio,
};
pub use thread_store::{SerializedThread, TextThreadStore, ThreadStore};
pub use zed_agent::*;
pub fn init(cx: &mut gpui::App) {
thread_store::init(cx);

View File

@@ -1,117 +0,0 @@
use anyhow::Result;
use assistant_tool::{Tool, ToolResultOutput};
use futures::{channel::oneshot, future::BoxFuture, stream::BoxStream};
use gpui::SharedString;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{
fmt::{self, Display},
sync::Arc,
};
#[derive(
Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
)]
pub struct ThreadId(SharedString);
impl ThreadId {
pub fn as_str(&self) -> &str {
&self.0
}
pub fn to_string(&self) -> String {
self.0.to_string()
}
}
impl From<&str> for ThreadId {
fn from(value: &str) -> Self {
ThreadId(SharedString::from(value.to_string()))
}
}
impl Display for ThreadId {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, Copy, Ord, PartialOrd, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub struct MessageId(pub usize);
#[derive(Debug, Clone)]
pub struct AgentThreadToolCallId(SharedString);
pub enum AgentThreadResponseEvent {
Text(String),
Thinking(String),
ToolCallChunk {
id: AgentThreadToolCallId,
tool: Arc<dyn Tool>,
input: serde_json::Value,
},
ToolCall {
id: AgentThreadToolCallId,
tool: Arc<dyn Tool>,
input: serde_json::Value,
response_tx: oneshot::Sender<Result<ToolResultOutput>>,
},
}
pub enum AgentThreadMessage {
User {
id: MessageId,
chunks: Vec<AgentThreadUserMessageChunk>,
},
Assistant {
id: MessageId,
chunks: Vec<AgentThreadAssistantMessageChunk>,
},
}
pub enum AgentThreadUserMessageChunk {
Text(String),
// here's where we would put mentions, etc.
}
pub enum AgentThreadAssistantMessageChunk {
Text(String),
Thinking(String),
ToolResult {
id: AgentThreadToolCallId,
tool: Arc<dyn Tool>,
input: serde_json::Value,
output: Result<ToolResultOutput>,
},
}
pub struct AgentThreadResponse {
pub user_message_id: MessageId,
pub assistant_message_id: MessageId,
pub events: BoxStream<'static, Result<AgentThreadResponseEvent>>,
}
pub trait Agent {
fn create_thread() -> BoxFuture<'static, Result<Arc<dyn AgentThread>>>;
fn list_threads();
fn load_thread(&self, thread_id: ThreadId) -> BoxFuture<'static, Result<Arc<dyn AgentThread>>>;
}
pub trait AgentThread {
fn id(&self) -> ThreadId;
fn title(&self) -> BoxFuture<'static, Result<String>>;
fn summary(&self) -> BoxFuture<'static, Result<String>>;
fn messages(&self) -> BoxFuture<'static, Result<Vec<AgentThreadMessage>>>;
fn truncate(&self, message_id: MessageId) -> BoxFuture<'static, Result<()>>;
fn edit(
&self,
message_id: MessageId,
content: Vec<AgentThreadUserMessageChunk>,
max_iterations: usize,
) -> BoxFuture<'static, Result<AgentThreadResponse>>;
fn send(
&self,
content: Vec<AgentThreadUserMessageChunk>,
max_iterations: usize,
) -> BoxFuture<'static, Result<AgentThreadResponse>>;
}

View File

@@ -96,16 +96,11 @@ impl AgentProfile {
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

@@ -581,7 +581,7 @@ impl ThreadContextHandle {
}
pub fn title(&self, cx: &App) -> SharedString {
self.thread.read(cx).title().or_default()
self.thread.read(cx).summary().or_default()
}
fn load(self, cx: &App) -> Task<Option<(AgentContext, Vec<Entity<Buffer>>)>> {
@@ -589,7 +589,7 @@ impl ThreadContextHandle {
let text = Thread::wait_for_detailed_summary_or_text(&self.thread, cx).await?;
let title = self
.thread
.read_with(cx, |thread, _cx| thread.title().or_default())
.read_with(cx, |thread, _cx| thread.summary().or_default())
.ok()?;
let context = AgentContext::Thread(ThreadContext {
title,

View File

@@ -1,11 +1,10 @@
use crate::{
MessageId, ThreadId,
context::{
AgentContextHandle, AgentContextKey, ContextId, ContextKind, DirectoryContextHandle,
FetchedUrlContext, FileContextHandle, ImageContext, RulesContextHandle,
SelectionContextHandle, SymbolContextHandle, TextThreadContextHandle, ThreadContextHandle,
},
thread::Thread,
thread::{MessageId, Thread, ThreadId},
thread_store::ThreadStore,
};
use anyhow::{Context as _, Result, anyhow};
@@ -72,8 +71,7 @@ impl ContextStore {
) -> Vec<AgentContextHandle> {
let existing_context = thread
.messages()
.iter()
.take_while(|message| exclude_messages_from_id.is_none_or(|id| message.id() != id))
.take_while(|message| exclude_messages_from_id.is_none_or(|id| message.id != id))
.flat_map(|message| {
message
.loaded_context
@@ -443,7 +441,7 @@ impl ContextStore {
match context {
AgentContextHandle::Thread(thread_context) => {
self.context_thread_ids
.remove(&thread_context.thread.read(cx).id());
.remove(thread_context.thread.read(cx).id());
}
AgentContextHandle::TextThread(text_thread_context) => {
if let Some(path) = text_thread_context.context.read(cx).path() {

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,8 @@
use crate::{
MessageId, ThreadId,
context_server_tool::ContextServerTool,
thread::{DetailedSummaryState, ExceededWindowError, ProjectSnapshot, Thread},
thread::{
DetailedSummaryState, ExceededWindowError, MessageId, ProjectSnapshot, Thread, ThreadId,
},
};
use agent_settings::{AgentProfileId, CompletionMode};
use anyhow::{Context as _, Result, anyhow};
@@ -399,17 +400,35 @@ impl ThreadStore {
self.threads.iter()
}
pub fn create_thread(&mut self, cx: &mut Context<Self>) -> Task<Result<Entity<Thread>>> {
todo!()
// cx.new(|cx| {
// Thread::new(
// self.project.clone(),
// self.tools.clone(),
// self.prompt_builder.clone(),
// self.project_context.clone(),
// cx,
// )
// })
pub fn create_thread(&mut self, cx: &mut Context<Self>) -> Entity<Thread> {
cx.new(|cx| {
Thread::new(
self.project.clone(),
self.tools.clone(),
self.prompt_builder.clone(),
self.project_context.clone(),
cx,
)
})
}
pub fn create_thread_from_serialized(
&mut self,
serialized: SerializedThread,
cx: &mut Context<Self>,
) -> Entity<Thread> {
cx.new(|cx| {
Thread::deserialize(
ThreadId::new(),
serialized,
self.project.clone(),
self.tools.clone(),
self.prompt_builder.clone(),
self.project_context.clone(),
None,
cx,
)
})
}
pub fn open_thread(
@@ -428,53 +447,51 @@ impl ThreadStore {
.await?
.with_context(|| format!("no thread found with ID: {id:?}"))?;
todo!();
// let thread = this.update_in(cx, |this, window, cx| {
// cx.new(|cx| {
// Thread::deserialize(
// id.clone(),
// thread,
// this.project.clone(),
// this.tools.clone(),
// this.prompt_builder.clone(),
// this.project_context.clone(),
// Some(window),
// cx,
// )
// })
// })?;
// Ok(thread)
let thread = this.update_in(cx, |this, window, cx| {
cx.new(|cx| {
Thread::deserialize(
id.clone(),
thread,
this.project.clone(),
this.tools.clone(),
this.prompt_builder.clone(),
this.project_context.clone(),
Some(window),
cx,
)
})
})?;
Ok(thread)
})
}
pub fn save_thread(&self, thread: &Entity<Thread>, cx: &mut Context<Self>) -> Task<Result<()>> {
todo!()
// let (metadata, serialized_thread) =
// thread.update(cx, |thread, cx| (thread.id().clone(), thread.serialize(cx)));
let (metadata, serialized_thread) =
thread.update(cx, |thread, cx| (thread.id().clone(), thread.serialize(cx)));
// let database_future = ThreadsDatabase::global_future(cx);
// cx.spawn(async move |this, cx| {
// let serialized_thread = serialized_thread.await?;
// let database = database_future.await.map_err(|err| anyhow!(err))?;
// database.save_thread(metadata, serialized_thread).await?;
let database_future = ThreadsDatabase::global_future(cx);
cx.spawn(async move |this, cx| {
let serialized_thread = serialized_thread.await?;
let database = database_future.await.map_err(|err| anyhow!(err))?;
database.save_thread(metadata, serialized_thread).await?;
// this.update(cx, |this, cx| this.reload(cx))?.await
// })
this.update(cx, |this, cx| this.reload(cx))?.await
})
}
pub fn delete_thread(&mut self, id: &ThreadId, cx: &mut Context<Self>) -> Task<Result<()>> {
todo!()
// let id = id.clone();
// let database_future = ThreadsDatabase::global_future(cx);
// cx.spawn(async move |this, cx| {
// let database = database_future.await.map_err(|err| anyhow!(err))?;
// database.delete_thread(id.clone()).await?;
let id = id.clone();
let database_future = ThreadsDatabase::global_future(cx);
cx.spawn(async move |this, cx| {
let database = database_future.await.map_err(|err| anyhow!(err))?;
database.delete_thread(id.clone()).await?;
// this.update(cx, |this, cx| {
// this.threads.retain(|thread| thread.id != id);
// cx.notify();
// })
// })
this.update(cx, |this, cx| {
this.threads.retain(|thread| thread.id != id);
cx.notify();
})
})
}
pub fn reload(&self, cx: &mut Context<Self>) -> Task<Result<()>> {
@@ -1050,7 +1067,7 @@ impl ThreadsDatabase {
#[cfg(test)]
mod tests {
use super::*;
use crate::{MessageId, thread::DetailedSummaryState};
use crate::thread::{DetailedSummaryState, MessageId};
use chrono::Utc;
use language_model::{Role, TokenUsage};
use pretty_assertions::assert_eq;

View File

@@ -0,0 +1,567 @@
use crate::{
thread::{MessageId, PromptId, ThreadId},
thread_store::SerializedMessage,
};
use anyhow::Result;
use assistant_tool::{
AnyToolCard, Tool, ToolResultContent, ToolResultOutput, ToolUseStatus, ToolWorkingSet,
};
use collections::HashMap;
use futures::{FutureExt as _, future::Shared};
use gpui::{App, Entity, SharedString, Task, Window};
use icons::IconName;
use language_model::{
ConfiguredModel, LanguageModel, LanguageModelRequest, LanguageModelToolResult,
LanguageModelToolResultContent, LanguageModelToolUse, LanguageModelToolUseId, Role,
};
use project::Project;
use std::sync::Arc;
use util::truncate_lines_to_byte_limit;
#[derive(Debug)]
pub struct ToolUse {
pub id: LanguageModelToolUseId,
pub name: SharedString,
pub ui_text: SharedString,
pub status: ToolUseStatus,
pub input: serde_json::Value,
pub icon: icons::IconName,
pub needs_confirmation: bool,
}
pub struct ToolUseState {
tools: Entity<ToolWorkingSet>,
tool_uses_by_assistant_message: HashMap<MessageId, Vec<LanguageModelToolUse>>,
tool_results: HashMap<LanguageModelToolUseId, LanguageModelToolResult>,
pending_tool_uses_by_id: HashMap<LanguageModelToolUseId, PendingToolUse>,
tool_result_cards: HashMap<LanguageModelToolUseId, AnyToolCard>,
tool_use_metadata_by_id: HashMap<LanguageModelToolUseId, ToolUseMetadata>,
}
impl ToolUseState {
pub fn new(tools: Entity<ToolWorkingSet>) -> Self {
Self {
tools,
tool_uses_by_assistant_message: HashMap::default(),
tool_results: HashMap::default(),
pending_tool_uses_by_id: HashMap::default(),
tool_result_cards: HashMap::default(),
tool_use_metadata_by_id: HashMap::default(),
}
}
/// Constructs a [`ToolUseState`] from the given list of [`SerializedMessage`]s.
///
/// Accepts a function to filter the tools that should be used to populate the state.
///
/// If `window` is `None` (e.g., when in headless mode or when running evals),
/// tool cards won't be deserialized
pub fn from_serialized_messages(
tools: Entity<ToolWorkingSet>,
messages: &[SerializedMessage],
project: Entity<Project>,
window: Option<&mut Window>, // None in headless mode
cx: &mut App,
) -> Self {
let mut this = Self::new(tools);
let mut tool_names_by_id = HashMap::default();
let mut window = window;
for message in messages {
match message.role {
Role::Assistant => {
if !message.tool_uses.is_empty() {
let tool_uses = message
.tool_uses
.iter()
.map(|tool_use| LanguageModelToolUse {
id: tool_use.id.clone(),
name: tool_use.name.clone().into(),
raw_input: tool_use.input.to_string(),
input: tool_use.input.clone(),
is_input_complete: true,
})
.collect::<Vec<_>>();
tool_names_by_id.extend(
tool_uses
.iter()
.map(|tool_use| (tool_use.id.clone(), tool_use.name.clone())),
);
this.tool_uses_by_assistant_message
.insert(message.id, tool_uses);
for tool_result in &message.tool_results {
let tool_use_id = tool_result.tool_use_id.clone();
let Some(tool_use) = tool_names_by_id.get(&tool_use_id) else {
log::warn!("no tool name found for tool use: {tool_use_id:?}");
continue;
};
this.tool_results.insert(
tool_use_id.clone(),
LanguageModelToolResult {
tool_use_id: tool_use_id.clone(),
tool_name: tool_use.clone(),
is_error: tool_result.is_error,
content: tool_result.content.clone(),
output: tool_result.output.clone(),
},
);
if let Some(window) = &mut window {
if let Some(tool) = this.tools.read(cx).tool(tool_use, cx) {
if let Some(output) = tool_result.output.clone() {
if let Some(card) = tool.deserialize_card(
output,
project.clone(),
window,
cx,
) {
this.tool_result_cards.insert(tool_use_id, card);
}
}
}
}
}
}
}
Role::System | Role::User => {}
}
}
this
}
pub fn cancel_pending(&mut self) -> Vec<PendingToolUse> {
let mut cancelled_tool_uses = Vec::new();
self.pending_tool_uses_by_id
.retain(|tool_use_id, tool_use| {
if matches!(tool_use.status, PendingToolUseStatus::Error { .. }) {
return true;
}
let content = "Tool canceled by user".into();
self.tool_results.insert(
tool_use_id.clone(),
LanguageModelToolResult {
tool_use_id: tool_use_id.clone(),
tool_name: tool_use.name.clone(),
content,
output: None,
is_error: true,
},
);
cancelled_tool_uses.push(tool_use.clone());
false
});
cancelled_tool_uses
}
pub fn pending_tool_uses(&self) -> Vec<&PendingToolUse> {
self.pending_tool_uses_by_id.values().collect()
}
pub fn tool_uses_for_message(&self, id: MessageId, cx: &App) -> Vec<ToolUse> {
let Some(tool_uses_for_message) = &self.tool_uses_by_assistant_message.get(&id) else {
return Vec::new();
};
let mut tool_uses = Vec::new();
for tool_use in tool_uses_for_message.iter() {
let tool_result = self.tool_results.get(&tool_use.id);
let status = (|| {
if let Some(tool_result) = tool_result {
let content = tool_result
.content
.to_str()
.map(|str| str.to_owned().into())
.unwrap_or_default();
return if tool_result.is_error {
ToolUseStatus::Error(content)
} else {
ToolUseStatus::Finished(content)
};
}
if let Some(pending_tool_use) = self.pending_tool_uses_by_id.get(&tool_use.id) {
match pending_tool_use.status {
PendingToolUseStatus::Idle => ToolUseStatus::Pending,
PendingToolUseStatus::NeedsConfirmation { .. } => {
ToolUseStatus::NeedsConfirmation
}
PendingToolUseStatus::Running { .. } => ToolUseStatus::Running,
PendingToolUseStatus::Error(ref err) => {
ToolUseStatus::Error(err.clone().into())
}
PendingToolUseStatus::InputStillStreaming => {
ToolUseStatus::InputStillStreaming
}
}
} else {
ToolUseStatus::Pending
}
})();
let (icon, needs_confirmation) =
if let Some(tool) = self.tools.read(cx).tool(&tool_use.name, cx) {
(tool.icon(), tool.needs_confirmation(&tool_use.input, cx))
} else {
(IconName::Cog, false)
};
tool_uses.push(ToolUse {
id: tool_use.id.clone(),
name: tool_use.name.clone().into(),
ui_text: self.tool_ui_label(
&tool_use.name,
&tool_use.input,
tool_use.is_input_complete,
cx,
),
input: tool_use.input.clone(),
status,
icon,
needs_confirmation,
})
}
tool_uses
}
pub fn tool_ui_label(
&self,
tool_name: &str,
input: &serde_json::Value,
is_input_complete: bool,
cx: &App,
) -> SharedString {
if let Some(tool) = self.tools.read(cx).tool(tool_name, cx) {
if is_input_complete {
tool.ui_text(input).into()
} else {
tool.still_streaming_ui_text(input).into()
}
} else {
format!("Unknown tool {tool_name:?}").into()
}
}
pub fn tool_results_for_message(
&self,
assistant_message_id: MessageId,
) -> Vec<&LanguageModelToolResult> {
let Some(tool_uses) = self
.tool_uses_by_assistant_message
.get(&assistant_message_id)
else {
return Vec::new();
};
tool_uses
.iter()
.filter_map(|tool_use| self.tool_results.get(&tool_use.id))
.collect()
}
pub fn message_has_tool_results(&self, assistant_message_id: MessageId) -> bool {
self.tool_uses_by_assistant_message
.get(&assistant_message_id)
.map_or(false, |results| !results.is_empty())
}
pub fn tool_result(
&self,
tool_use_id: &LanguageModelToolUseId,
) -> Option<&LanguageModelToolResult> {
self.tool_results.get(tool_use_id)
}
pub fn tool_result_card(&self, tool_use_id: &LanguageModelToolUseId) -> Option<&AnyToolCard> {
self.tool_result_cards.get(tool_use_id)
}
pub fn insert_tool_result_card(
&mut self,
tool_use_id: LanguageModelToolUseId,
card: AnyToolCard,
) {
self.tool_result_cards.insert(tool_use_id, card);
}
pub fn request_tool_use(
&mut self,
assistant_message_id: MessageId,
tool_use: LanguageModelToolUse,
metadata: ToolUseMetadata,
cx: &App,
) -> Arc<str> {
let tool_uses = self
.tool_uses_by_assistant_message
.entry(assistant_message_id)
.or_default();
let mut existing_tool_use_found = false;
for existing_tool_use in tool_uses.iter_mut() {
if existing_tool_use.id == tool_use.id {
*existing_tool_use = tool_use.clone();
existing_tool_use_found = true;
}
}
if !existing_tool_use_found {
tool_uses.push(tool_use.clone());
}
let status = if tool_use.is_input_complete {
self.tool_use_metadata_by_id
.insert(tool_use.id.clone(), metadata);
PendingToolUseStatus::Idle
} else {
PendingToolUseStatus::InputStillStreaming
};
let ui_text: Arc<str> = self
.tool_ui_label(
&tool_use.name,
&tool_use.input,
tool_use.is_input_complete,
cx,
)
.into();
let may_perform_edits = self
.tools
.read(cx)
.tool(&tool_use.name, cx)
.is_some_and(|tool| tool.may_perform_edits());
self.pending_tool_uses_by_id.insert(
tool_use.id.clone(),
PendingToolUse {
assistant_message_id,
id: tool_use.id,
name: tool_use.name.clone(),
ui_text: ui_text.clone(),
input: tool_use.input,
may_perform_edits,
status,
},
);
ui_text
}
pub fn run_pending_tool(
&mut self,
tool_use_id: LanguageModelToolUseId,
ui_text: SharedString,
task: Task<()>,
) {
if let Some(tool_use) = self.pending_tool_uses_by_id.get_mut(&tool_use_id) {
tool_use.ui_text = ui_text.into();
tool_use.status = PendingToolUseStatus::Running {
_task: task.shared(),
};
}
}
pub fn confirm_tool_use(
&mut self,
tool_use_id: LanguageModelToolUseId,
ui_text: impl Into<Arc<str>>,
input: serde_json::Value,
request: Arc<LanguageModelRequest>,
tool: Arc<dyn Tool>,
) {
if let Some(tool_use) = self.pending_tool_uses_by_id.get_mut(&tool_use_id) {
let ui_text = ui_text.into();
tool_use.ui_text = ui_text.clone();
let confirmation = Confirmation {
tool_use_id,
input,
request,
tool,
ui_text,
};
tool_use.status = PendingToolUseStatus::NeedsConfirmation(Arc::new(confirmation));
}
}
pub fn insert_tool_output(
&mut self,
tool_use_id: LanguageModelToolUseId,
tool_name: Arc<str>,
output: Result<ToolResultOutput>,
configured_model: Option<&ConfiguredModel>,
) -> Option<PendingToolUse> {
let metadata = self.tool_use_metadata_by_id.remove(&tool_use_id);
telemetry::event!(
"Agent Tool Finished",
model = metadata
.as_ref()
.map(|metadata| metadata.model.telemetry_id()),
model_provider = metadata
.as_ref()
.map(|metadata| metadata.model.provider_id().to_string()),
thread_id = metadata.as_ref().map(|metadata| metadata.thread_id.clone()),
prompt_id = metadata.as_ref().map(|metadata| metadata.prompt_id.clone()),
tool_name,
success = output.is_ok()
);
match output {
Ok(output) => {
let tool_result = output.content;
const BYTES_PER_TOKEN_ESTIMATE: usize = 3;
let old_use = self.pending_tool_uses_by_id.remove(&tool_use_id);
// Protect from overly large output
let tool_output_limit = configured_model
.map(|model| model.model.max_token_count() as usize * BYTES_PER_TOKEN_ESTIMATE)
.unwrap_or(usize::MAX);
let content = match tool_result {
ToolResultContent::Text(text) => {
let text = if text.len() < tool_output_limit {
text
} else {
let truncated = truncate_lines_to_byte_limit(&text, tool_output_limit);
format!(
"Tool result too long. The first {} bytes:\n\n{}",
truncated.len(),
truncated
)
};
LanguageModelToolResultContent::Text(text.into())
}
ToolResultContent::Image(language_model_image) => {
if language_model_image.estimate_tokens() < tool_output_limit {
LanguageModelToolResultContent::Image(language_model_image)
} else {
self.tool_results.insert(
tool_use_id.clone(),
LanguageModelToolResult {
tool_use_id: tool_use_id.clone(),
tool_name,
content: "Tool responded with an image that would exceeded the remaining tokens".into(),
is_error: true,
output: None,
},
);
return old_use;
}
}
};
self.tool_results.insert(
tool_use_id.clone(),
LanguageModelToolResult {
tool_use_id: tool_use_id.clone(),
tool_name,
content,
is_error: false,
output: output.output,
},
);
old_use
}
Err(err) => {
self.tool_results.insert(
tool_use_id.clone(),
LanguageModelToolResult {
tool_use_id: tool_use_id.clone(),
tool_name,
content: LanguageModelToolResultContent::Text(err.to_string().into()),
is_error: true,
output: None,
},
);
if let Some(tool_use) = self.pending_tool_uses_by_id.get_mut(&tool_use_id) {
tool_use.status = PendingToolUseStatus::Error(err.to_string().into());
}
self.pending_tool_uses_by_id.get(&tool_use_id).cloned()
}
}
}
pub fn has_tool_results(&self, assistant_message_id: MessageId) -> bool {
self.tool_uses_by_assistant_message
.contains_key(&assistant_message_id)
}
pub fn tool_results(
&self,
assistant_message_id: MessageId,
) -> impl Iterator<Item = (&LanguageModelToolUse, Option<&LanguageModelToolResult>)> {
self.tool_uses_by_assistant_message
.get(&assistant_message_id)
.into_iter()
.flatten()
.map(|tool_use| (tool_use, self.tool_results.get(&tool_use.id)))
}
}
#[derive(Debug, Clone)]
pub struct PendingToolUse {
pub id: LanguageModelToolUseId,
/// The ID of the Assistant message in which the tool use was requested.
#[allow(unused)]
pub assistant_message_id: MessageId,
pub name: Arc<str>,
pub ui_text: Arc<str>,
pub input: serde_json::Value,
pub status: PendingToolUseStatus,
pub may_perform_edits: bool,
}
#[derive(Debug, Clone)]
pub struct Confirmation {
pub tool_use_id: LanguageModelToolUseId,
pub input: serde_json::Value,
pub ui_text: Arc<str>,
pub request: Arc<LanguageModelRequest>,
pub tool: Arc<dyn Tool>,
}
#[derive(Debug, Clone)]
pub enum PendingToolUseStatus {
InputStillStreaming,
Idle,
NeedsConfirmation(Arc<Confirmation>),
Running { _task: Shared<Task<()>> },
Error(#[allow(unused)] Arc<str>),
}
impl PendingToolUseStatus {
pub fn is_idle(&self) -> bool {
matches!(self, PendingToolUseStatus::Idle)
}
pub fn is_error(&self) -> bool {
matches!(self, PendingToolUseStatus::Error(_))
}
pub fn needs_confirmation(&self) -> bool {
matches!(self, PendingToolUseStatus::NeedsConfirmation { .. })
}
}
#[derive(Clone)]
pub struct ToolUseMetadata {
pub model: Arc<dyn LanguageModel>,
pub thread_id: ThreadId,
pub prompt_id: PromptId,
}

View File

@@ -1 +0,0 @@
pub struct ZedAgentThread {}

View File

@@ -6,9 +6,10 @@ use anyhow::{Result, bail};
use collections::IndexMap;
use gpui::{App, Pixels, SharedString};
use language_model::LanguageModel;
use schemars::{JsonSchema, schema::Schema};
use schemars::{JsonSchema, json_schema};
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
use std::borrow::Cow;
pub use crate::agent_profile::*;
@@ -49,7 +50,7 @@ pub struct AgentSettings {
pub dock: AgentDockPosition,
pub default_width: Pixels,
pub default_height: Pixels,
pub default_model: LanguageModelSelection,
pub default_model: Option<LanguageModelSelection>,
pub inline_assistant_model: Option<LanguageModelSelection>,
pub commit_message_model: Option<LanguageModelSelection>,
pub thread_summary_model: Option<LanguageModelSelection>,
@@ -211,7 +212,6 @@ impl AgentSettingsContent {
}
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, Default)]
#[schemars(deny_unknown_fields)]
pub struct AgentSettingsContent {
/// Whether the Agent is enabled.
///
@@ -321,29 +321,27 @@ pub struct LanguageModelSelection {
pub struct LanguageModelProviderSetting(pub String);
impl JsonSchema for LanguageModelProviderSetting {
fn schema_name() -> String {
fn schema_name() -> Cow<'static, str> {
"LanguageModelProviderSetting".into()
}
fn json_schema(_: &mut schemars::r#gen::SchemaGenerator) -> Schema {
schemars::schema::SchemaObject {
enum_values: Some(vec![
"anthropic".into(),
"amazon-bedrock".into(),
"google".into(),
"lmstudio".into(),
"ollama".into(),
"openai".into(),
"zed.dev".into(),
"copilot_chat".into(),
"deepseek".into(),
"openrouter".into(),
"mistral".into(),
"vercel".into(),
]),
..Default::default()
}
.into()
fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
json_schema!({
"enum": [
"anthropic",
"amazon-bedrock",
"google",
"lmstudio",
"ollama",
"openai",
"zed.dev",
"copilot_chat",
"deepseek",
"openrouter",
"mistral",
"vercel"
]
})
}
}
@@ -359,15 +357,6 @@ impl From<&str> for LanguageModelProviderSetting {
}
}
impl Default for LanguageModelSelection {
fn default() -> Self {
Self {
provider: LanguageModelProviderSetting("openai".to_string()),
model: "gpt-4".to_string(),
}
}
}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)]
pub struct AgentProfileContent {
pub name: Arc<str>,
@@ -411,7 +400,10 @@ impl Settings for AgentSettings {
&mut settings.default_height,
value.default_height.map(Into::into),
);
merge(&mut settings.default_model, value.default_model.clone());
settings.default_model = value
.default_model
.clone()
.or(settings.default_model.take());
settings.inline_assistant_model = value
.inline_assistant_model
.clone()

View File

@@ -13,12 +13,10 @@ path = "src/agent_ui.rs"
doctest = false
[features]
test-support = [
"gpui/test-support",
"language/test-support",
]
test-support = ["gpui/test-support", "language/test-support"]
[dependencies]
acp.workspace = true
agent.workspace = true
agent_settings.workspace = true
anyhow.workspace = true

File diff suppressed because it is too large Load Diff

View File

@@ -16,7 +16,9 @@ use gpui::{
Focusable, ScrollHandle, Subscription, Task, Transformation, WeakEntity, percentage,
};
use language::LanguageRegistry;
use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry};
use language_model::{
LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID,
};
use notifications::status_toast::{StatusToast, ToastIcon};
use project::{
context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore},
@@ -86,6 +88,14 @@ impl AgentConfiguration {
let scroll_handle = ScrollHandle::new();
let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
let mut expanded_provider_configurations = HashMap::default();
if LanguageModelRegistry::read_global(cx)
.provider(&ZED_CLOUD_PROVIDER_ID)
.map_or(false, |cloud_provider| cloud_provider.must_accept_terms(cx))
{
expanded_provider_configurations.insert(ZED_CLOUD_PROVIDER_ID, true);
}
let mut this = Self {
fs,
language_registry,
@@ -94,7 +104,7 @@ impl AgentConfiguration {
configuration_views_by_provider: HashMap::default(),
context_server_store,
expanded_context_server_tools: HashMap::default(),
expanded_provider_configurations: HashMap::default(),
expanded_provider_configurations,
tools,
_registry_subscription: registry_subscription,
scroll_handle,

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}
}}
}}"#
)
@@ -439,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))
}

View File

@@ -211,7 +211,7 @@ impl AgentDiffPane {
}
fn update_title(&mut self, cx: &mut Context<Self>) {
let new_title = self.thread.read(cx).title().unwrap_or("Agent Changes");
let new_title = self.thread.read(cx).summary().unwrap_or("Agent Changes");
if new_title != self.title {
self.title = new_title;
cx.emit(EditorEvent::TitleChanged);
@@ -461,7 +461,7 @@ impl Item for AgentDiffPane {
}
fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement {
let summary = self.thread.read(cx).title().unwrap_or("Agent Changes");
let summary = self.thread.read(cx).summary().unwrap_or("Agent Changes");
Label::new(format!("Review: {}", summary))
.color(if params.selected {
Color::Default
@@ -1369,6 +1369,8 @@ impl AgentDiff {
| ThreadEvent::MessageDeleted(_)
| ThreadEvent::SummaryGenerated
| ThreadEvent::SummaryChanged
| ThreadEvent::UsePendingTools { .. }
| ThreadEvent::ToolFinished { .. }
| ThreadEvent::CheckpointChanged
| ThreadEvent::ToolConfirmationNeeded
| ThreadEvent::ToolUseLimitReached
@@ -1799,10 +1801,7 @@ mod tests {
})
.await
.unwrap();
let thread = thread_store
.update(cx, |store, cx| store.create_thread(cx))
.await
.unwrap();
let thread = thread_store.update(cx, |store, cx| store.create_thread(cx));
let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone());
let (workspace, cx) =
@@ -1967,10 +1966,7 @@ mod tests {
})
.await
.unwrap();
let thread = thread_store
.update(cx, |store, cx| store.create_thread(cx))
.await
.unwrap();
let thread = thread_store.update(cx, |store, cx| store.create_thread(cx));
let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone());
let (workspace, cx) =

View File

@@ -45,7 +45,7 @@ impl AgentModelSelector {
let registry = LanguageModelRegistry::read_global(cx);
if let Some(provider) = registry.provider(&model.provider_id())
{
thread.set_model(
thread.set_configured_model(
Some(ConfiguredModel {
provider,
model: model.clone(),

View File

@@ -7,6 +7,7 @@ use std::time::Duration;
use db::kvp::{Dismissable, KEY_VALUE_STORE};
use serde::{Deserialize, Serialize};
use crate::NewGeminiThread;
use crate::language_model_selector::ToggleModelSelector;
use crate::{
AddContextServer, AgentDiffPane, ContinueThread, ContinueWithBurnMode,
@@ -26,7 +27,7 @@ use crate::{
ui::AgentOnboardingModal,
};
use agent::{
Thread, ThreadError, ThreadEvent, ThreadId, ThreadTitle, TokenUsageRatio,
Thread, ThreadError, ThreadEvent, ThreadId, ThreadSummary, TokenUsageRatio,
context_store::ContextStore,
history_store::{HistoryEntryId, HistoryStore},
thread_store::{TextThreadStore, ThreadStore},
@@ -41,7 +42,7 @@ use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
use fs::Fs;
use gpui::{
Action, Animation, AnimationExt as _, AnyElement, App, AsyncWindowContext, ClipboardItem,
Corner, DismissEvent, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, FontWeight,
Corner, DismissEvent, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, Hsla,
KeyContext, Pixels, Subscription, Task, UpdateGlobal, WeakEntity, linear_color_stop,
linear_gradient, prelude::*, pulsating_between,
};
@@ -59,7 +60,7 @@ use theme::ThemeSettings;
use time::UtcOffset;
use ui::utils::WithRemSize;
use ui::{
Banner, CheckboxWithLabel, ContextMenu, ElevationIndex, KeyBinding, PopoverMenu,
Banner, Callout, CheckboxWithLabel, ContextMenu, ElevationIndex, KeyBinding, PopoverMenu,
PopoverMenuHandle, ProgressBar, Tab, Tooltip, Vector, VectorName, prelude::*,
};
use util::ResultExt as _;
@@ -72,7 +73,7 @@ use zed_actions::{
agent::{OpenConfiguration, OpenOnboardingModal, ResetOnboarding},
assistant::{OpenRulesLibrary, ToggleFocus},
};
use zed_llm_client::UsageLimit;
use zed_llm_client::{CompletionIntent, UsageLimit};
const AGENT_PANEL_KEY: &str = "agent_panel";
@@ -109,6 +110,12 @@ pub fn init(cx: &mut App) {
panel.update(cx, |panel, cx| panel.new_prompt_editor(window, cx));
}
})
.register_action(|workspace, _: &NewGeminiThread, window, cx| {
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
workspace.focus_panel::<AgentPanel>(window, cx);
panel.update(cx, |panel, cx| panel.new_gemini_thread(window, cx));
}
})
.register_action(|workspace, action: &OpenRulesLibrary, window, cx| {
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
workspace.focus_panel::<AgentPanel>(window, cx);
@@ -125,6 +132,7 @@ pub fn init(cx: &mut App) {
let thread = thread.read(cx).thread().clone();
AgentDiffPane::deploy_in_workspace(thread, workspace, window, cx);
}
ActiveView::AcpThread { .. } => todo!(),
ActiveView::TextThread { .. }
| ActiveView::History
| ActiveView::Configuration => {}
@@ -188,6 +196,9 @@ enum ActiveView {
message_editor: Entity<MessageEditor>,
_subscriptions: Vec<gpui::Subscription>,
},
AcpThread {
thread_view: Entity<acp::AcpThreadView>,
},
TextThread {
context_editor: Entity<TextThreadEditor>,
title_editor: Entity<Editor>,
@@ -207,7 +218,9 @@ enum WhichFontSize {
impl ActiveView {
pub fn which_font_size_used(&self) -> WhichFontSize {
match self {
ActiveView::Thread { .. } | ActiveView::History => WhichFontSize::AgentFont,
ActiveView::Thread { .. } | ActiveView::AcpThread { .. } | ActiveView::History => {
WhichFontSize::AgentFont
}
ActiveView::TextThread { .. } => WhichFontSize::BufferFont,
ActiveView::Configuration => WhichFontSize::None,
}
@@ -238,6 +251,9 @@ impl ActiveView {
thread.scroll_to_bottom(cx);
});
}
ActiveView::AcpThread { .. } => {
// todo!
}
ActiveView::TextThread { .. }
| ActiveView::History
| ActiveView::Configuration => {}
@@ -252,7 +268,7 @@ impl ActiveView {
thread.update(cx, |thread, cx| {
thread.thread().update(cx, |thread, cx| {
thread.set_title(new_summary, cx);
thread.set_summary(new_summary, cx);
});
})
}
@@ -278,7 +294,7 @@ impl ActiveView {
let editor = editor.clone();
move |_, thread, event, window, cx| match event {
ThreadEvent::SummaryGenerated => {
let summary = thread.read(cx).title().or_default();
let summary = thread.read(cx).summary().or_default();
editor.update(cx, |editor, cx| {
editor.set_text(summary, window, cx);
@@ -492,15 +508,10 @@ impl AgentPanel {
None
};
let thread = thread_store
.update(cx, |this, cx| this.create_thread(cx))?
.await?;
let panel = workspace.update_in(cx, |workspace, window, cx| {
let panel = cx.new(|cx| {
Self::new(
workspace,
thread,
thread_store,
context_store,
prompt_store,
@@ -523,13 +534,13 @@ impl AgentPanel {
fn new(
workspace: &Workspace,
thread: Entity<Thread>,
thread_store: Entity<ThreadStore>,
context_store: Entity<TextThreadStore>,
prompt_store: Option<Entity<PromptStore>>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let thread = thread_store.update(cx, |this, cx| this.create_thread(cx));
let fs = workspace.app_state().fs.clone();
let user_store = workspace.app_state().user_store.clone();
let project = workspace.project();
@@ -652,12 +663,14 @@ impl AgentPanel {
|this, _, event: &language_model::Event, cx| match event {
language_model::Event::DefaultModelChanged => match &this.active_view {
ActiveView::Thread { thread, .. } => {
// todo!(do we need this?);
// thread
// .read(cx)
// .thread()
// .clone()
// .update(cx, |thread, cx| thread.get_or_init_configured_model(cx));
thread
.read(cx)
.thread()
.clone()
.update(cx, |thread, cx| thread.get_or_init_configured_model(cx));
}
ActiveView::AcpThread { .. } => {
// todo!
}
ActiveView::TextThread { .. }
| ActiveView::History
@@ -739,6 +752,9 @@ impl AgentPanel {
ActiveView::Thread { thread, .. } => {
thread.update(cx, |thread, cx| thread.cancel_last_completion(window, cx));
}
ActiveView::AcpThread { thread_view, .. } => {
thread_view.update(cx, |thread_element, cx| thread_element.cancel(cx));
}
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {}
}
}
@@ -746,6 +762,10 @@ impl AgentPanel {
fn active_message_editor(&self) -> Option<&Entity<MessageEditor>> {
match &self.active_view {
ActiveView::Thread { message_editor, .. } => Some(message_editor),
ActiveView::AcpThread { .. } => {
// todo!
None
}
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => None,
}
}
@@ -790,61 +810,46 @@ impl AgentPanel {
.detach_and_log_err(cx);
}
let fs = self.fs.clone();
let user_store = self.user_store.clone();
let thread_store = self.thread_store.clone();
let text_thread_store = self.context_store.clone();
let prompt_store = self.prompt_store.clone();
let language_registry = self.language_registry.clone();
let workspace = self.workspace.clone();
let active_thread = cx.new(|cx| {
ActiveThread::new(
thread.clone(),
self.thread_store.clone(),
self.context_store.clone(),
context_store.clone(),
self.language_registry.clone(),
self.workspace.clone(),
window,
cx,
)
});
cx.spawn_in(window, async move |this, cx| {
let thread = thread.await?;
let active_thread = cx.new_window_entity(|window, cx| {
ActiveThread::new(
thread.clone(),
thread_store.clone(),
text_thread_store.clone(),
context_store.clone(),
language_registry.clone(),
workspace.clone(),
window,
cx,
)
})?;
let message_editor = cx.new(|cx| {
MessageEditor::new(
self.fs.clone(),
self.workspace.clone(),
self.user_store.clone(),
context_store.clone(),
self.prompt_store.clone(),
self.thread_store.downgrade(),
self.context_store.downgrade(),
thread.clone(),
window,
cx,
)
});
let message_editor = cx.new_window_entity(|window, cx| {
MessageEditor::new(
fs.clone(),
workspace.clone(),
user_store.clone(),
context_store.clone(),
prompt_store.clone(),
thread_store.downgrade(),
text_thread_store.downgrade(),
thread.clone(),
window,
cx,
)
})?;
if let Some(text) = preserved_text {
message_editor.update(cx, |editor, cx| {
editor.set_text(text, window, cx);
});
}
if let Some(text) = preserved_text {
message_editor.update_in(cx, |editor, window, cx| {
editor.set_text(text, window, cx);
});
}
message_editor.focus_handle(cx).focus(window);
this.update_in(cx, |this, window, cx| {
message_editor.focus_handle(cx).focus(window);
let thread_view = ActiveView::thread(active_thread.clone(), message_editor, window, cx);
self.set_active_view(thread_view, window, cx);
let thread_view =
ActiveView::thread(active_thread.clone(), message_editor, window, cx);
this.set_active_view(thread_view, window, cx);
AgentDiff::set_active_thread(&this.workspace, &thread, window, cx);
})
})
.detach_and_log_err(cx);
AgentDiff::set_active_thread(&self.workspace, &thread, window, cx);
}
fn new_prompt_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
@@ -883,6 +888,19 @@ impl AgentPanel {
context_editor.focus_handle(cx).focus(window);
}
fn new_gemini_thread(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let project = self.project.clone();
cx.spawn_in(window, async move |this, cx| {
let thread_view =
cx.new_window_entity(|window, cx| acp::AcpThreadView::new(project, window, cx))?;
this.update_in(cx, |this, window, cx| {
this.set_active_view(ActiveView::AcpThread { thread_view }, window, cx);
})
})
.detach();
}
fn deploy_rules_library(
&mut self,
action: &OpenRulesLibrary,
@@ -1015,6 +1033,7 @@ impl AgentPanel {
cx,
)
});
let message_editor = cx.new(|cx| {
MessageEditor::new(
self.fs.clone(),
@@ -1039,6 +1058,7 @@ impl AgentPanel {
pub fn go_back(&mut self, _: &workspace::GoBack, window: &mut Window, cx: &mut Context<Self>) {
match self.active_view {
ActiveView::Configuration | ActiveView::History => {
// todo! check go back works correctly
if let Some(previous_view) = self.previous_view.take() {
self.active_view = previous_view;
@@ -1046,6 +1066,9 @@ impl AgentPanel {
ActiveView::Thread { message_editor, .. } => {
message_editor.focus_handle(cx).focus(window);
}
ActiveView::AcpThread { .. } => {
todo!()
}
ActiveView::TextThread { context_editor, .. } => {
context_editor.focus_handle(cx).focus(window);
}
@@ -1165,6 +1188,7 @@ impl AgentPanel {
})
.log_err();
}
ActiveView::AcpThread { .. } => todo!(),
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {}
}
}
@@ -1218,6 +1242,9 @@ impl AgentPanel {
)
.detach_and_log_err(cx);
}
ActiveView::AcpThread { .. } => {
todo!()
}
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {}
}
}
@@ -1252,6 +1279,10 @@ impl AgentPanel {
pub(crate) fn active_thread(&self, cx: &App) -> Option<Entity<Thread>> {
match &self.active_view {
ActiveView::Thread { thread, .. } => Some(thread.read(cx).thread().clone()),
ActiveView::AcpThread { .. } => {
// todo!
None
}
_ => None,
}
}
@@ -1275,11 +1306,23 @@ impl AgentPanel {
return;
}
thread.update(cx, |active_thread, cx| {
active_thread
.thread()
.update(cx, |thread, cx| thread.resume(window, cx))
});
let model = thread_state.configured_model().map(|cm| cm.model.clone());
if let Some(model) = model {
thread.update(cx, |active_thread, cx| {
active_thread.thread().update(cx, |thread, cx| {
thread.insert_invisible_continue_message(cx);
thread.advance_prompt_id();
thread.send_to_model(
model,
CompletionIntent::UserPrompt,
Some(window.window_handle()),
cx,
);
});
});
} else {
log::warn!("No configured model available for continuation");
}
}
fn toggle_burn_mode(
@@ -1345,6 +1388,9 @@ impl AgentPanel {
});
}
}
ActiveView::AcpThread { .. } => {
// todo!
}
_ => {}
}
@@ -1360,6 +1406,9 @@ impl AgentPanel {
}
})
}
ActiveView::AcpThread { .. } => {
// todo! push history entry
}
_ => {}
}
@@ -1446,6 +1495,7 @@ impl Focusable for AgentPanel {
fn focus_handle(&self, cx: &App) -> FocusHandle {
match &self.active_view {
ActiveView::Thread { message_editor, .. } => message_editor.focus_handle(cx),
ActiveView::AcpThread { thread_view, .. } => thread_view.focus_handle(cx),
ActiveView::History => self.history.focus_handle(cx),
ActiveView::TextThread { context_editor, .. } => context_editor.focus_handle(cx),
ActiveView::Configuration => {
@@ -1561,24 +1611,24 @@ impl AgentPanel {
let state = {
let active_thread = active_thread.read(cx);
if active_thread.is_empty() {
&ThreadTitle::Pending
&ThreadSummary::Pending
} else {
active_thread.summary(cx)
}
};
match state {
ThreadTitle::Pending => Label::new(ThreadTitle::DEFAULT.clone())
ThreadSummary::Pending => Label::new(ThreadSummary::DEFAULT.clone())
.truncate()
.into_any_element(),
ThreadTitle::Generating => Label::new(LOADING_SUMMARY_PLACEHOLDER)
ThreadSummary::Generating => Label::new(LOADING_SUMMARY_PLACEHOLDER)
.truncate()
.into_any_element(),
ThreadTitle::Ready(_) => div()
ThreadSummary::Ready(_) => div()
.w_full()
.child(change_title_editor.clone())
.into_any_element(),
ThreadTitle::Error => h_flex()
ThreadSummary::Error => h_flex()
.w_full()
.child(change_title_editor.clone())
.child(
@@ -1602,6 +1652,9 @@ impl AgentPanel {
.into_any_element(),
}
}
ActiveView::AcpThread { thread_view } => Label::new(thread_view.read(cx).title(cx))
.truncate()
.into_any_element(),
ActiveView::TextThread {
title_editor,
context_editor,
@@ -1736,6 +1789,10 @@ impl AgentPanel {
let active_thread = match &self.active_view {
ActiveView::Thread { thread, .. } => Some(thread.read(cx).thread().clone()),
ActiveView::AcpThread { .. } => {
// todo!
None
}
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => None,
};
@@ -1764,6 +1821,7 @@ impl AgentPanel {
menu = menu
.action("New Thread", NewThread::default().boxed_clone())
.action("New Text Thread", NewTextThread.boxed_clone())
.action("New Gemini Thread", NewGeminiThread.boxed_clone())
.when_some(active_thread, |this, active_thread| {
let thread = active_thread.read(cx);
if !thread.is_empty() {
@@ -1902,6 +1960,10 @@ impl AgentPanel {
message_editor,
..
} => (thread.read(cx), message_editor.read(cx)),
ActiveView::AcpThread { .. } => {
// todo!
return None;
}
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {
return None;
}
@@ -2033,15 +2095,17 @@ impl AgentPanel {
.read(cx)
.thread()
.read(cx)
.model()
.map_or(false, |model| {
model.provider.id().0 == ZED_CLOUD_PROVIDER_ID
});
.configured_model()
.map_or(false, |model| model.provider.id() == ZED_CLOUD_PROVIDER_ID);
if !is_using_zed_provider {
return false;
}
}
ActiveView::AcpThread { .. } => {
// todo!
return false;
}
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {
return false;
}
@@ -2609,7 +2673,7 @@ impl AgentPanel {
Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
parent.child(Banner::new().severity(ui::Severity::Warning).child(
h_flex().w_full().children(provider.render_accept_terms(
LanguageModelProviderTosView::ThreadtEmptyState,
LanguageModelProviderTosView::ThreadEmptyState,
cx,
)),
))
@@ -2626,6 +2690,10 @@ impl AgentPanel {
) -> Option<AnyElement> {
let active_thread = match &self.active_view {
ActiveView::Thread { thread, .. } => thread,
ActiveView::AcpThread { .. } => {
// todo!
return None;
}
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {
return None;
}
@@ -2638,7 +2706,7 @@ impl AgentPanel {
return None;
}
let model = thread.model()?.model;
let model = thread.configured_model()?.model;
let focus_handle = self.focus_handle(cx);
@@ -2700,58 +2768,90 @@ impl AgentPanel {
Some(div().px_2().pb_2().child(banner).into_any_element())
}
fn create_copy_button(&self, message: impl Into<String>) -> impl IntoElement {
let message = message.into();
IconButton::new("copy", IconName::Copy)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.tooltip(Tooltip::text("Copy Error Message"))
.on_click(move |_, _, cx| {
cx.write_to_clipboard(ClipboardItem::new_string(message.clone()))
})
}
fn dismiss_error_button(
&self,
thread: &Entity<ActiveThread>,
cx: &mut Context<Self>,
) -> impl IntoElement {
IconButton::new("dismiss", IconName::Close)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.tooltip(Tooltip::text("Dismiss Error"))
.on_click(cx.listener({
let thread = thread.clone();
move |_, _, _, cx| {
thread.update(cx, |this, _cx| {
this.clear_last_error();
});
cx.notify();
}
}))
}
fn upgrade_button(
&self,
thread: &Entity<ActiveThread>,
cx: &mut Context<Self>,
) -> impl IntoElement {
Button::new("upgrade", "Upgrade")
.label_size(LabelSize::Small)
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
.on_click(cx.listener({
let thread = thread.clone();
move |_, _, _, cx| {
thread.update(cx, |this, _cx| {
this.clear_last_error();
});
cx.open_url(&zed_urls::account_url(cx));
cx.notify();
}
}))
}
fn error_callout_bg(&self, cx: &Context<Self>) -> Hsla {
cx.theme().status().error.opacity(0.08)
}
fn render_payment_required_error(
&self,
thread: &Entity<ActiveThread>,
cx: &mut Context<Self>,
) -> AnyElement {
const ERROR_MESSAGE: &str = "Free tier exceeded. Subscribe and add payment to continue using Zed LLMs. You'll be billed at cost for tokens used.";
const ERROR_MESSAGE: &str =
"You reached your free usage limit. Upgrade to Zed Pro for more prompts.";
v_flex()
.gap_0p5()
.child(
h_flex()
.gap_1p5()
.items_center()
.child(Icon::new(IconName::XCircle).color(Color::Error))
.child(Label::new("Free Usage Exceeded").weight(FontWeight::MEDIUM)),
)
.child(
div()
.id("error-message")
.max_h_24()
.overflow_y_scroll()
.child(Label::new(ERROR_MESSAGE)),
)
.child(
h_flex()
.justify_end()
.mt_1()
.gap_1()
.child(self.create_copy_button(ERROR_MESSAGE))
.child(Button::new("subscribe", "Subscribe").on_click(cx.listener({
let thread = thread.clone();
move |_, _, _, cx| {
thread.update(cx, |this, _cx| {
this.clear_last_error();
});
let icon = Icon::new(IconName::XCircle)
.size(IconSize::Small)
.color(Color::Error);
cx.open_url(&zed_urls::account_url(cx));
cx.notify();
}
})))
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener({
let thread = thread.clone();
move |_, _, _, cx| {
thread.update(cx, |this, _cx| {
this.clear_last_error();
});
cx.notify();
}
}))),
div()
.border_t_1()
.border_color(cx.theme().colors().border)
.child(
Callout::new()
.icon(icon)
.title("Free Usage Exceeded")
.description(ERROR_MESSAGE)
.tertiary_action(self.upgrade_button(thread, cx))
.secondary_action(self.create_copy_button(ERROR_MESSAGE))
.primary_action(self.dismiss_error_button(thread, cx))
.bg_color(self.error_callout_bg(cx)),
)
.into_any()
.into_any_element()
}
fn render_model_request_limit_reached_error(
@@ -2761,67 +2861,28 @@ impl AgentPanel {
cx: &mut Context<Self>,
) -> AnyElement {
let error_message = match plan {
Plan::ZedPro => {
"Model request limit reached. Upgrade to usage-based billing for more requests."
}
Plan::ZedProTrial => {
"Model request limit reached. Upgrade to Zed Pro for more requests."
}
Plan::Free => "Model request limit reached. Upgrade to Zed Pro for more requests.",
};
let call_to_action = match plan {
Plan::ZedPro => "Upgrade to usage-based billing",
Plan::ZedProTrial => "Upgrade to Zed Pro",
Plan::Free => "Upgrade to Zed Pro",
Plan::ZedPro => "Upgrade to usage-based billing for more prompts.",
Plan::ZedProTrial | Plan::Free => "Upgrade to Zed Pro for more prompts.",
};
v_flex()
.gap_0p5()
.child(
h_flex()
.gap_1p5()
.items_center()
.child(Icon::new(IconName::XCircle).color(Color::Error))
.child(Label::new("Model Request Limit Reached").weight(FontWeight::MEDIUM)),
)
.child(
div()
.id("error-message")
.max_h_24()
.overflow_y_scroll()
.child(Label::new(error_message)),
)
.child(
h_flex()
.justify_end()
.mt_1()
.gap_1()
.child(self.create_copy_button(error_message))
.child(
Button::new("subscribe", call_to_action).on_click(cx.listener({
let thread = thread.clone();
move |_, _, _, cx| {
thread.update(cx, |this, _cx| {
this.clear_last_error();
});
let icon = Icon::new(IconName::XCircle)
.size(IconSize::Small)
.color(Color::Error);
cx.open_url(&zed_urls::account_url(cx));
cx.notify();
}
})),
)
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener({
let thread = thread.clone();
move |_, _, _, cx| {
thread.update(cx, |this, _cx| {
this.clear_last_error();
});
cx.notify();
}
}))),
div()
.border_t_1()
.border_color(cx.theme().colors().border)
.child(
Callout::new()
.icon(icon)
.title("Model Prompt Limit Reached")
.description(error_message)
.tertiary_action(self.upgrade_button(thread, cx))
.secondary_action(self.create_copy_button(error_message))
.primary_action(self.dismiss_error_button(thread, cx))
.bg_color(self.error_callout_bg(cx)),
)
.into_any()
.into_any_element()
}
fn render_error_message(
@@ -2832,40 +2893,24 @@ impl AgentPanel {
cx: &mut Context<Self>,
) -> AnyElement {
let message_with_header = format!("{}\n{}", header, message);
v_flex()
.gap_0p5()
.child(
h_flex()
.gap_1p5()
.items_center()
.child(Icon::new(IconName::XCircle).color(Color::Error))
.child(Label::new(header).weight(FontWeight::MEDIUM)),
)
.child(
div()
.id("error-message")
.max_h_32()
.overflow_y_scroll()
.child(Label::new(message.clone())),
)
.child(
h_flex()
.justify_end()
.mt_1()
.gap_1()
.child(self.create_copy_button(message_with_header))
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener({
let thread = thread.clone();
move |_, _, _, cx| {
thread.update(cx, |this, _cx| {
this.clear_last_error();
});
cx.notify();
}
}))),
let icon = Icon::new(IconName::XCircle)
.size(IconSize::Small)
.color(Color::Error);
div()
.border_t_1()
.border_color(cx.theme().colors().border)
.child(
Callout::new()
.icon(icon)
.title(header)
.description(message.clone())
.primary_action(self.dismiss_error_button(thread, cx))
.secondary_action(self.create_copy_button(message_with_header))
.bg_color(self.error_callout_bg(cx)),
)
.into_any()
.into_any_element()
}
fn render_prompt_editor(
@@ -2995,6 +3040,9 @@ impl AgentPanel {
.detach();
});
}
ActiveView::AcpThread { .. } => {
unimplemented!()
}
ActiveView::TextThread { context_editor, .. } => {
context_editor.update(cx, |context_editor, cx| {
TextThreadEditor::insert_dragged_files(
@@ -3010,15 +3058,6 @@ impl AgentPanel {
}
}
fn create_copy_button(&self, message: impl Into<String>) -> impl IntoElement {
let message = message.into();
IconButton::new("copy", IconName::Copy)
.on_click(move |_, _, cx| {
cx.write_to_clipboard(ClipboardItem::new_string(message.clone()))
})
.tooltip(Tooltip::text("Copy Error Message"))
}
fn key_context(&self) -> KeyContext {
let mut key_context = KeyContext::new_with_defaults();
key_context.add("AgentPanel");
@@ -3077,6 +3116,9 @@ impl Render for AgentPanel {
});
this.continue_conversation(window, cx);
}
ActiveView::AcpThread { .. } => {
todo!()
}
ActiveView::TextThread { .. }
| ActiveView::History
| ActiveView::Configuration => {}
@@ -3100,18 +3142,9 @@ impl Render for AgentPanel {
thread.clone().into_any_element()
})
.children(self.render_tool_use_limit_reached(window, cx))
.child(h_flex().child(message_editor.clone()))
.when_some(thread.read(cx).last_error(), |this, last_error| {
this.child(
div()
.absolute()
.right_3()
.bottom_12()
.max_w_96()
.py_2()
.px_3()
.elevation_2(cx)
.occlude()
.child(match last_error {
ThreadError::PaymentRequired => {
self.render_payment_required_error(thread, cx)
@@ -3125,6 +3158,13 @@ impl Render for AgentPanel {
.into_any(),
)
})
.child(h_flex().child(message_editor.clone()))
.child(self.render_drag_target(cx)),
ActiveView::AcpThread { thread_view, .. } => parent
.relative()
.child(thread_view.clone())
// todo!
// .child(h_flex().child(self.message_editor.clone()))
.child(self.render_drag_target(cx)),
ActiveView::History => parent.child(self.history.clone()),
ActiveView::TextThread {

View File

@@ -55,6 +55,7 @@ actions!(
agent,
[
NewTextThread,
NewGeminiThread,
ToggleContextPicker,
ToggleNavigationMenu,
ToggleOptionsMenu,
@@ -65,7 +66,6 @@ actions!(
OpenHistory,
AddContextServer,
RemoveSelectedThread,
Chat,
ChatWithFollow,
CycleNextInlineAssist,
CyclePreviousInlineAssist,
@@ -92,6 +92,7 @@ actions!(
#[derive(Default, Clone, PartialEq, Deserialize, JsonSchema, Action)]
#[action(namespace = agent)]
#[serde(deny_unknown_fields)]
pub struct NewThread {
#[serde(default)]
from_thread_id: Option<ThreadId>,
@@ -99,6 +100,7 @@ pub struct NewThread {
#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)]
#[action(namespace = agent)]
#[serde(deny_unknown_fields)]
pub struct ManageProfiles {
#[serde(default)]
pub customize_tools: Option<AgentProfileId>,
@@ -121,7 +123,7 @@ pub(crate) enum ModelUsageContext {
impl ModelUsageContext {
pub fn configured_model(&self, cx: &App) -> Option<ConfiguredModel> {
match self {
Self::Thread(thread) => thread.read(cx).model(),
Self::Thread(thread) => thread.read(cx).configured_model(),
Self::InlineAssistant => {
LanguageModelRegistry::read_global(cx).inline_assistant_model()
}
@@ -209,7 +211,7 @@ fn update_active_language_model_from_settings(cx: &mut App) {
}
}
let default = to_selected_model(&settings.default_model);
let default = settings.default_model.as_ref().map(to_selected_model);
let inline_assistant = settings
.inline_assistant_model
.as_ref()
@@ -229,7 +231,7 @@ fn update_active_language_model_from_settings(cx: &mut App) {
.collect::<Vec<_>>();
LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
registry.select_default_model(Some(&default), cx);
registry.select_default_model(default.as_ref(), cx);
registry.select_inline_assistant_model(inline_assistant.as_ref(), cx);
registry.select_commit_message_model(commit_message.as_ref(), cx);
registry.select_thread_summary_model(thread_summary.as_ref(), cx);

View File

@@ -670,7 +670,7 @@ fn recent_context_picker_entries(
let mut threads = unordered_thread_entries(thread_store, text_thread_store, cx)
.filter(|(_, thread)| match thread {
ThreadContextEntry::Thread { id, .. } => {
Some(id) != active_thread_id.as_ref() && !current_threads.contains(id)
Some(id) != active_thread_id && !current_threads.contains(id)
}
ThreadContextEntry::Context { .. } => true,
})

View File

@@ -169,13 +169,13 @@ impl ContextStrip {
if self
.context_store
.read(cx)
.includes_thread(&active_thread.id())
.includes_thread(active_thread.id())
{
return None;
}
Some(SuggestedContext::Thread {
name: active_thread.title().or_default(),
name: active_thread.summary().or_default(),
thread: weak_active_thread,
})
} else if let Some(active_context_editor) = panel.active_context_editor() {

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();
})

View File

@@ -47,13 +47,14 @@ use ui::{
};
use util::ResultExt as _;
use workspace::{CollaboratorId, Workspace};
use zed_actions::agent::Chat;
use zed_llm_client::CompletionIntent;
use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider, crease_for_mention};
use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
use crate::profile_selector::ProfileSelector;
use crate::{
ActiveThread, AgentDiffPane, Chat, ChatWithFollow, ExpandMessageEditor, Follow, KeepAll,
ActiveThread, AgentDiffPane, ChatWithFollow, ExpandMessageEditor, Follow, KeepAll,
ModelUsageContext, NewThread, OpenAgentDiff, RejectAll, RemoveAllContext, ToggleBurnMode,
ToggleContextPicker, ToggleProfileSelector, register_agent_preview,
};
@@ -387,14 +388,25 @@ impl MessageEditor {
thread
.update(cx, |thread, cx| {
todo!();
// thread.send(
// user_message,
// loaded_context,
// checkpoint.ok(),
// user_message_creases,
// cx,
// );
thread.insert_user_message(
user_message,
loaded_context,
checkpoint.ok(),
user_message_creases,
cx,
);
})
.log_err();
thread
.update(cx, |thread, cx| {
thread.advance_prompt_id();
thread.send_to_model(
model,
CompletionIntent::UserPrompt,
Some(window_handle),
cx,
);
})
.log_err();
})
@@ -1239,9 +1251,7 @@ impl MessageEditor {
self.thread
.read(cx)
.configured_model()
.map_or(false, |model| {
model.provider.id().0 == ZED_CLOUD_PROVIDER_ID
})
.map_or(false, |model| model.provider.id() == ZED_CLOUD_PROVIDER_ID)
}
fn render_usage_callout(&self, line_height: Pixels, cx: &mut Context<Self>) -> Option<Div> {

View File

@@ -156,7 +156,7 @@ impl Render for ProfileSelector {
.map(|profile| profile.name.clone())
.unwrap_or_else(|| "Unknown".into());
let configured_model = self.thread.read(cx).model().or_else(|| {
let configured_model = self.thread.read(cx).configured_model().or_else(|| {
let model_registry = LanguageModelRegistry::read_global(cx);
model_registry.default_model()
});

View File

@@ -498,7 +498,7 @@ impl AddedContext {
render_hover: {
let thread = handle.thread.clone();
Some(Rc::new(move |_, cx| {
let text = thread.read(cx).latest_detailed_summary_or_text(cx);
let text = thread.read(cx).latest_detailed_summary_or_text();
ContextPillHover::new_text(text.clone(), cx).into()
}))
},

View File

@@ -6,7 +6,7 @@ use anyhow::{Context as _, Result, anyhow};
use chrono::{DateTime, Utc};
use futures::{AsyncBufReadExt, AsyncReadExt, StreamExt, io::BufReader, stream::BoxStream};
use http_client::http::{self, HeaderMap, HeaderValue};
use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest};
use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest, StatusCode};
use serde::{Deserialize, Serialize};
use strum::{EnumIter, EnumString};
use thiserror::Error;
@@ -356,7 +356,7 @@ pub async fn complete(
.send(request)
.await
.map_err(AnthropicError::HttpSend)?;
let status = response.status();
let status_code = response.status();
let mut body = String::new();
response
.body_mut()
@@ -364,12 +364,12 @@ pub async fn complete(
.await
.map_err(AnthropicError::ReadResponse)?;
if status.is_success() {
if status_code.is_success() {
Ok(serde_json::from_str(&body).map_err(AnthropicError::DeserializeResponse)?)
} else {
Err(AnthropicError::HttpResponseError {
status: status.as_u16(),
body,
status_code,
message: body,
})
}
}
@@ -444,11 +444,7 @@ impl RateLimitInfo {
}
Self {
retry_after: headers
.get("retry-after")
.and_then(|v| v.to_str().ok())
.and_then(|v| v.parse::<u64>().ok())
.map(Duration::from_secs),
retry_after: parse_retry_after(headers),
requests: RateLimit::from_headers("requests", headers).ok(),
tokens: RateLimit::from_headers("tokens", headers).ok(),
input_tokens: RateLimit::from_headers("input-tokens", headers).ok(),
@@ -457,6 +453,17 @@ impl RateLimitInfo {
}
}
/// Parses the Retry-After header value as an integer number of seconds (anthropic always uses
/// seconds). Note that other services might specify an HTTP date or some other format for this
/// header. Returns `None` if the header is not present or cannot be parsed.
pub fn parse_retry_after(headers: &HeaderMap<HeaderValue>) -> Option<Duration> {
headers
.get("retry-after")
.and_then(|v| v.to_str().ok())
.and_then(|v| v.parse::<u64>().ok())
.map(Duration::from_secs)
}
fn get_header<'a>(key: &str, headers: &'a HeaderMap) -> anyhow::Result<&'a str> {
Ok(headers
.get(key)
@@ -520,6 +527,10 @@ pub async fn stream_completion_with_rate_limit_info(
})
.boxed();
Ok((stream, Some(rate_limits)))
} else if response.status().as_u16() == 529 {
Err(AnthropicError::ServerOverloaded {
retry_after: rate_limits.retry_after,
})
} else if let Some(retry_after) = rate_limits.retry_after {
Err(AnthropicError::RateLimit { retry_after })
} else {
@@ -532,10 +543,9 @@ pub async fn stream_completion_with_rate_limit_info(
match serde_json::from_str::<Event>(&body) {
Ok(Event::Error { error }) => Err(AnthropicError::ApiError(error)),
Ok(_) => Err(AnthropicError::UnexpectedResponseFormat(body)),
Err(_) => Err(AnthropicError::HttpResponseError {
status: response.status().as_u16(),
body: body,
Ok(_) | Err(_) => Err(AnthropicError::HttpResponseError {
status_code: response.status(),
message: body,
}),
}
}
@@ -801,16 +811,19 @@ pub enum AnthropicError {
ReadResponse(io::Error),
/// HTTP error response from the API
HttpResponseError { status: u16, body: String },
HttpResponseError {
status_code: StatusCode,
message: String,
},
/// Rate limit exceeded
RateLimit { retry_after: Duration },
/// Server overloaded
ServerOverloaded { retry_after: Option<Duration> },
/// API returned an error response
ApiError(ApiError),
/// Unexpected response format
UnexpectedResponseFormat(String),
}
#[derive(Debug, Serialize, Deserialize, Error)]

View File

@@ -2140,7 +2140,8 @@ impl AssistantContext {
);
}
LanguageModelCompletionEvent::ToolUse(_) |
LanguageModelCompletionEvent::UsageUpdate(_) => {}
LanguageModelCompletionEvent::ToolUseJsonParseError { .. } |
LanguageModelCompletionEvent::UsageUpdate(_) => {}
}
});

View File

@@ -29,6 +29,7 @@ use std::{
path::Path,
str::FromStr,
sync::mpsc,
time::Duration,
};
use util::path;
@@ -1658,12 +1659,14 @@ async fn retry_on_rate_limit<R>(mut request: impl AsyncFnMut() -> Result<R>) ->
match request().await {
Ok(result) => return Ok(result),
Err(err) => match err.downcast::<LanguageModelCompletionError>() {
Ok(err) => match err {
LanguageModelCompletionError::RateLimitExceeded { retry_after } => {
Ok(err) => match &err {
LanguageModelCompletionError::RateLimitExceeded { retry_after, .. }
| LanguageModelCompletionError::ServerOverloaded { retry_after, .. } => {
let retry_after = retry_after.unwrap_or(Duration::from_secs(5));
// Wait for the duration supplied, with some jitter to avoid all requests being made at the same time.
let jitter = retry_after.mul_f64(rand::thread_rng().gen_range(0.0..1.0));
eprintln!(
"Attempt #{attempt}: Rate limit exceeded. Retry after {retry_after:?} + jitter of {jitter:?}"
"Attempt #{attempt}: {err}. Retry after {retry_after:?} + jitter of {jitter:?}"
);
Timer::after(retry_after + jitter).await;
continue;

View File

@@ -1,8 +1,9 @@
use anyhow::Result;
use language_model::LanguageModelToolSchemaFormat;
use schemars::{
JsonSchema,
schema::{RootSchema, Schema, SchemaObject},
JsonSchema, Schema,
generate::SchemaSettings,
transform::{Transform, transform_subschemas},
};
pub fn json_schema_for<T: JsonSchema>(
@@ -13,7 +14,7 @@ pub fn json_schema_for<T: JsonSchema>(
}
fn schema_to_json(
schema: &RootSchema,
schema: &Schema,
format: LanguageModelToolSchemaFormat,
) -> Result<serde_json::Value> {
let mut value = serde_json::to_value(schema)?;
@@ -21,58 +22,42 @@ fn schema_to_json(
Ok(value)
}
fn root_schema_for<T: JsonSchema>(format: LanguageModelToolSchemaFormat) -> RootSchema {
fn root_schema_for<T: JsonSchema>(format: LanguageModelToolSchemaFormat) -> Schema {
let mut generator = match format {
LanguageModelToolSchemaFormat::JsonSchema => schemars::SchemaGenerator::default(),
LanguageModelToolSchemaFormat::JsonSchemaSubset => {
schemars::r#gen::SchemaSettings::default()
.with(|settings| {
settings.meta_schema = None;
settings.inline_subschemas = true;
settings
.visitors
.push(Box::new(TransformToJsonSchemaSubsetVisitor));
})
.into_generator()
}
LanguageModelToolSchemaFormat::JsonSchema => SchemaSettings::draft07().into_generator(),
// TODO: Gemini docs mention using a subset of OpenAPI 3, so this may benefit from using
// `SchemaSettings::openapi3()`.
LanguageModelToolSchemaFormat::JsonSchemaSubset => SchemaSettings::draft07()
.with(|settings| {
settings.meta_schema = None;
settings.inline_subschemas = true;
})
.with_transform(ToJsonSchemaSubsetTransform)
.into_generator(),
};
generator.root_schema_for::<T>()
}
#[derive(Debug, Clone)]
struct TransformToJsonSchemaSubsetVisitor;
struct ToJsonSchemaSubsetTransform;
impl schemars::visit::Visitor for TransformToJsonSchemaSubsetVisitor {
fn visit_root_schema(&mut self, root: &mut RootSchema) {
schemars::visit::visit_root_schema(self, root)
}
fn visit_schema(&mut self, schema: &mut Schema) {
schemars::visit::visit_schema(self, schema)
}
fn visit_schema_object(&mut self, schema: &mut SchemaObject) {
impl Transform for ToJsonSchemaSubsetTransform {
fn transform(&mut self, schema: &mut Schema) {
// Ensure that the type field is not an array, this happens when we use
// Option<T>, the type will be [T, "null"].
if let Some(instance_type) = schema.instance_type.take() {
schema.instance_type = match instance_type {
schemars::schema::SingleOrVec::Single(t) => {
Some(schemars::schema::SingleOrVec::Single(t))
if let Some(type_field) = schema.get_mut("type") {
if let Some(types) = type_field.as_array() {
if let Some(first_type) = types.first() {
*type_field = first_type.clone();
}
schemars::schema::SingleOrVec::Vec(items) => items
.into_iter()
.next()
.map(schemars::schema::SingleOrVec::from),
};
}
// One of is not supported, use anyOf instead.
if let Some(subschema) = schema.subschemas.as_mut() {
if let Some(one_of) = subschema.one_of.take() {
subschema.any_of = Some(one_of);
}
}
schemars::visit::visit_schema_object(self, schema)
// oneOf is not supported, use anyOf instead
if let Some(one_of) = schema.remove("oneOf") {
schema.insert("anyOf".to_string(), one_of);
}
transform_subschemas(self, schema);
}
}

View File

@@ -25,5 +25,4 @@ serde.workspace = true
serde_json.workspace = true
strum.workspace = true
thiserror.workspace = true
tokio = { workspace = true, features = ["rt", "rt-multi-thread"] }
workspace-hack.workspace = true

View File

@@ -1,9 +1,6 @@
mod models;
use std::collections::HashMap;
use std::pin::Pin;
use anyhow::{Context as _, Error, Result, anyhow};
use anyhow::{Context, Error, Result, anyhow};
use aws_sdk_bedrockruntime as bedrock;
pub use aws_sdk_bedrockruntime as bedrock_client;
pub use aws_sdk_bedrockruntime::types::{
@@ -24,9 +21,10 @@ pub use bedrock::types::{
ToolResultContentBlock as BedrockToolResultContentBlock,
ToolResultStatus as BedrockToolResultStatus, ToolUseBlock as BedrockToolUseBlock,
};
use futures::stream::{self, BoxStream, Stream};
use futures::stream::{self, BoxStream};
use serde::{Deserialize, Serialize};
use serde_json::{Number, Value};
use std::collections::HashMap;
use thiserror::Error;
pub use crate::models::*;
@@ -34,70 +32,59 @@ pub use crate::models::*;
pub async fn stream_completion(
client: bedrock::Client,
request: Request,
handle: tokio::runtime::Handle,
) -> Result<BoxStream<'static, Result<BedrockStreamingResponse, BedrockError>>, Error> {
handle
.spawn(async move {
let mut response = bedrock::Client::converse_stream(&client)
.model_id(request.model.clone())
.set_messages(request.messages.into());
let mut response = bedrock::Client::converse_stream(&client)
.model_id(request.model.clone())
.set_messages(request.messages.into());
if let Some(Thinking::Enabled {
budget_tokens: Some(budget_tokens),
}) = request.thinking
{
response =
response.additional_model_request_fields(Document::Object(HashMap::from([(
"thinking".to_string(),
Document::from(HashMap::from([
("type".to_string(), Document::String("enabled".to_string())),
(
"budget_tokens".to_string(),
Document::Number(AwsNumber::PosInt(budget_tokens)),
),
])),
)])));
}
if let Some(Thinking::Enabled {
budget_tokens: Some(budget_tokens),
}) = request.thinking
{
let thinking_config = HashMap::from([
("type".to_string(), Document::String("enabled".to_string())),
(
"budget_tokens".to_string(),
Document::Number(AwsNumber::PosInt(budget_tokens)),
),
]);
response = response.additional_model_request_fields(Document::Object(HashMap::from([(
"thinking".to_string(),
Document::from(thinking_config),
)])));
}
if request.tools.is_some() && !request.tools.as_ref().unwrap().tools.is_empty() {
response = response.set_tool_config(request.tools);
}
if request
.tools
.as_ref()
.map_or(false, |t| !t.tools.is_empty())
{
response = response.set_tool_config(request.tools);
}
let response = response.send().await;
let output = response
.send()
.await
.context("Failed to send API request to Bedrock");
match response {
Ok(output) => {
let stream: Pin<
Box<
dyn Stream<Item = Result<BedrockStreamingResponse, BedrockError>>
+ Send,
>,
> = Box::pin(stream::unfold(output.stream, |mut stream| async move {
match stream.recv().await {
Ok(Some(output)) => Some(({ Ok(output) }, stream)),
Ok(None) => None,
Err(err) => {
Some((
// TODO: Figure out how we can capture Throttling Exceptions
Err(BedrockError::ClientError(anyhow!(
"{:?}",
aws_sdk_bedrockruntime::error::DisplayErrorContext(err)
))),
stream,
))
}
}
}));
Ok(stream)
}
Err(err) => Err(anyhow!(
"{:?}",
aws_sdk_bedrockruntime::error::DisplayErrorContext(err)
let stream = Box::pin(stream::unfold(
output?.stream,
move |mut stream| async move {
match stream.recv().await {
Ok(Some(output)) => Some((Ok(output), stream)),
Ok(None) => None,
Err(err) => Some((
Err(BedrockError::ClientError(anyhow!(
"{:?}",
aws_sdk_bedrockruntime::error::DisplayErrorContext(err)
))),
stream,
)),
}
})
.await
.context("spawning a task")?
},
));
Ok(stream)
}
pub fn aws_document_to_value(document: &Document) -> Value {

View File

@@ -12,7 +12,6 @@ pub struct CallSettings {
/// Configuration of voice calls in Zed.
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
#[schemars(deny_unknown_fields)]
pub struct CallSettingsContent {
/// Whether the microphone should be muted when joining a channel or a call.
///

View File

@@ -22,9 +22,7 @@ use gpui::{
use language::{
Diagnostic, DiagnosticEntry, DiagnosticSourceKind, FakeLspAdapter, Language, LanguageConfig,
LanguageMatcher, LineEnding, OffsetRangeExt, Point, Rope,
language_settings::{
AllLanguageSettings, Formatter, FormatterList, PrettierSettings, SelectedFormatter,
},
language_settings::{AllLanguageSettings, Formatter, PrettierSettings, SelectedFormatter},
tree_sitter_rust, tree_sitter_typescript,
};
use lsp::{LanguageServerId, OneOf};
@@ -4591,15 +4589,13 @@ async fn test_formatting_buffer(
cx_a.update(|cx| {
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings::<AllLanguageSettings>(cx, |file| {
file.defaults.formatter = Some(SelectedFormatter::List(FormatterList(
vec![Formatter::External {
file.defaults.formatter =
Some(SelectedFormatter::List(vec![Formatter::External {
command: "awk".into(),
arguments: Some(
vec!["{sub(/two/,\"{buffer_path}\")}1".to_string()].into(),
),
}]
.into(),
)));
}]));
});
});
});
@@ -4699,9 +4695,10 @@ async fn test_prettier_formatting_buffer(
cx_b.update(|cx| {
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings::<AllLanguageSettings>(cx, |file| {
file.defaults.formatter = Some(SelectedFormatter::List(FormatterList(
vec![Formatter::LanguageServer { name: None }].into(),
)));
file.defaults.formatter =
Some(SelectedFormatter::List(vec![Formatter::LanguageServer {
name: None,
}]));
file.defaults.prettier = Some(PrettierSettings {
allowed: true,
..PrettierSettings::default()

View File

@@ -14,8 +14,7 @@ use http_client::BlockedHttpClient;
use language::{
FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, LanguageRegistry,
language_settings::{
AllLanguageSettings, Formatter, FormatterList, PrettierSettings, SelectedFormatter,
language_settings,
AllLanguageSettings, Formatter, PrettierSettings, SelectedFormatter, language_settings,
},
tree_sitter_typescript,
};
@@ -505,9 +504,10 @@ async fn test_ssh_collaboration_formatting_with_prettier(
cx_b.update(|cx| {
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings::<AllLanguageSettings>(cx, |file| {
file.defaults.formatter = Some(SelectedFormatter::List(FormatterList(
vec![Formatter::LanguageServer { name: None }].into(),
)));
file.defaults.formatter =
Some(SelectedFormatter::List(vec![Formatter::LanguageServer {
name: None,
}]));
file.defaults.prettier = Some(PrettierSettings {
allowed: true,
..PrettierSettings::default()

View File

@@ -28,7 +28,6 @@ pub struct ChatPanelSettings {
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
#[schemars(deny_unknown_fields)]
pub struct ChatPanelSettingsContent {
/// When to show the panel button in the status bar.
///
@@ -52,7 +51,6 @@ pub struct NotificationPanelSettings {
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
#[schemars(deny_unknown_fields)]
pub struct PanelSettingsContent {
/// Whether to show the panel button in the status bar.
///
@@ -69,7 +67,6 @@ pub struct PanelSettingsContent {
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
#[schemars(deny_unknown_fields)]
pub struct MessageEditorSettings {
/// Whether to automatically replace emoji shortcodes with emoji characters.
/// For example: typing `:wave:` gets replaced with `👋`.

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.
fn normalize_query(input: &str) -> String {
pub fn normalize_action_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_query(query.as_str());
let query = normalize_action_query(query.as_str());
async move {
commands.sort_by_key(|action| {
(
@@ -311,29 +311,17 @@ impl PickerDelegate for CommandPaletteDelegate {
.enumerate()
.map(|(ix, command)| StringMatchCandidate::new(ix, &command.name))
.collect::<Vec<_>>();
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
};
let matches = fuzzy::match_strings(
&candidates,
&query,
true,
true,
10000,
&Default::default(),
executor,
)
.await;
tx.send((commands, matches)).await.log_err();
}
@@ -422,8 +410,8 @@ impl PickerDelegate for CommandPaletteDelegate {
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
let r#match = self.matches.get(ix)?;
let command = self.commands.get(r#match.candidate_id)?;
let matching_command = self.matches.get(ix)?;
let command = self.commands.get(matching_command.candidate_id)?;
Some(
ListItem::new(ix)
.inset(true)
@@ -436,7 +424,7 @@ impl PickerDelegate for CommandPaletteDelegate {
.justify_between()
.child(HighlightedLabel::new(
command.name.clone(),
r#match.positions.clone(),
matching_command.positions.clone(),
))
.children(KeyBinding::for_action_in(
&*command.action,
@@ -512,19 +500,28 @@ 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_query("editor::GoToDefinition"),
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"),
"editor:GoToDefinition"
);
assert_eq!(
normalize_query("editor::::GoToDefinition"),
normalize_action_query("editor::::GoToDefinition"),
"editor:GoToDefinition"
);
assert_eq!(
normalize_query("editor: :GoToDefinition"),
normalize_action_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

@@ -10,6 +10,7 @@ use gpui::{AsyncApp, SharedString};
pub use http_client::{HttpClient, github::latest_github_release};
use language::{LanguageName, LanguageToolchainStore};
use node_runtime::NodeRuntime;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::WorktreeId;
use smol::fs::File;
@@ -47,7 +48,10 @@ pub trait DapDelegate: Send + Sync + 'static {
async fn shell_env(&self) -> collections::HashMap<String, String>;
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
#[derive(
Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize, JsonSchema,
)]
#[serde(transparent)]
pub struct DebugAdapterName(pub SharedString);
impl Deref for DebugAdapterName {

View File

@@ -25,7 +25,9 @@ anyhow.workspace = true
async-trait.workspace = true
collections.workspace = true
dap.workspace = true
dotenvy.workspace = true
futures.workspace = true
fs.workspace = true
gpui.workspace = true
json_dotpath.workspace = true
language.workspace = true

View File

@@ -22,17 +22,16 @@ impl CodeLldbDebugAdapter {
async fn request_args(
&self,
delegate: &Arc<dyn DapDelegate>,
task_definition: &DebugTaskDefinition,
mut configuration: Value,
label: &str,
) -> Result<dap::StartDebuggingRequestArguments> {
// CodeLLDB uses `name` for a terminal label.
let mut configuration = task_definition.config.clone();
let obj = configuration
.as_object_mut()
.context("CodeLLDB is not a valid json object")?;
// CodeLLDB uses `name` for a terminal label.
obj.entry("name")
.or_insert(Value::String(String::from(task_definition.label.as_ref())));
.or_insert(Value::String(String::from(label)));
obj.entry("cwd")
.or_insert(delegate.worktree_root_path().to_string_lossy().into());
@@ -361,17 +360,31 @@ impl DebugAdapter for CodeLldbDebugAdapter {
self.path_to_codelldb.set(path.clone()).ok();
command = Some(path);
};
let mut json_config = config.config.clone();
Ok(DebugAdapterBinary {
command: Some(command.unwrap()),
cwd: Some(delegate.worktree_root_path().to_path_buf()),
arguments: user_args.unwrap_or_else(|| {
vec![
"--settings".into(),
json!({"sourceLanguages": ["cpp", "rust"]}).to_string(),
]
if let Some(config) = json_config.as_object_mut()
&& let Some(source_languages) = config.get("sourceLanguages").filter(|value| {
value
.as_array()
.map_or(false, |array| array.iter().all(Value::is_string))
})
{
let ret = vec![
"--settings".into(),
json!({"sourceLanguages": source_languages}).to_string(),
];
config.remove("sourceLanguages");
ret
} else {
vec![]
}
}),
request_args: self.request_args(delegate, &config).await?,
request_args: self
.request_args(delegate, json_config, &config.label)
.await?,
envs: HashMap::default(),
connection: None,
})

View File

@@ -4,7 +4,6 @@ mod go;
mod javascript;
mod php;
mod python;
mod ruby;
use std::sync::Arc;
@@ -25,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};
@@ -35,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));

View File

@@ -7,13 +7,22 @@ use dap::{
latest_github_release,
},
};
use fs::Fs;
use gpui::{AsyncApp, SharedString};
use language::LanguageName;
use std::{env::consts, ffi::OsStr, path::PathBuf, sync::OnceLock};
use log::warn;
use serde_json::{Map, Value};
use task::TcpArgumentsTemplate;
use util;
use std::{
env::consts,
ffi::OsStr,
path::{Path, PathBuf},
str::FromStr,
sync::OnceLock,
};
use crate::*;
#[derive(Default, Debug)]
@@ -437,22 +446,34 @@ impl DebugAdapter for GoDebugAdapter {
adapter_path.join("dlv").to_string_lossy().to_string()
};
let cwd = task_definition
.config
.get("cwd")
.and_then(|s| s.as_str())
.map(PathBuf::from)
.unwrap_or_else(|| delegate.worktree_root_path().to_path_buf());
let cwd = Some(
task_definition
.config
.get("cwd")
.and_then(|s| s.as_str())
.map(PathBuf::from)
.unwrap_or_else(|| delegate.worktree_root_path().to_path_buf()),
);
let arguments;
let command;
let connection;
let mut configuration = task_definition.config.clone();
let mut envs = HashMap::default();
if let Some(configuration) = configuration.as_object_mut() {
configuration
.entry("cwd")
.or_insert_with(|| delegate.worktree_root_path().to_string_lossy().into());
handle_envs(
configuration,
&mut envs,
cwd.as_deref(),
delegate.fs().clone(),
)
.await;
}
if let Some(connection_options) = &task_definition.tcp_connection {
@@ -494,8 +515,8 @@ impl DebugAdapter for GoDebugAdapter {
Ok(DebugAdapterBinary {
command,
arguments,
cwd: Some(cwd),
envs: HashMap::default(),
cwd,
envs,
connection,
request_args: StartDebuggingRequestArguments {
configuration,
@@ -504,3 +525,44 @@ impl DebugAdapter for GoDebugAdapter {
})
}
}
// delve doesn't do anything with the envFile setting, so we intercept it
async fn handle_envs(
config: &mut Map<String, Value>,
envs: &mut HashMap<String, String>,
cwd: Option<&Path>,
fs: Arc<dyn Fs>,
) -> Option<()> {
let env_files = match config.get("envFile")? {
Value::Array(arr) => arr.iter().map(|v| v.as_str()).collect::<Vec<_>>(),
Value::String(s) => vec![Some(s.as_str())],
_ => return None,
};
let rebase_path = |path: PathBuf| {
if path.is_absolute() {
Some(path)
} else {
cwd.map(|p| p.join(path))
}
};
for path in env_files {
let Some(path) = path
.and_then(|s| PathBuf::from_str(s).ok())
.and_then(rebase_path)
else {
continue;
};
if let Ok(file) = fs.open_sync(&path).await {
envs.extend(dotenvy::from_read_iter(file).filter_map(Result::ok))
} else {
warn!("While starting Go debug session: failed to read env file {path:?}");
};
}
// remove envFile now that it's been handled
config.remove("entry");
Some(())
}

View File

@@ -282,6 +282,10 @@ impl DebugAdapter for JsDebugAdapter {
"description": "Automatically stop program after launch",
"default": false
},
"attachSimplePort": {
"type": "number",
"description": "If set, attaches to the process via the given port. This is generally no longer necessary for Node.js programs and loses the ability to debug child processes, but can be useful in more esoteric scenarios such as with Deno and Docker launches. If set to 0, a random port will be chosen and --inspect-brk added to the launch arguments automatically."
},
"runtimeExecutable": {
"type": ["string", "null"],
"description": "Runtime to use, an absolute path or the name of a runtime available on PATH",

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,116 @@ 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| {
let mut allowed_terminated_sessions = 10u32;
project.debug_sessions.retain(|_, session| {
if !session.is_terminated {
return true;
}
allowed_terminated_sessions = allowed_terminated_sessions.saturating_sub(1);
allowed_terminated_sessions > 0
});
});
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 +488,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 +520,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 +546,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 +568,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 +587,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 +669,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 +687,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 +711,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 +758,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 +800,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 +820,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 +849,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 +1067,9 @@ impl Focusable for DapLogView {
}
}
pub enum Event {
enum Event {
NewLogEntry {
id: SessionId,
id: LogStoreEntryIdentifier<'static>,
entry: SharedString,
kind: LogKind,
},
@@ -1008,31 +1082,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

@@ -868,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)
})
@@ -1298,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)))
@@ -1468,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()
@@ -1475,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

@@ -900,7 +900,7 @@ impl RunningState {
let config_is_valid = request_type.is_ok();
let mut extra_config = Value::Null;
let build_output = if let Some(build) = build {
let (task_template, locator_name) = match build {
BuildTaskDefinition::Template {
@@ -930,6 +930,7 @@ impl RunningState {
};
let locator_name = if let Some(locator_name) = locator_name {
extra_config = config.clone();
debug_assert!(!config_is_valid);
Some(locator_name)
} else if !config_is_valid {
@@ -945,6 +946,7 @@ impl RunningState {
});
if let Ok(t) = task {
t.await.and_then(|scenario| {
extra_config = scenario.config;
match scenario.build {
Some(BuildTaskDefinition::Template {
locator_name, ..
@@ -1008,13 +1010,13 @@ impl RunningState {
if !exit_status.success() {
anyhow::bail!("Build failed");
}
Some((task.resolved.clone(), locator_name))
Some((task.resolved.clone(), locator_name, extra_config))
} else {
None
};
if config_is_valid {
} else if let Some((task, locator_name)) = build_output {
} else if let Some((task, locator_name, extra_config)) = build_output {
let locator_name =
locator_name.with_context(|| {
format!("Could not find a valid locator for a build task and configure is invalid with error: {}", request_type.err()
@@ -1039,6 +1041,8 @@ impl RunningState {
.with_context(|| anyhow!("{}: is not a valid adapter name", &adapter))?.config_from_zed_format(zed_config)
.await?;
config = scenario.config;
util::merge_non_null_json_value_into(extra_config, &mut config);
Self::substitute_variables_in_config(&mut config, &task_context);
} else {
let Err(e) = request_type else {

View File

@@ -878,19 +878,30 @@ impl LineBreakpoint {
.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 {
@@ -1234,14 +1245,15 @@ impl RenderOnce for BreakpointOptionsStrip {
};
h_flex()
.gap_2()
.gap_1()
.child(
div() .map(self.add_border(ActiveBreakpointStripMode::Log, supports_logs, window, cx))
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)
@@ -1261,6 +1273,7 @@ impl RenderOnce for BreakpointOptionsStrip {
SharedString::from(format!("{id}-condition-toggle")),
IconName::SplitAlt,
)
.icon_size(IconSize::XSmall)
.style(style_for_toggle(
ActiveBreakpointStripMode::Condition,
has_condition
@@ -1274,7 +1287,7 @@ impl RenderOnce for BreakpointOptionsStrip {
.when(!has_condition && !self.is_selected, |this| this.invisible()),
)
.child(
div() .map(self.add_border(
div().map(self.add_border(
ActiveBreakpointStripMode::HitCondition,
supports_hit_condition,window, cx
))
@@ -1283,6 +1296,7 @@ impl RenderOnce for BreakpointOptionsStrip {
SharedString::from(format!("{id}-hit-condition-toggle")),
IconName::ArrowDown10,
)
.icon_size(IconSize::XSmall)
.style(style_for_toggle(
ActiveBreakpointStripMode::HitCondition,
has_hit_condition,

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(

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

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

@@ -61,6 +61,7 @@ parking_lot.workspace = true
pretty_assertions.workspace = true
project.workspace = true
rand.workspace = true
regex.workspace = true
rpc.workspace = true
schemars.workspace = true
serde.workspace = true

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);

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 mut style = self.highlight_styles.inlay_hint.unwrap_or_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,

View File

@@ -547,6 +547,7 @@ pub enum SoftWrap {
#[derive(Clone)]
pub struct EditorStyle {
pub background: Hsla,
pub border: Hsla,
pub local_player: PlayerColor,
pub text: TextStyle,
pub scrollbar_width: Pixels,
@@ -562,6 +563,7 @@ impl Default for EditorStyle {
fn default() -> Self {
Self {
background: Hsla::default(),
border: Hsla::default(),
local_player: PlayerColor::default(),
text: TextStyle::default(),
scrollbar_width: Pixels::default(),
@@ -11539,66 +11541,90 @@ impl Editor {
let language_settings = buffer.language_settings_at(selection.head(), cx);
let language_scope = buffer.language_scope_at(selection.head());
let indent_and_prefix_for_row =
|row: u32| -> (IndentSize, Option<String>, Option<String>) {
let indent = buffer.indent_size_for_line(MultiBufferRow(row));
let (comment_prefix, rewrap_prefix) =
if let Some(language_scope) = &language_scope {
let indent_end = Point::new(row, indent.len);
let comment_prefix = language_scope
.line_comment_prefixes()
.iter()
.find(|prefix| buffer.contains_str_at(indent_end, prefix))
.map(|prefix| prefix.to_string());
let line_end = Point::new(row, buffer.line_len(MultiBufferRow(row)));
let line_text_after_indent = buffer
.text_for_range(indent_end..line_end)
.collect::<String>();
let rewrap_prefix = language_scope
.rewrap_prefixes()
.iter()
.find_map(|prefix_regex| {
prefix_regex.find(&line_text_after_indent).map(|mat| {
if mat.start() == 0 {
Some(mat.as_str().to_string())
} else {
None
}
})
})
.flatten();
(comment_prefix, rewrap_prefix)
} else {
(None, None)
};
(indent, comment_prefix, rewrap_prefix)
};
let mut ranges = Vec::new();
let mut current_range_start = first_row;
let from_empty_selection = selection.is_empty();
let mut current_range_start = first_row;
let mut prev_row = first_row;
let mut prev_indent = buffer.indent_size_for_line(MultiBufferRow(first_row));
let mut prev_comment_prefix = if let Some(language_scope) = &language_scope {
let indent = buffer.indent_size_for_line(MultiBufferRow(first_row));
let indent_end = Point::new(first_row, indent.len);
language_scope
.line_comment_prefixes()
.iter()
.find(|prefix| buffer.contains_str_at(indent_end, prefix))
.cloned()
} else {
None
};
let (
mut current_range_indent,
mut current_range_comment_prefix,
mut current_range_rewrap_prefix,
) = indent_and_prefix_for_row(first_row);
for row in non_blank_rows_iter.skip(1) {
let has_paragraph_break = row > prev_row + 1;
let row_indent = buffer.indent_size_for_line(MultiBufferRow(row));
let row_comment_prefix = if let Some(language_scope) = &language_scope {
let indent = buffer.indent_size_for_line(MultiBufferRow(row));
let indent_end = Point::new(row, indent.len);
language_scope
.line_comment_prefixes()
.iter()
.find(|prefix| buffer.contains_str_at(indent_end, prefix))
.cloned()
} else {
None
};
let (row_indent, row_comment_prefix, row_rewrap_prefix) =
indent_and_prefix_for_row(row);
let has_boundary_change =
row_indent != prev_indent || row_comment_prefix != prev_comment_prefix;
let has_indent_change = row_indent != current_range_indent;
let has_comment_change = row_comment_prefix != current_range_comment_prefix;
let has_boundary_change = has_comment_change
|| row_rewrap_prefix.is_some()
|| (has_indent_change && current_range_comment_prefix.is_some());
if has_paragraph_break || has_boundary_change {
ranges.push((
language_settings.clone(),
Point::new(current_range_start, 0)
..Point::new(prev_row, buffer.line_len(MultiBufferRow(prev_row))),
prev_indent,
prev_comment_prefix.clone(),
current_range_indent,
current_range_comment_prefix.clone(),
current_range_rewrap_prefix.clone(),
from_empty_selection,
));
current_range_start = row;
current_range_indent = row_indent;
current_range_comment_prefix = row_comment_prefix;
current_range_rewrap_prefix = row_rewrap_prefix;
}
prev_row = row;
prev_indent = row_indent;
prev_comment_prefix = row_comment_prefix;
}
ranges.push((
language_settings.clone(),
Point::new(current_range_start, 0)
..Point::new(prev_row, buffer.line_len(MultiBufferRow(prev_row))),
prev_indent,
prev_comment_prefix,
current_range_indent,
current_range_comment_prefix,
current_range_rewrap_prefix,
from_empty_selection,
));
@@ -11608,8 +11634,14 @@ impl Editor {
let mut edits = Vec::new();
let mut rewrapped_row_ranges = Vec::<RangeInclusive<u32>>::new();
for (language_settings, wrap_range, indent_size, comment_prefix, from_empty_selection) in
wrap_ranges
for (
language_settings,
wrap_range,
indent_size,
comment_prefix,
rewrap_prefix,
from_empty_selection,
) in wrap_ranges
{
let mut start_row = wrap_range.start.row;
let mut end_row = wrap_range.end.row;
@@ -11625,12 +11657,16 @@ impl Editor {
let tab_size = language_settings.tab_size;
let mut line_prefix = indent_size.chars().collect::<String>();
let indent_prefix = indent_size.chars().collect::<String>();
let mut line_prefix = indent_prefix.clone();
let mut inside_comment = false;
if let Some(prefix) = &comment_prefix {
line_prefix.push_str(prefix);
inside_comment = true;
}
if let Some(prefix) = &rewrap_prefix {
line_prefix.push_str(prefix);
}
let allow_rewrap_based_on_language = match language_settings.allow_rewrap {
RewrapBehavior::InComments => inside_comment,
@@ -11677,12 +11713,18 @@ impl Editor {
let selection_text = buffer.text_for_range(start..end).collect::<String>();
let Some(lines_without_prefixes) = selection_text
.lines()
.map(|line| {
line.strip_prefix(&line_prefix)
.or_else(|| line.trim_start().strip_prefix(&line_prefix.trim_start()))
.with_context(|| {
format!("line did not start with prefix {line_prefix:?}: {line:?}")
})
.enumerate()
.map(|(ix, line)| {
let line_trimmed = line.trim_start();
if rewrap_prefix.is_some() && ix > 0 {
Ok(line_trimmed)
} else {
line_trimmed
.strip_prefix(&line_prefix.trim_start())
.with_context(|| {
format!("line did not start with prefix {line_prefix:?}: {line:?}")
})
}
})
.collect::<Result<Vec<_>, _>>()
.log_err()
@@ -11695,8 +11737,16 @@ impl Editor {
.language_settings_at(Point::new(start_row, 0), cx)
.preferred_line_length as usize
});
let subsequent_lines_prefix = if let Some(rewrap_prefix_str) = &rewrap_prefix {
format!("{}{}", indent_prefix, " ".repeat(rewrap_prefix_str.len()))
} else {
line_prefix.clone()
};
let wrapped_text = wrap_with_prefix(
line_prefix,
subsequent_lines_prefix,
lines_without_prefixes.join("\n"),
wrap_column,
tab_size,
@@ -17331,9 +17381,9 @@ impl Editor {
self.active_indent_guides_state.dirty = true;
}
pub fn update_fold_widths(
pub fn update_renderer_widths(
&mut self,
widths: impl IntoIterator<Item = (FoldId, Pixels)>,
widths: impl IntoIterator<Item = (ChunkRendererId, Pixels)>,
cx: &mut Context<Self>,
) -> bool {
self.display_map
@@ -21198,18 +21248,22 @@ fn test_word_breaking_tokenizer() {
}
fn wrap_with_prefix(
line_prefix: String,
first_line_prefix: String,
subsequent_lines_prefix: String,
unwrapped_text: String,
wrap_column: usize,
tab_size: NonZeroU32,
preserve_existing_whitespace: bool,
) -> String {
let line_prefix_len = char_len_with_expanded_tabs(0, &line_prefix, tab_size);
let first_line_prefix_len = char_len_with_expanded_tabs(0, &first_line_prefix, tab_size);
let subsequent_lines_prefix_len =
char_len_with_expanded_tabs(0, &subsequent_lines_prefix, tab_size);
let mut wrapped_text = String::new();
let mut current_line = line_prefix.clone();
let mut current_line = first_line_prefix.clone();
let mut is_first_line = true;
let tokenizer = WordBreakingTokenizer::new(&unwrapped_text);
let mut current_line_len = line_prefix_len;
let mut current_line_len = first_line_prefix_len;
let mut in_whitespace = false;
for token in tokenizer {
let have_preceding_whitespace = in_whitespace;
@@ -21219,13 +21273,19 @@ fn wrap_with_prefix(
grapheme_len,
} => {
in_whitespace = false;
let current_prefix_len = if is_first_line {
first_line_prefix_len
} else {
subsequent_lines_prefix_len
};
if current_line_len + grapheme_len > wrap_column
&& current_line_len != line_prefix_len
&& current_line_len != current_prefix_len
{
wrapped_text.push_str(current_line.trim_end());
wrapped_text.push('\n');
current_line.truncate(line_prefix.len());
current_line_len = line_prefix_len;
is_first_line = false;
current_line = subsequent_lines_prefix.clone();
current_line_len = subsequent_lines_prefix_len;
}
current_line.push_str(token);
current_line_len += grapheme_len;
@@ -21242,32 +21302,46 @@ fn wrap_with_prefix(
token = " ";
grapheme_len = 1;
}
let current_prefix_len = if is_first_line {
first_line_prefix_len
} else {
subsequent_lines_prefix_len
};
if current_line_len + grapheme_len > wrap_column {
wrapped_text.push_str(current_line.trim_end());
wrapped_text.push('\n');
current_line.truncate(line_prefix.len());
current_line_len = line_prefix_len;
} else if current_line_len != line_prefix_len || preserve_existing_whitespace {
is_first_line = false;
current_line = subsequent_lines_prefix.clone();
current_line_len = subsequent_lines_prefix_len;
} else if current_line_len != current_prefix_len || preserve_existing_whitespace {
current_line.push_str(token);
current_line_len += grapheme_len;
}
}
WordBreakToken::Newline => {
in_whitespace = true;
let current_prefix_len = if is_first_line {
first_line_prefix_len
} else {
subsequent_lines_prefix_len
};
if preserve_existing_whitespace {
wrapped_text.push_str(current_line.trim_end());
wrapped_text.push('\n');
current_line.truncate(line_prefix.len());
current_line_len = line_prefix_len;
is_first_line = false;
current_line = subsequent_lines_prefix.clone();
current_line_len = subsequent_lines_prefix_len;
} else if have_preceding_whitespace {
continue;
} else if current_line_len + 1 > wrap_column && current_line_len != line_prefix_len
} else if current_line_len + 1 > wrap_column
&& current_line_len != current_prefix_len
{
wrapped_text.push_str(current_line.trim_end());
wrapped_text.push('\n');
current_line.truncate(line_prefix.len());
current_line_len = line_prefix_len;
} else if current_line_len != line_prefix_len {
is_first_line = false;
current_line = subsequent_lines_prefix.clone();
current_line_len = subsequent_lines_prefix_len;
} else if current_line_len != current_prefix_len {
current_line.push(' ');
current_line_len += 1;
}
@@ -21285,6 +21359,7 @@ fn wrap_with_prefix(
fn test_wrap_with_prefix() {
assert_eq!(
wrap_with_prefix(
"# ".to_string(),
"# ".to_string(),
"abcdefg".to_string(),
4,
@@ -21295,6 +21370,7 @@ fn test_wrap_with_prefix() {
);
assert_eq!(
wrap_with_prefix(
"".to_string(),
"".to_string(),
"\thello world".to_string(),
8,
@@ -21305,6 +21381,7 @@ fn test_wrap_with_prefix() {
);
assert_eq!(
wrap_with_prefix(
"// ".to_string(),
"// ".to_string(),
"xx \nyy zz aa bb cc".to_string(),
12,
@@ -21315,6 +21392,7 @@ fn test_wrap_with_prefix() {
);
assert_eq!(
wrap_with_prefix(
String::new(),
String::new(),
"这是什么 \n 钢笔".to_string(),
3,
@@ -22405,6 +22483,7 @@ impl Render for Editor {
&cx.entity(),
EditorStyle {
background,
border: cx.theme().colors().border,
local_player: cx.theme().players().local(),
text: text_style,
scrollbar_width: EditorElement::SCROLLBAR_WIDTH,

View File

@@ -378,7 +378,6 @@ pub enum SnippetSortOrder {
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
#[schemars(deny_unknown_fields)]
pub struct EditorSettingsContent {
/// Whether the cursor blinks in the editor.
///

View File

@@ -3,7 +3,7 @@ use std::sync::Arc;
use gpui::{App, FontFeatures, FontWeight};
use project::project_settings::{InlineBlameSettings, ProjectSettings};
use settings::{EditableSettingControl, Settings};
use theme::{FontFamilyCache, ThemeSettings};
use theme::{FontFamilyCache, FontFamilyName, ThemeSettings};
use ui::{
CheckboxWithLabel, ContextMenu, DropdownMenu, NumericStepper, SettingsContainer, SettingsGroup,
prelude::*,
@@ -75,7 +75,7 @@ impl EditableSettingControl for BufferFontFamilyControl {
value: Self::Value,
_cx: &App,
) {
settings.buffer_font_family = Some(value.to_string());
settings.buffer_font_family = Some(FontFamilyName(value.into()));
}
}

View File

@@ -30,7 +30,7 @@ use language::{
},
tree_sitter_python,
};
use language_settings::{Formatter, FormatterList, IndentGuideSettings};
use language_settings::{Formatter, IndentGuideSettings};
use lsp::CompletionParams;
use multi_buffer::{IndentGuide, PathKey};
use parking_lot::Mutex;
@@ -3567,7 +3567,7 @@ async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
#[gpui::test]
fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
init_test(cx, |settings| {
settings.languages.extend([
settings.languages.0.extend([
(
"TOML".into(),
LanguageSettingsContent {
@@ -5145,7 +5145,7 @@ fn test_transpose(cx: &mut TestAppContext) {
#[gpui::test]
async fn test_rewrap(cx: &mut TestAppContext) {
init_test(cx, |settings| {
settings.languages.extend([
settings.languages.0.extend([
(
"Markdown".into(),
LanguageSettingsContent {
@@ -5210,6 +5210,10 @@ async fn test_rewrap(cx: &mut TestAppContext) {
let markdown_language = Arc::new(Language::new(
LanguageConfig {
name: "Markdown".into(),
rewrap_prefixes: vec![
regex::Regex::new("\\d+\\.\\s+").unwrap(),
regex::Regex::new("[-*+]\\s+").unwrap(),
],
..LanguageConfig::default()
},
None,
@@ -5372,7 +5376,82 @@ async fn test_rewrap(cx: &mut TestAppContext) {
A long long long line of markdown text
to wrap.ˇ
"},
markdown_language,
markdown_language.clone(),
&mut cx,
);
// Test that rewrapping boundary works and preserves relative indent for Markdown documents
assert_rewrap(
indoc! {"
«1. This is a numbered list item that is very long and needs to be wrapped properly.
2. This is a numbered list item that is very long and needs to be wrapped properly.
- This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
"},
indoc! {"
«1. This is a numbered list item that is
very long and needs to be wrapped
properly.
2. This is a numbered list item that is
very long and needs to be wrapped
properly.
- This is an unordered list item that is
also very long and should not merge
with the numbered item.ˇ»
"},
markdown_language.clone(),
&mut cx,
);
// Test that rewrapping add indents for rewrapping boundary if not exists already.
assert_rewrap(
indoc! {"
«1. This is a numbered list item that is
very long and needs to be wrapped
properly.
2. This is a numbered list item that is
very long and needs to be wrapped
properly.
- This is an unordered list item that is
also very long and should not merge with
the numbered item.ˇ»
"},
indoc! {"
«1. This is a numbered list item that is
very long and needs to be wrapped
properly.
2. This is a numbered list item that is
very long and needs to be wrapped
properly.
- This is an unordered list item that is
also very long and should not merge
with the numbered item.ˇ»
"},
markdown_language.clone(),
&mut cx,
);
// Test that rewrapping maintain indents even when they already exists.
assert_rewrap(
indoc! {"
«1. This is a numbered list
item that is very long and needs to be wrapped properly.
2. This is a numbered list
item that is very long and needs to be wrapped properly.
- This is an unordered list item that is also very long and
should not merge with the numbered item.ˇ»
"},
indoc! {"
«1. This is a numbered list item that is
very long and needs to be wrapped
properly.
2. This is a numbered list item that is
very long and needs to be wrapped
properly.
- This is an unordered list item that is
also very long and should not merge
with the numbered item.ˇ»
"},
markdown_language.clone(),
&mut cx,
);
@@ -9326,7 +9405,7 @@ async fn test_document_format_during_save(cx: &mut TestAppContext) {
// Set rust language override and assert overridden tabsize is sent to language server
update_test_language_settings(cx, |settings| {
settings.languages.insert(
settings.languages.0.insert(
"Rust".into(),
LanguageSettingsContent {
tab_size: NonZeroU32::new(8),
@@ -9890,7 +9969,7 @@ async fn test_range_format_during_save(cx: &mut TestAppContext) {
// Set Rust language override and assert overridden tabsize is sent to language server
update_test_language_settings(cx, |settings| {
settings.languages.insert(
settings.languages.0.insert(
"Rust".into(),
LanguageSettingsContent {
tab_size: NonZeroU32::new(8),
@@ -9933,9 +10012,9 @@ async fn test_range_format_during_save(cx: &mut TestAppContext) {
#[gpui::test]
async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
init_test(cx, |settings| {
settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
))
settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(vec![
Formatter::LanguageServer { name: None },
]))
});
let fs = FakeFs::new(cx.executor());
@@ -10062,21 +10141,17 @@ async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
async fn test_multiple_formatters(cx: &mut TestAppContext) {
init_test(cx, |settings| {
settings.defaults.remove_trailing_whitespace_on_save = Some(true);
settings.defaults.formatter =
Some(language_settings::SelectedFormatter::List(FormatterList(
vec![
Formatter::LanguageServer { name: None },
Formatter::CodeActions(
[
("code-action-1".into(), true),
("code-action-2".into(), true),
]
.into_iter()
.collect(),
),
settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(vec![
Formatter::LanguageServer { name: None },
Formatter::CodeActions(
[
("code-action-1".into(), true),
("code-action-2".into(), true),
]
.into(),
)))
.into_iter()
.collect(),
),
]))
});
let fs = FakeFs::new(cx.executor());
@@ -10328,9 +10403,9 @@ async fn test_multiple_formatters(cx: &mut TestAppContext) {
#[gpui::test]
async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
init_test(cx, |settings| {
settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
))
settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(vec![
Formatter::LanguageServer { name: None },
]))
});
let fs = FakeFs::new(cx.executor());
@@ -14905,7 +14980,7 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppCon
.unwrap();
let _fake_server = fake_servers.next().await.unwrap();
update_test_language_settings(cx, |language_settings| {
language_settings.languages.insert(
language_settings.languages.0.insert(
language_name.clone(),
LanguageSettingsContent {
tab_size: NonZeroU32::new(8),
@@ -15803,9 +15878,9 @@ fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
#[gpui::test]
async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
init_test(cx, |settings| {
settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
FormatterList(vec![Formatter::Prettier].into()),
))
settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(vec![
Formatter::Prettier,
]))
});
let fs = FakeFs::new(cx.executor());

View File

@@ -12,8 +12,8 @@ use crate::{
ToggleFold,
code_context_menus::{CodeActionsMenu, MENU_ASIDE_MAX_WIDTH, MENU_ASIDE_MIN_WIDTH, MENU_GAP},
display_map::{
Block, BlockContext, BlockStyle, DisplaySnapshot, EditorMargins, FoldId, HighlightKey,
HighlightedChunk, ToDisplayPoint,
Block, BlockContext, BlockStyle, ChunkRendererId, DisplaySnapshot, EditorMargins,
HighlightKey, HighlightedChunk, ToDisplayPoint,
},
editor_settings::{
CurrentLineHighlight, DocumentColorsRenderMode, DoubleClickInMultibuffer, Minimap,
@@ -7119,7 +7119,7 @@ pub(crate) struct LineWithInvisibles {
enum LineFragment {
Text(ShapedLine),
Element {
id: FoldId,
id: ChunkRendererId,
element: Option<AnyElement>,
size: Size<Pixels>,
len: usize,
@@ -8297,7 +8297,7 @@ impl Element for EditorElement {
window,
cx,
);
let new_fold_widths = line_layouts
let new_renrerer_widths = line_layouts
.iter()
.flat_map(|layout| &layout.fragments)
.filter_map(|fragment| {
@@ -8308,7 +8308,7 @@ impl Element for EditorElement {
}
});
if self.editor.update(cx, |editor, cx| {
editor.update_fold_widths(new_fold_widths, cx)
editor.update_renderer_widths(new_renrerer_widths, cx)
}) {
// If the fold widths have changed, we need to prepaint
// the element again to account for any changes in

View File

@@ -19,18 +19,21 @@ use crate::{
#[derive(Debug)]
pub(super) struct LspColorData {
cache_version_used: usize,
buffer_colors: HashMap<BufferId, BufferColors>,
render_mode: DocumentColorsRenderMode,
}
#[derive(Debug, Default)]
struct BufferColors {
colors: Vec<(Range<Anchor>, DocumentColor, InlayId)>,
inlay_colors: HashMap<InlayId, usize>,
render_mode: DocumentColorsRenderMode,
cache_version_used: usize,
}
impl LspColorData {
pub fn new(cx: &App) -> Self {
Self {
cache_version_used: 0,
colors: Vec::new(),
inlay_colors: HashMap::default(),
buffer_colors: HashMap::default(),
render_mode: EditorSettings::get_global(cx).lsp_document_colors,
}
}
@@ -47,8 +50,9 @@ impl LspColorData {
DocumentColorsRenderMode::Inlay => Some(InlaySplice {
to_remove: Vec::new(),
to_insert: self
.colors
.buffer_colors
.iter()
.flat_map(|(_, buffer_colors)| buffer_colors.colors.iter())
.map(|(range, color, id)| {
Inlay::color(
id.id(),
@@ -63,33 +67,49 @@ impl LspColorData {
})
.collect(),
}),
DocumentColorsRenderMode::None => {
self.colors.clear();
Some(InlaySplice {
to_remove: self.inlay_colors.drain().map(|(id, _)| id).collect(),
to_insert: Vec::new(),
})
}
DocumentColorsRenderMode::None => Some(InlaySplice {
to_remove: self
.buffer_colors
.drain()
.flat_map(|(_, buffer_colors)| buffer_colors.inlay_colors)
.map(|(id, _)| id)
.collect(),
to_insert: Vec::new(),
}),
DocumentColorsRenderMode::Border | DocumentColorsRenderMode::Background => {
Some(InlaySplice {
to_remove: self.inlay_colors.drain().map(|(id, _)| id).collect(),
to_remove: self
.buffer_colors
.iter_mut()
.flat_map(|(_, buffer_colors)| buffer_colors.inlay_colors.drain())
.map(|(id, _)| id)
.collect(),
to_insert: Vec::new(),
})
}
}
}
fn set_colors(&mut self, colors: Vec<(Range<Anchor>, DocumentColor, InlayId)>) -> bool {
if self.colors == colors {
fn set_colors(
&mut self,
buffer_id: BufferId,
colors: Vec<(Range<Anchor>, DocumentColor, InlayId)>,
cache_version: Option<usize>,
) -> bool {
let buffer_colors = self.buffer_colors.entry(buffer_id).or_default();
if let Some(cache_version) = cache_version {
buffer_colors.cache_version_used = cache_version;
}
if buffer_colors.colors == colors {
return false;
}
self.inlay_colors = colors
buffer_colors.inlay_colors = colors
.iter()
.enumerate()
.map(|(i, (_, _, id))| (*id, i))
.collect();
self.colors = colors;
buffer_colors.colors = colors;
true
}
@@ -103,8 +123,9 @@ impl LspColorData {
{
Vec::new()
} else {
self.colors
self.buffer_colors
.iter()
.flat_map(|(_, buffer_colors)| &buffer_colors.colors)
.map(|(range, color, _)| {
let display_range = range.clone().to_display_points(snapshot);
let color = Hsla::from(Rgba {
@@ -162,10 +183,9 @@ impl Editor {
ColorFetchStrategy::IgnoreCache
} else {
ColorFetchStrategy::UseCache {
known_cache_version: self
.colors
.as_ref()
.map(|colors| colors.cache_version_used),
known_cache_version: self.colors.as_ref().and_then(|colors| {
Some(colors.buffer_colors.get(&buffer_id)?.cache_version_used)
}),
}
};
let colors_task = lsp_store.document_colors(fetch_strategy, buffer, cx)?;
@@ -201,15 +221,13 @@ impl Editor {
return;
};
let mut cache_version = None;
let mut new_editor_colors = Vec::<(Range<Anchor>, DocumentColor)>::new();
let mut new_editor_colors = HashMap::default();
for (buffer_id, colors) in all_colors {
let Some(excerpts) = editor_excerpts.get(&buffer_id) else {
continue;
};
match colors {
Ok(colors) => {
cache_version = colors.cache_version;
for color in colors.colors {
let color_start = point_from_lsp(color.lsp_range.start);
let color_end = point_from_lsp(color.lsp_range.end);
@@ -243,8 +261,15 @@ impl Editor {
continue;
};
let new_entry =
new_editor_colors.entry(buffer_id).or_insert_with(|| {
(Vec::<(Range<Anchor>, DocumentColor)>::new(), None)
});
new_entry.1 = colors.cache_version;
let new_buffer_colors = &mut new_entry.0;
let (Ok(i) | Err(i)) =
new_editor_colors.binary_search_by(|(probe, _)| {
new_buffer_colors.binary_search_by(|(probe, _)| {
probe
.start
.cmp(&color_start_anchor, &multi_buffer_snapshot)
@@ -254,7 +279,7 @@ impl Editor {
.cmp(&color_end_anchor, &multi_buffer_snapshot)
})
});
new_editor_colors
new_buffer_colors
.insert(i, (color_start_anchor..color_end_anchor, color));
break;
}
@@ -267,45 +292,70 @@ impl Editor {
editor
.update(cx, |editor, cx| {
let mut colors_splice = InlaySplice::default();
let mut new_color_inlays = Vec::with_capacity(new_editor_colors.len());
let Some(colors) = &mut editor.colors else {
return;
};
let mut existing_colors = colors.colors.iter().peekable();
for (new_range, new_color) in new_editor_colors {
let rgba_color = Rgba {
r: new_color.color.red,
g: new_color.color.green,
b: new_color.color.blue,
a: new_color.color.alpha,
};
let mut updated = false;
for (buffer_id, (new_buffer_colors, new_cache_version)) in new_editor_colors {
let mut new_buffer_color_inlays =
Vec::with_capacity(new_buffer_colors.len());
let mut existing_buffer_colors = colors
.buffer_colors
.entry(buffer_id)
.or_default()
.colors
.iter()
.peekable();
for (new_range, new_color) in new_buffer_colors {
let rgba_color = Rgba {
r: new_color.color.red,
g: new_color.color.green,
b: new_color.color.blue,
a: new_color.color.alpha,
};
loop {
match existing_colors.peek() {
Some((existing_range, existing_color, existing_inlay_id)) => {
match existing_range
.start
.cmp(&new_range.start, &multi_buffer_snapshot)
.then_with(|| {
existing_range
.end
.cmp(&new_range.end, &multi_buffer_snapshot)
}) {
cmp::Ordering::Less => {
colors_splice.to_remove.push(*existing_inlay_id);
existing_colors.next();
continue;
}
cmp::Ordering::Equal => {
if existing_color == &new_color {
new_color_inlays.push((
new_range,
new_color,
*existing_inlay_id,
));
} else {
loop {
match existing_buffer_colors.peek() {
Some((existing_range, existing_color, existing_inlay_id)) => {
match existing_range
.start
.cmp(&new_range.start, &multi_buffer_snapshot)
.then_with(|| {
existing_range
.end
.cmp(&new_range.end, &multi_buffer_snapshot)
}) {
cmp::Ordering::Less => {
colors_splice.to_remove.push(*existing_inlay_id);
existing_buffer_colors.next();
continue;
}
cmp::Ordering::Equal => {
if existing_color == &new_color {
new_buffer_color_inlays.push((
new_range,
new_color,
*existing_inlay_id,
));
} else {
colors_splice
.to_remove
.push(*existing_inlay_id);
let inlay = Inlay::color(
post_inc(&mut editor.next_color_inlay_id),
new_range.start,
rgba_color,
);
let inlay_id = inlay.id;
colors_splice.to_insert.push(inlay);
new_buffer_color_inlays
.push((new_range, new_color, inlay_id));
}
existing_buffer_colors.next();
break;
}
cmp::Ordering::Greater => {
let inlay = Inlay::color(
post_inc(&mut editor.next_color_inlay_id),
new_range.start,
@@ -313,49 +363,40 @@ impl Editor {
);
let inlay_id = inlay.id;
colors_splice.to_insert.push(inlay);
new_color_inlays
new_buffer_color_inlays
.push((new_range, new_color, inlay_id));
break;
}
existing_colors.next();
break;
}
cmp::Ordering::Greater => {
let inlay = Inlay::color(
post_inc(&mut editor.next_color_inlay_id),
new_range.start,
rgba_color,
);
let inlay_id = inlay.id;
colors_splice.to_insert.push(inlay);
new_color_inlays.push((new_range, new_color, inlay_id));
break;
}
}
}
None => {
let inlay = Inlay::color(
post_inc(&mut editor.next_color_inlay_id),
new_range.start,
rgba_color,
);
let inlay_id = inlay.id;
colors_splice.to_insert.push(inlay);
new_color_inlays.push((new_range, new_color, inlay_id));
break;
None => {
let inlay = Inlay::color(
post_inc(&mut editor.next_color_inlay_id),
new_range.start,
rgba_color,
);
let inlay_id = inlay.id;
colors_splice.to_insert.push(inlay);
new_buffer_color_inlays
.push((new_range, new_color, inlay_id));
break;
}
}
}
}
}
if existing_colors.peek().is_some() {
colors_splice
.to_remove
.extend(existing_colors.map(|(_, _, id)| *id));
if existing_buffer_colors.peek().is_some() {
colors_splice
.to_remove
.extend(existing_buffer_colors.map(|(_, _, id)| *id));
}
updated |= colors.set_colors(
buffer_id,
new_buffer_color_inlays,
new_cache_version,
);
}
let mut updated = colors.set_colors(new_color_inlays);
if let Some(cache_version) = cache_version {
colors.cache_version_used = cache_version;
}
if colors.render_mode == DocumentColorsRenderMode::Inlay
&& (!colors_splice.to_insert.is_empty()
|| !colors_splice.to_remove.is_empty())

View File

@@ -32,7 +32,7 @@ client.workspace = true
collections.workspace = true
debug_adapter_extension.workspace = true
dirs.workspace = true
dotenv.workspace = true
dotenvy.workspace = true
env_logger.workspace = true
extension.workspace = true
fs.workspace = true

View File

@@ -63,7 +63,7 @@ struct Args {
}
fn main() {
dotenv::from_filename(CARGO_MANIFEST_DIR.join(".env")).ok();
dotenvy::from_filename(CARGO_MANIFEST_DIR.join(".env")).ok();
env_logger::init();

View File

@@ -1054,6 +1054,15 @@ pub fn response_events_to_markdown(
| LanguageModelCompletionEvent::StartMessage { .. }
| LanguageModelCompletionEvent::StatusUpdate { .. },
) => {}
Ok(LanguageModelCompletionEvent::ToolUseJsonParseError {
json_parse_error, ..
}) => {
flush_buffers(&mut response, &mut text_buffer, &mut thinking_buffer);
response.push_str(&format!(
"**Error**: parse error in tool use JSON: {}\n\n",
json_parse_error
));
}
Err(error) => {
flush_buffers(&mut response, &mut text_buffer, &mut thinking_buffer);
response.push_str(&format!("**Error**: {}\n\n", error));
@@ -1132,6 +1141,17 @@ impl ThreadDialog {
| Ok(LanguageModelCompletionEvent::StartMessage { .. })
| Ok(LanguageModelCompletionEvent::Stop(_)) => {}
Ok(LanguageModelCompletionEvent::ToolUseJsonParseError {
json_parse_error,
..
}) => {
flush_text(&mut current_text, &mut content);
content.push(MessageContent::Text(format!(
"ERROR: parse error in tool use JSON: {}",
json_parse_error
)));
}
Err(error) => {
flush_text(&mut current_text, &mut content);
content.push(MessageContent::Text(format!("ERROR: {}", error)));

View File

@@ -70,6 +70,7 @@ const SUGGESTIONS_BY_EXTENSION_ID: &[(&str, &[&str])] = &[
("templ", &["templ"]),
("terraform", &["tf", "tfvars", "hcl"]),
("toml", &["Cargo.lock", "toml"]),
("typst", &["typ"]),
("vue", &["vue"]),
("wgsl", &["wgsl"]),
("wit", &["wit"]),

View File

@@ -65,6 +65,7 @@ actions!(
#[derive(Clone, Debug, Default, PartialEq, Deserialize, JsonSchema, Action)]
#[action(namespace = git, deprecated_aliases = ["editor::RevertFile"])]
#[serde(deny_unknown_fields)]
pub struct RestoreFile {
#[serde(default)]
pub skip_prompt: bool,

View File

@@ -388,6 +388,7 @@ pub(crate) fn commit_message_editor(
commit_editor.set_collaboration_hub(Box::new(project));
commit_editor.set_use_autoclose(false);
commit_editor.set_show_gutter(false, cx);
commit_editor.set_use_modal_editing(true);
commit_editor.set_show_wrap_guides(false, cx);
commit_editor.set_show_indent_guides(false, cx);
let placeholder = placeholder.unwrap_or("Enter commit message".into());

View File

@@ -12,7 +12,7 @@ license = "Apache-2.0"
workspace = true
[features]
default = ["http_client", "font-kit", "wayland", "x11"]
default = ["http_client", "font-kit", "wayland", "x11", "windows-manifest"]
test-support = [
"leak-detection",
"collections/test-support",
@@ -69,7 +69,7 @@ x11 = [
"open",
"scap",
]
windows-manifest = []
[lib]
path = "src/gpui.rs"

View File

@@ -17,7 +17,7 @@ fn main() {
#[cfg(target_os = "macos")]
macos::build();
}
#[cfg(target_os = "windows")]
#[cfg(all(target_os = "windows", feature = "windows-manifest"))]
Ok("windows") => {
let manifest = std::path::Path::new("resources/windows/gpui.manifest.xml");
let rc_file = std::path::Path::new("resources/windows/gpui.rc");

View File

@@ -125,9 +125,7 @@ pub trait Action: Any + Send {
Self: Sized;
/// Optional JSON schema for the action's input data.
fn action_json_schema(
_: &mut schemars::r#gen::SchemaGenerator,
) -> Option<schemars::schema::Schema>
fn action_json_schema(_: &mut schemars::SchemaGenerator) -> Option<schemars::Schema>
where
Self: Sized,
{
@@ -238,7 +236,7 @@ impl Default for ActionRegistry {
struct ActionData {
pub build: ActionBuilder,
pub json_schema: fn(&mut schemars::r#gen::SchemaGenerator) -> Option<schemars::schema::Schema>,
pub json_schema: fn(&mut schemars::SchemaGenerator) -> Option<schemars::Schema>,
}
/// This type must be public so that our macros can build it in other crates.
@@ -253,7 +251,7 @@ pub struct MacroActionData {
pub name: &'static str,
pub type_id: TypeId,
pub build: ActionBuilder,
pub json_schema: fn(&mut schemars::r#gen::SchemaGenerator) -> Option<schemars::schema::Schema>,
pub json_schema: fn(&mut schemars::SchemaGenerator) -> Option<schemars::Schema>,
pub deprecated_aliases: &'static [&'static str],
pub deprecation_message: Option<&'static str>,
}
@@ -357,8 +355,8 @@ impl ActionRegistry {
pub fn action_schemas(
&self,
generator: &mut schemars::r#gen::SchemaGenerator,
) -> Vec<(&'static str, Option<schemars::schema::Schema>)> {
generator: &mut schemars::SchemaGenerator,
) -> Vec<(&'static str, Option<schemars::Schema>)> {
// Use the order from all_names so that the resulting schema has sensible order.
self.all_names
.iter()

View File

@@ -1334,6 +1334,11 @@ impl App {
self.pending_effects.push_back(Effect::RefreshWindows);
}
/// Get all key bindings in the app.
pub fn key_bindings(&self) -> Rc<RefCell<Keymap>> {
self.keymap.clone()
}
/// Register a global listener for actions invoked via the keyboard.
pub fn on_action<A: Action>(&mut self, listener: impl Fn(&A, &mut Self) + 'static) {
self.global_action_listeners
@@ -1388,8 +1393,8 @@ impl App {
/// Get all non-internal actions that have been registered, along with their schemas.
pub fn action_schemas(
&self,
generator: &mut schemars::r#gen::SchemaGenerator,
) -> Vec<(&'static str, Option<schemars::schema::Schema>)> {
generator: &mut schemars::SchemaGenerator,
) -> Vec<(&'static str, Option<schemars::Schema>)> {
self.actions.action_schemas(generator)
}

View File

@@ -1,9 +1,10 @@
use anyhow::{Context as _, bail};
use schemars::{JsonSchema, SchemaGenerator, schema::Schema};
use schemars::{JsonSchema, json_schema};
use serde::{
Deserialize, Deserializer, Serialize, Serializer,
de::{self, Visitor},
};
use std::borrow::Cow;
use std::{
fmt::{self, Display, Formatter},
hash::{Hash, Hasher},
@@ -99,22 +100,14 @@ impl Visitor<'_> for RgbaVisitor {
}
impl JsonSchema for Rgba {
fn schema_name() -> String {
"Rgba".to_string()
fn schema_name() -> Cow<'static, str> {
"Rgba".into()
}
fn json_schema(_generator: &mut SchemaGenerator) -> Schema {
use schemars::schema::{InstanceType, SchemaObject, StringValidation};
Schema::Object(SchemaObject {
instance_type: Some(InstanceType::String.into()),
string: Some(Box::new(StringValidation {
pattern: Some(
r"^#([0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$".to_string(),
),
..Default::default()
})),
..Default::default()
fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
json_schema!({
"type": "string",
"pattern": "^#([0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$"
})
}
}
@@ -629,11 +622,11 @@ impl From<Rgba> for Hsla {
}
impl JsonSchema for Hsla {
fn schema_name() -> String {
fn schema_name() -> Cow<'static, str> {
Rgba::schema_name()
}
fn json_schema(generator: &mut SchemaGenerator) -> Schema {
fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
Rgba::json_schema(generator)
}
}

View File

@@ -613,10 +613,10 @@ pub trait InteractiveElement: Sized {
/// Track the focus state of the given focus handle on this element.
/// If the focus handle is focused by the application, this element will
/// apply its focused styles.
fn track_focus(mut self, focus_handle: &FocusHandle) -> FocusableWrapper<Self> {
fn track_focus(mut self, focus_handle: &FocusHandle) -> Self {
self.interactivity().focusable = true;
self.interactivity().tracked_focus_handle = Some(focus_handle.clone());
FocusableWrapper { element: self }
self
}
/// Set the keymap context for this element. This will be used to determine
@@ -980,15 +980,35 @@ pub trait InteractiveElement: Sized {
self.interactivity().block_mouse_except_scroll();
self
}
/// Set the given styles to be applied when this element, specifically, is focused.
/// Requires that the element is focusable. Elements can be made focusable using [`InteractiveElement::track_focus`].
fn focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
where
Self: Sized,
{
self.interactivity().focus_style = Some(Box::new(f(StyleRefinement::default())));
self
}
/// Set the given styles to be applied when this element is inside another element that is focused.
/// Requires that the element is focusable. Elements can be made focusable using [`InteractiveElement::track_focus`].
fn in_focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
where
Self: Sized,
{
self.interactivity().in_focus_style = Some(Box::new(f(StyleRefinement::default())));
self
}
}
/// A trait for elements that want to use the standard GPUI interactivity features
/// that require state.
pub trait StatefulInteractiveElement: InteractiveElement {
/// Set this element to focusable.
fn focusable(mut self) -> FocusableWrapper<Self> {
fn focusable(mut self) -> Self {
self.interactivity().focusable = true;
FocusableWrapper { element: self }
self
}
/// Set the overflow x and y to scroll.
@@ -1118,27 +1138,6 @@ pub trait StatefulInteractiveElement: InteractiveElement {
}
}
/// A trait for providing focus related APIs to interactive elements
pub trait FocusableElement: InteractiveElement {
/// Set the given styles to be applied when this element, specifically, is focused.
fn focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
where
Self: Sized,
{
self.interactivity().focus_style = Some(Box::new(f(StyleRefinement::default())));
self
}
/// Set the given styles to be applied when this element is inside another element that is focused.
fn in_focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
where
Self: Sized,
{
self.interactivity().in_focus_style = Some(Box::new(f(StyleRefinement::default())));
self
}
}
pub(crate) type MouseDownListener =
Box<dyn Fn(&MouseDownEvent, DispatchPhase, &Hitbox, &mut Window, &mut App) + 'static>;
pub(crate) type MouseUpListener =
@@ -2777,126 +2776,6 @@ impl GroupHitboxes {
}
}
/// A wrapper around an element that can be focused.
pub struct FocusableWrapper<E> {
/// The element that is focusable
pub element: E,
}
impl<E: InteractiveElement> FocusableElement for FocusableWrapper<E> {}
impl<E> InteractiveElement for FocusableWrapper<E>
where
E: InteractiveElement,
{
fn interactivity(&mut self) -> &mut Interactivity {
self.element.interactivity()
}
}
impl<E: StatefulInteractiveElement> StatefulInteractiveElement for FocusableWrapper<E> {}
impl<E> Styled for FocusableWrapper<E>
where
E: Styled,
{
fn style(&mut self) -> &mut StyleRefinement {
self.element.style()
}
}
impl FocusableWrapper<Div> {
/// Add a listener to be called when the children of this `Div` are prepainted.
/// This allows you to store the [`Bounds`] of the children for later use.
pub fn on_children_prepainted(
mut self,
listener: impl Fn(Vec<Bounds<Pixels>>, &mut Window, &mut App) + 'static,
) -> Self {
self.element = self.element.on_children_prepainted(listener);
self
}
}
impl<E> Element for FocusableWrapper<E>
where
E: Element,
{
type RequestLayoutState = E::RequestLayoutState;
type PrepaintState = E::PrepaintState;
fn id(&self) -> Option<ElementId> {
self.element.id()
}
fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
self.element.source_location()
}
fn request_layout(
&mut self,
id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState) {
self.element.request_layout(id, inspector_id, window, cx)
}
fn prepaint(
&mut self,
id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
state: &mut Self::RequestLayoutState,
window: &mut Window,
cx: &mut App,
) -> E::PrepaintState {
self.element
.prepaint(id, inspector_id, bounds, state, window, cx)
}
fn paint(
&mut self,
id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
request_layout: &mut Self::RequestLayoutState,
prepaint: &mut Self::PrepaintState,
window: &mut Window,
cx: &mut App,
) {
self.element.paint(
id,
inspector_id,
bounds,
request_layout,
prepaint,
window,
cx,
)
}
}
impl<E> IntoElement for FocusableWrapper<E>
where
E: IntoElement,
{
type Element = E::Element;
fn into_element(self) -> Self::Element {
self.element.into_element()
}
}
impl<E> ParentElement for FocusableWrapper<E>
where
E: ParentElement,
{
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.element.extend(elements)
}
}
/// A wrapper around an element that can store state, produced after assigning an ElementId.
pub struct Stateful<E> {
pub(crate) element: E,
@@ -2927,8 +2806,6 @@ where
}
}
impl<E: FocusableElement> FocusableElement for Stateful<E> {}
impl<E> Element for Stateful<E>
where
E: Element,

View File

@@ -25,7 +25,7 @@ use std::{
use thiserror::Error;
use util::ResultExt;
use super::{FocusableElement, Stateful, StatefulInteractiveElement};
use super::{Stateful, StatefulInteractiveElement};
/// The delay before showing the loading state.
pub const LOADING_DELAY: Duration = Duration::from_millis(200);
@@ -509,8 +509,6 @@ impl IntoElement for Img {
}
}
impl FocusableElement for Img {}
impl StatefulInteractiveElement for Img {}
impl ImageSource {

View File

@@ -10,8 +10,8 @@
use crate::{
AnyElement, App, AvailableSpace, Bounds, ContentMask, DispatchPhase, Edges, Element, EntityId,
FocusHandle, GlobalElementId, Hitbox, HitboxBehavior, InspectorElementId, IntoElement,
Overflow, Pixels, Point, ScrollWheelEvent, Size, Style, StyleRefinement, Styled, Window, point,
px, size,
Overflow, Pixels, Point, ScrollDelta, ScrollWheelEvent, Size, Style, StyleRefinement, Styled,
Window, point, px, size,
};
use collections::VecDeque;
use refineable::Refineable as _;
@@ -962,12 +962,15 @@ impl Element for List {
let height = bounds.size.height;
let scroll_top = prepaint.layout.scroll_top;
let hitbox_id = prepaint.hitbox.id;
let mut accumulated_scroll_delta = ScrollDelta::default();
window.on_mouse_event(move |event: &ScrollWheelEvent, phase, window, cx| {
if phase == DispatchPhase::Bubble && hitbox_id.should_handle_scroll(window) {
accumulated_scroll_delta = accumulated_scroll_delta.coalesce(event.delta);
let pixel_delta = accumulated_scroll_delta.pixel_delta(px(20.));
list_state.0.borrow_mut().scroll(
&scroll_top,
height,
event.delta.pixel_delta(px(20.)),
pixel_delta,
current_view,
window,
cx,

View File

@@ -6,8 +6,9 @@ use anyhow::{Context as _, anyhow};
use core::fmt::Debug;
use derive_more::{Add, AddAssign, Div, DivAssign, Mul, Neg, Sub, SubAssign};
use refineable::Refineable;
use schemars::{JsonSchema, SchemaGenerator, schema::Schema};
use schemars::{JsonSchema, json_schema};
use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
use std::borrow::Cow;
use std::{
cmp::{self, PartialOrd},
fmt::{self, Display},
@@ -3229,20 +3230,14 @@ impl TryFrom<&'_ str> for AbsoluteLength {
}
impl JsonSchema for AbsoluteLength {
fn schema_name() -> String {
"AbsoluteLength".to_string()
fn schema_name() -> Cow<'static, str> {
"AbsoluteLength".into()
}
fn json_schema(_generator: &mut SchemaGenerator) -> Schema {
use schemars::schema::{InstanceType, SchemaObject, StringValidation};
Schema::Object(SchemaObject {
instance_type: Some(InstanceType::String.into()),
string: Some(Box::new(StringValidation {
pattern: Some(r"^-?\d+(\.\d+)?(px|rem)$".to_string()),
..Default::default()
})),
..Default::default()
fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
json_schema!({
"type": "string",
"pattern": r"^-?\d+(\.\d+)?(px|rem)$"
})
}
}
@@ -3366,20 +3361,14 @@ impl TryFrom<&'_ str> for DefiniteLength {
}
impl JsonSchema for DefiniteLength {
fn schema_name() -> String {
"DefiniteLength".to_string()
fn schema_name() -> Cow<'static, str> {
"DefiniteLength".into()
}
fn json_schema(_generator: &mut SchemaGenerator) -> Schema {
use schemars::schema::{InstanceType, SchemaObject, StringValidation};
Schema::Object(SchemaObject {
instance_type: Some(InstanceType::String.into()),
string: Some(Box::new(StringValidation {
pattern: Some(r"^-?\d+(\.\d+)?(px|rem|%)$".to_string()),
..Default::default()
})),
..Default::default()
fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
json_schema!({
"type": "string",
"pattern": r"^-?\d+(\.\d+)?(px|rem|%)$"
})
}
}
@@ -3480,20 +3469,14 @@ impl TryFrom<&'_ str> for Length {
}
impl JsonSchema for Length {
fn schema_name() -> String {
"Length".to_string()
fn schema_name() -> Cow<'static, str> {
"Length".into()
}
fn json_schema(_generator: &mut SchemaGenerator) -> Schema {
use schemars::schema::{InstanceType, SchemaObject, StringValidation};
Schema::Object(SchemaObject {
instance_type: Some(InstanceType::String.into()),
string: Some(Box::new(StringValidation {
pattern: Some(r"^(auto|-?\d+(\.\d+)?(px|rem|%))$".to_string()),
..Default::default()
})),
..Default::default()
fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
json_schema!({
"type": "string",
"pattern": r"^(auto|-?\d+(\.\d+)?(px|rem|%))$"
})
}
}

View File

@@ -2,7 +2,7 @@ use std::rc::Rc;
use collections::HashMap;
use crate::{Action, InvalidKeystrokeError, KeyBindingContextPredicate, Keystroke};
use crate::{Action, InvalidKeystrokeError, KeyBindingContextPredicate, Keystroke, SharedString};
use smallvec::SmallVec;
/// A keybinding and its associated metadata, from the keymap.
@@ -11,6 +11,8 @@ pub struct KeyBinding {
pub(crate) keystrokes: SmallVec<[Keystroke; 2]>,
pub(crate) context_predicate: Option<Rc<KeyBindingContextPredicate>>,
pub(crate) meta: Option<KeyBindingMetaIndex>,
/// The json input string used when building the keybinding, if any
pub(crate) action_input: Option<SharedString>,
}
impl Clone for KeyBinding {
@@ -20,6 +22,7 @@ impl Clone for KeyBinding {
keystrokes: self.keystrokes.clone(),
context_predicate: self.context_predicate.clone(),
meta: self.meta,
action_input: self.action_input.clone(),
}
}
}
@@ -32,7 +35,7 @@ impl KeyBinding {
} else {
None
};
Self::load(keystrokes, Box::new(action), context_predicate, None).unwrap()
Self::load(keystrokes, Box::new(action), context_predicate, None, None).unwrap()
}
/// Load a keybinding from the given raw data.
@@ -41,6 +44,7 @@ impl KeyBinding {
action: Box<dyn Action>,
context_predicate: Option<Rc<KeyBindingContextPredicate>>,
key_equivalents: Option<&HashMap<char, char>>,
action_input: Option<SharedString>,
) -> std::result::Result<Self, InvalidKeystrokeError> {
let mut keystrokes: SmallVec<[Keystroke; 2]> = keystrokes
.split_whitespace()
@@ -62,6 +66,7 @@ impl KeyBinding {
action,
context_predicate,
meta: None,
action_input,
})
}
@@ -110,6 +115,11 @@ impl KeyBinding {
pub fn meta(&self) -> Option<KeyBindingMetaIndex> {
self.meta
}
/// Get the action input associated with the action for this binding
pub fn action_input(&self) -> Option<SharedString> {
self.action_input.clone()
}
}
impl std::fmt::Debug for KeyBinding {

View File

@@ -151,7 +151,7 @@ pub fn guess_compositor() -> &'static str {
pub(crate) fn current_platform(_headless: bool) -> Rc<dyn Platform> {
Rc::new(
WindowsPlatform::new()
.inspect_err(|err| show_error("Error: Zed failed to launch", err.to_string()))
.inspect_err(|err| show_error("Failed to launch", err.to_string()))
.unwrap(),
)
}

View File

@@ -1299,12 +1299,8 @@ mod windows_renderer {
size: Default::default(),
transparent,
};
BladeRenderer::new(context, &raw, config).inspect_err(|err| {
show_error(
"Error: Zed failed to initialize BladeRenderer",
err.to_string(),
)
})
BladeRenderer::new(context, &raw, config)
.inspect_err(|err| show_error("Failed to initialize BladeRenderer", err.to_string()))
}
struct RawWindow {

View File

@@ -3,7 +3,7 @@
//! application to avoid having to import each trait individually.
pub use crate::{
AppContext as _, BorrowAppContext, Context, Element, FocusableElement, InteractiveElement,
IntoElement, ParentElement, Refineable, Render, RenderOnce, StatefulInteractiveElement, Styled,
StyledImage, VisualContext, util::FluentBuilder,
AppContext as _, BorrowAppContext, Context, Element, InteractiveElement, IntoElement,
ParentElement, Refineable, Render, RenderOnce, StatefulInteractiveElement, Styled, StyledImage,
VisualContext, util::FluentBuilder,
};

View File

@@ -2,7 +2,10 @@ use derive_more::{Deref, DerefMut};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{borrow::Borrow, sync::Arc};
use std::{
borrow::{Borrow, Cow},
sync::Arc,
};
use util::arc_cow::ArcCow;
/// A shared string is an immutable string that can be cheaply cloned in GPUI
@@ -23,12 +26,16 @@ impl SharedString {
}
impl JsonSchema for SharedString {
fn schema_name() -> String {
fn inline_schema() -> bool {
String::inline_schema()
}
fn schema_name() -> Cow<'static, str> {
String::schema_name()
}
fn json_schema(r#gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
String::json_schema(r#gen)
fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
String::json_schema(generator)
}
}

View File

@@ -1,6 +1,7 @@
use std::borrow::Cow;
use std::sync::Arc;
use schemars::schema::{InstanceType, SchemaObject};
use schemars::{JsonSchema, json_schema};
/// The OpenType features that can be configured for a given font.
#[derive(Default, Clone, Eq, PartialEq, Hash)]
@@ -128,36 +129,23 @@ impl serde::Serialize for FontFeatures {
}
}
impl schemars::JsonSchema for FontFeatures {
fn schema_name() -> String {
impl JsonSchema for FontFeatures {
fn schema_name() -> Cow<'static, str> {
"FontFeatures".into()
}
fn json_schema(_: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
let mut schema = SchemaObject::default();
schema.instance_type = Some(schemars::schema::SingleOrVec::Single(Box::new(
InstanceType::Object,
)));
{
let mut property = SchemaObject {
instance_type: Some(schemars::schema::SingleOrVec::Vec(vec![
InstanceType::Boolean,
InstanceType::Integer,
])),
..Default::default()
};
{
let mut number_constraints = property.number();
number_constraints.multiple_of = Some(1.0);
number_constraints.minimum = Some(0.0);
}
schema
.object()
.pattern_properties
.insert("[0-9a-zA-Z]{4}$".into(), property.into());
}
schema.into()
fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
json_schema!({
"type": "object",
"patternProperties": {
"[0-9a-zA-Z]{4}$": {
"type": ["boolean", "integer"],
"minimum": 0,
"multipleOf": 1
}
},
"additionalProperties": false
})
}
}

View File

@@ -16,9 +16,11 @@ fn test_action_macros() {
#[derive(PartialEq, Clone, Deserialize, JsonSchema, Action)]
#[action(namespace = test_only)]
struct AnotherSomeAction;
#[serde(deny_unknown_fields)]
struct AnotherAction;
#[derive(PartialEq, Clone, gpui::private::serde_derive::Deserialize)]
#[serde(deny_unknown_fields)]
struct RegisterableAction {}
register_action!(RegisterableAction);

View File

@@ -159,8 +159,8 @@ pub(crate) fn derive_action(input: TokenStream) -> TokenStream {
}
fn action_json_schema(
_generator: &mut gpui::private::schemars::r#gen::SchemaGenerator,
) -> Option<gpui::private::schemars::schema::Schema> {
_generator: &mut gpui::private::schemars::SchemaGenerator,
) -> Option<gpui::private::schemars::Schema> {
#json_schema_fn_body
}

View File

@@ -967,6 +967,7 @@ fn toggle_show_inline_completions_for_language(
all_language_settings(None, cx).show_edit_predictions(Some(&language), cx);
update_settings_file::<AllLanguageSettings>(fs, cx, move |file, _| {
file.languages
.0
.entry(language.name())
.or_default()
.show_edit_predictions = Some(!show_edit_predictions);

View File

@@ -39,6 +39,7 @@ globset.workspace = true
gpui.workspace = true
http_client.workspace = true
imara-diff.workspace = true
inventory.workspace = true
itertools.workspace = true
log.workspace = true
lsp.workspace = true

View File

@@ -2006,7 +2006,7 @@ fn test_autoindent_language_without_indents_query(cx: &mut App) {
#[gpui::test]
fn test_autoindent_with_injected_languages(cx: &mut App) {
init_settings(cx, |settings| {
settings.languages.extend([
settings.languages.0.extend([
(
"HTML".into(),
LanguageSettingsContent {

View File

@@ -39,11 +39,7 @@ use lsp::{CodeActionKind, InitializeParams, LanguageServerBinary, LanguageServer
pub use manifest::{ManifestDelegate, ManifestName, ManifestProvider, ManifestQuery};
use parking_lot::Mutex;
use regex::Regex;
use schemars::{
JsonSchema,
r#gen::SchemaGenerator,
schema::{InstanceType, Schema, SchemaObject},
};
use schemars::{JsonSchema, SchemaGenerator, json_schema};
use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
use serde_json::Value;
use settings::WorktreeId;
@@ -694,7 +690,6 @@ pub struct LanguageConfig {
pub matcher: LanguageMatcher,
/// List of bracket types in a language.
#[serde(default)]
#[schemars(schema_with = "bracket_pair_config_json_schema")]
pub brackets: BracketPairConfig,
/// If set to true, auto indentation uses last non empty line to determine
/// the indentation level for a new line.
@@ -735,6 +730,13 @@ pub struct LanguageConfig {
/// Starting and closing characters of a block comment.
#[serde(default)]
pub block_comment: Option<(Arc<str>, Arc<str>)>,
/// A list of additional regex patterns that should be treated as prefixes
/// for creating boundaries during rewrapping, ensuring content from one
/// prefixed section doesn't merge with another (e.g., markdown list items).
/// By default, Zed treats as paragraph and comment prefixes as boundaries.
#[serde(default, deserialize_with = "deserialize_regex_vec")]
#[schemars(schema_with = "regex_vec_json_schema")]
pub rewrap_prefixes: Vec<Regex>,
/// A list of language servers that are allowed to run on subranges of a given language.
#[serde(default)]
pub scope_opt_in_language_servers: Vec<LanguageServerName>,
@@ -914,6 +916,7 @@ impl Default for LanguageConfig {
autoclose_before: Default::default(),
line_comments: Default::default(),
block_comment: Default::default(),
rewrap_prefixes: Default::default(),
scope_opt_in_language_servers: Default::default(),
overrides: Default::default(),
word_characters: Default::default(),
@@ -944,10 +947,9 @@ fn deserialize_regex<'de, D: Deserializer<'de>>(d: D) -> Result<Option<Regex>, D
}
}
fn regex_json_schema(_: &mut SchemaGenerator) -> Schema {
Schema::Object(SchemaObject {
instance_type: Some(InstanceType::String.into()),
..Default::default()
fn regex_json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
json_schema!({
"type": "string"
})
}
@@ -961,6 +963,22 @@ where
}
}
fn deserialize_regex_vec<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<Regex>, D::Error> {
let sources = Vec::<String>::deserialize(d)?;
let mut regexes = Vec::new();
for source in sources {
regexes.push(regex::Regex::new(&source).map_err(de::Error::custom)?);
}
Ok(regexes)
}
fn regex_vec_json_schema(_: &mut SchemaGenerator) -> schemars::Schema {
json_schema!({
"type": "array",
"items": { "type": "string" }
})
}
#[doc(hidden)]
#[cfg(any(test, feature = "test-support"))]
pub struct FakeLspAdapter {
@@ -988,12 +1006,12 @@ pub struct FakeLspAdapter {
/// This struct includes settings for defining which pairs of characters are considered brackets and
/// also specifies any language-specific scopes where these pairs should be ignored for bracket matching purposes.
#[derive(Clone, Debug, Default, JsonSchema)]
#[schemars(with = "Vec::<BracketPairContent>")]
pub struct BracketPairConfig {
/// A list of character pairs that should be treated as brackets in the context of a given language.
pub pairs: Vec<BracketPair>,
/// A list of tree-sitter scopes for which a given bracket should not be active.
/// N-th entry in `[Self::disabled_scopes_by_bracket_ix]` contains a list of disabled scopes for an n-th entry in `[Self::pairs]`
#[serde(skip)]
pub disabled_scopes_by_bracket_ix: Vec<Vec<String>>,
}
@@ -1003,10 +1021,6 @@ impl BracketPairConfig {
}
}
fn bracket_pair_config_json_schema(r#gen: &mut SchemaGenerator) -> Schema {
Option::<Vec<BracketPairContent>>::json_schema(r#gen)
}
#[derive(Deserialize, JsonSchema)]
pub struct BracketPairContent {
#[serde(flatten)]
@@ -1841,6 +1855,14 @@ impl LanguageScope {
.map(|e| (&e.0, &e.1))
}
/// Returns additional regex patterns that act as prefix markers for creating
/// boundaries during rewrapping.
///
/// By default, Zed treats as paragraph and comment prefixes as boundaries.
pub fn rewrap_prefixes(&self) -> &[Regex] {
&self.language.config.rewrap_prefixes
}
/// Returns a list of language-specific word characters.
///
/// By default, Zed treats alphanumeric characters (and '_') as word characters for

View File

@@ -1170,7 +1170,7 @@ impl LanguageRegistryState {
if let Some(theme) = self.theme.as_ref() {
language.set_theme(theme.syntax());
}
self.language_settings.languages.insert(
self.language_settings.languages.0.insert(
language.name(),
LanguageSettingsContent {
tab_size: language.config.tab_size,

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