Compare commits

..

73 Commits

Author SHA1 Message Date
Conrad Irwin
40be8253e1 Don't pass CLI env to terminal either 2025-05-17 00:30:53 +02:00
Conrad Irwin
2416c79eea os:: 2025-05-16 23:50:06 +02:00
Conrad Irwin
bd0d9cf9f5 Merge branch 'main' into dont-set-shell 2025-05-16 23:23:16 +02:00
Conrad Irwin
dc8bc6a819 Fix imports 2025-05-16 23:18:28 +02:00
Gen Tamura
c7725e31d9 terminal: Implement basic Japanese IME support on macOS (#29879)
## Description

This PR implements basic support for Japanese Input Method Editors
(IMEs) in the Zed terminal on macOS, addressing issue #9900. Previously,
users had to switch input modes to confirm Japanese text, and pre-edit
(marked) text was not displayed.

With these changes:

- **Marked Text Display:** Pre-edit text (e.g., underlined characters
during Japanese composition) is now rendered directly in the terminal at
the cursor's current position.
- **Composition Confirmation:** Pressing Enter correctly finalizes the
IME composition, clears the marked text, and sends the confirmed string
to the underlying PTY process. This allows for a more natural input flow
similar to other macOS applications like iTerm2.
- **State Management:** IME state (marked text and its selected range
within the marked text) is now managed within the `TerminalView` struct.
- **Input Handling:** `TerminalInputHandler` has been updated to
correctly process IME callbacks (`replace_and_mark_text_in_range`,
`replace_text_in_range`, `unmark_text`, `marked_text_range`) by
interacting with `TerminalView`.
- **Painting Logic:** `TerminalElement::paint` now fetches the marked
text and its range from `TerminalView` and renders it with an underline.
The standard terminal cursor is hidden when marked text is present to
avoid visual clutter.
- **Candidate Window Positioning:**
`TerminalInputHandler::bounds_for_range` now attempts to provide more
accurate bounds for the IME candidate window by using the actual painted
bounds of the pre-edit text, falling back to a cursor-based
approximation if necessary.

This significantly improves the usability of the Zed terminal for users
who need to input Japanese characters, bringing the experience closer to
system-standard IME behavior.

## Movies


https://github.com/user-attachments/assets/be6c7597-7b65-49a6-b376-e1adff6da974

---

Closes #9900

Release Notes:

- **Terminal:** Implemented basic support for Japanese Input Method
Editors (IMEs) on macOS. Users can now see pre-edit (marked) text as
they type Japanese and confirm their input with the Enter key directly
in the terminal. This provides a more natural and efficient experience
for Japanese language input. (Fixes #9900)

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-05-16 23:10:41 +02:00
Conrad Irwin
7826c6fc2c it depends 2025-05-16 22:53:49 +02:00
Conrad Irwin
dce95f781b more 2025-05-16 22:49:55 +02:00
Nate Butler
e26620d1cf gpui: Add a standard text example (#30747)
This is a dumb first pass at a standard text example. We'll use this to
start digging in to some text/scale rendering issues.

There will be a ton of follow-up features to this, but starting simple.

Release Notes:

- N/A
2025-05-16 17:35:44 +02:00
Ben Brandt
9dabf491f0 agent: Only focus on the context strip if it has items to display (#30379) 2025-05-16 12:05:03 -03:00
Danilo Leal
f2dcc98216 agent: Improve layout shift in the previous message editor (#30825)
This PR also moves the context strip to be at the top, so it matches the
main message editor, making the arrow-up keyboard interaction to focus
on it to work the same way.

Release Notes:

- agent: Made the previous message editing UX more consistent with the
main message editor.
2025-05-16 11:36:37 -03:00
Jakob Herpel
23bbfc4b94 Run ignored test when running single test (#30830)
Release Notes:

- languages: Run ignored test if user wants to run one specific test
2025-05-16 14:23:27 +00:00
张小白
98aefcca83 windows: Some refactor (#30826)
Release Notes:

- N/A
2025-05-16 14:14:42 +00:00
Remco Smits
9be1e9aab1 debugger: Prevent pane context menu from showing on secondary mouse click in list entries (#30781)
This PR prevents the debug panel pane context menu from showing when you
click your secondary mouse button in **stackframe**, **breakpoint** and
**module** list entries.

Release Notes:

- N/A
2025-05-16 15:43:12 +02:00
Anthony Eid
33b60bc16d debugger: Fix inline values panic when selecting stack frames (#30821)
Release Notes:

- debugger beta: Fix panic that could occur when selecting a stack frame
- debugger beta: Fix inline values not showing in stack trace view

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
Co-authored-by: Remco Smits <djsmits12@gmail.com>
2025-05-16 15:42:09 +02:00
Smit Barmase
0355b9dfab editor: Fix line comments not extending when adding new line immediately after slash (#30824)
This PR fixes a bug where comments don't extend when cursor is right
next to the second slash. We added `// ` as a prefix character to
correctly position the cursor after a new line, but this broke comment
validation by including that trailing space, which it shouldn't.

Now both line comments and block comments (already handled in JSDoc PR)
can extend right after the prefix without needing an additional space.

Before:


https://github.com/user-attachments/assets/ca4d4c1b-b9b9-4f1b-b47a-56ae35776f41

After:


https://github.com/user-attachments/assets/b3408e1e-3efe-4787-ba68-d33cd2ea8563

Release Notes:

- Fixed issue where comments weren't extending when adding new line
immediately after comment prefix (`//`).
2025-05-16 19:11:37 +05:30
Danilo Leal
6bec76cd5d agent: Allow dismissing previous message by clicking on the backdrop (#30822)
Release Notes:

- agent: Improved UX for dismissing an edit to a previous message.
2025-05-16 10:25:21 -03:00
张小白
d4f47aa653 client: Add support for HTTP/HTTPS proxy (#30812)
Closes #30732

I tested it on my machine, and the HTTP proxy is working properly now.

Release Notes:

- N/A
2025-05-16 20:35:30 +08:00
Oleksiy Syvokon
5112fcebeb evals: Make LLMs configurable in edit_agent evals (#30813)
Release Notes:

- N/A
2025-05-16 11:10:15 +00:00
Ben Kunkle
dcf7f714f7 Revert "Revert "python: Enable subroot detection for pylsp and pyright (#27364)" (#29658)" (#30810)
Revert "Revert "python: Enable subroot detection for pylsp and pyright
(#27364)" (#29658)"

This reverts commit 59708ef56c.

Closes #29699

Release Notes:

- N/A
2025-05-16 07:05:33 -04:00
Conrad Irwin
3516881ca1 more 2025-05-16 12:30:34 +02:00
Conrad Irwin
76376144a5 license 2025-05-16 12:01:29 +02:00
Smit Barmase
16f668b8e3 editor: Add astrick on new line in multiline comment for Go, Rust, C, and C++ (#30808)
Add asterisk on new line in multiline comments for Go, Rust, C, and C++.
While `*` is entirely for style. There's no actual need for it. It can
be disabled from setting. More:
https://doc.rust-lang.org/rust-by-example/hello/comment.html

<img width="491" alt="image"
src="https://github.com/user-attachments/assets/385b1eb5-be81-446c-b7cf-34165d6b384a"
/>

Release Notes:

- Added automatic asterisk insertion for new lines in multiline comments
for Go, Rust, C, and C++. This can be disable by setting
`extend_comment_on_newline` to `false`.
2025-05-16 15:30:04 +05:30
Conrad Irwin
9830354690 Don't set env on the process 2025-05-16 11:58:04 +02:00
Danilo Leal
0f4e52bde8 agent: Ensure background color is the same even while zoomed in (#30804)
Release Notes:

- agent: Fixed the background color of the agent panel changing if you
zoomed it in.
2025-05-16 06:48:22 -03:00
Danilo Leal
dfe37b0a07 agent: Make Markdown codeblocks expanded by default (#30806)
Release Notes:

- N/A
2025-05-16 06:48:15 -03:00
Ben Kunkle
2da37988b5 fix bedrock name in assistant settings schema (#30805)
Closes #30778 

Release Notes:

- Fixed an issue with the assistant settings where `amazon-bedrock` was
incorrectly called `bedrock` in the settings schema
2025-05-16 09:29:58 +00:00
Peter Tripp
05955e4faa keymap: Move 'project_panel::NewSearchInDirectory' to a dedicated bind (#29681)
Previously cmd-shift-f / ctrl-shift-f had different behavior when
invoked from the project panel context than from an editor (for project
panel `include` field was populated from the currently select project
panel directory).

Change this so that it has it's own keybind of cmd-alt-shift-f /
ctrl-alt-shift-f so cmd-shift-f and ctrl-shift-f has consistent behavior
(`pane::DeploySearch`) everywhere.

Release Notes:

- Add dedicated keybind for "Find in Folder..." from the project panel
(cmd-alt-shift-f, ctrl-alt-shift-f).
2025-05-16 11:05:13 +02:00
Ben Kunkle
1d043b37fb askpass: Workaround rust lang 69343 (#30774)
Closes #ISSUE

Work around https://github.com/rust-lang/rust/issues/69343 in askpass

Release Notes:

- linux: Fixed an issue with askpass where the Zed binary path would be incorrect after an auto-update is installed
but not yet applied
2025-05-16 05:04:36 -04:00
Smit Barmase
18d39e3f81 editor: Improve JSDoc extend comment on newline to follow convention (#30800)
Follow up for https://github.com/zed-industries/zed/pull/30768

This PR makes JSDoc auto comment on new line lot better by:

- Inserting delimiters regardless of whether previous delimiters have
trailing spaces or not
- When on start tag, auto-indenting both prefix and end tag upon new
line

This makes it correct as per convention out of the box. No need to
manually adjust spaces on every new line.


https://github.com/user-attachments/assets/81b8e05a-fe8a-4459-9e90-c8a3d70a51a2

Release Notes:

- Improved JSDoc auto-commenting on newline which now correctly indents
as per convention.
2025-05-16 12:42:11 +05:30
Oleksiy Syvokon
cc3a28a8e8 agent: Fix unnecessary "tool result too long" (#30798)
Release Notes:

- N/A
2025-05-16 06:39:02 +00:00
Piotr Osiewicz
0f17e82154 chore: Bump Rust to 1.87 (#30739)
Closes #ISSUE

Release Notes:

- N/A
2025-05-15 22:28:52 +00:00
morgankrey
a316428686 docs: Update Claude 3.5 Sonnet context window (#30518)
Release Notes:

- N/A

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-05-15 13:15:36 -04:00
Ben Brandt
355266988d extension: Update wasi preview adapter (#30759)
Replace dynamic downloading of WASI adapter with the provided crate.

More importantly, this makes sure we are using the same adapter version
as our version of wasmtime, which includes several fixes.

Arguably we could also at this point update to wasm32-wasip2 target and
remove this dependency as well if we want, but that might need further
testing.

Release Notes:

- N/A
2025-05-15 19:10:13 +02:00
Danilo Leal
72007c9a62 docs: Polish AI content (#30770)
Release Notes:

- N/A
2025-05-15 13:59:17 -03:00
Smit Barmase
c2feffac9d editor: Add prefix on newline in documentation block (e.g. JSDoc) (#30768)
Closes #8973

- [x] Tests


https://github.com/user-attachments/assets/7fc6608f-1c11-4c70-a69b-34bfa8f789a2

Release Notes:

- Added auto-insertion of asterisk (*) prefix when creating new lines
within JSDoc comment blocks.
2025-05-15 20:30:06 +05:30
张小白
4b7b5db58c windows: Remove unnecessay helper function (#30764)
Release Notes:

- N/A
2025-05-15 14:22:04 +00:00
张小白
58ba833792 windows: Fix keystroke (#30753)
Closes #22656

Part of #29144, this PR completely rewrites the key handling logic on
Windows, making it much more consistent with how things work on macOS.
However, one remaining issue is that on Windows, we should be using
`Ctrl+Shift+4` instead of `Ctrl+$`. That part is expected to be
addressed in #29144.


Release Notes:

- N/A
2025-05-15 20:49:06 +08:00
Joseph T. Lyons
f021b401f4 Fix command casing in issue templates (#30761)
Release Notes:

- N/A
2025-05-15 12:15:27 +00:00
Antonio Scandurra
47f6d4e5a7 Fix rejecting overwritten files if the agent previously edited them (#30744)
Release Notes:

- Fixed rejecting overwritten files if the agent had previously edited them.
2025-05-15 09:47:54 +00:00
Danilo Leal
e60f029525 agent: Add adjustments to settings view (#30743)
- Make provider blocks collapsed by default
- Fix sections growing unnecessarily when there's available space

Release Notes:

- N/A
2025-05-15 06:30:45 -03:00
Marshall Bowers
d7b5c61ec8 ui_macros: Remove unused module (#30741)
This PR removes an unused module from the `ui_macros` crate.

Release Notes:

- N/A
2025-05-15 08:53:38 +00:00
Marshall Bowers
23d42e3eaf agent: Use inventory for AgentPreview (#30740)
This PR updates the `AgentPreview` to use `inventory` instead of
`linkme`.

Release Notes:

- N/A
2025-05-15 08:36:13 +00:00
CharlesChen0823
b2fc4064c0 gpui: Avoid dereferencing null pointer (#30579)
as
[comments](https://github.com/zed-industries/zed/pull/24545#issuecomment-2872833658),
I really don't known why, But IMO, add this code is not harm.

If you think this is not necessary, can close.

Release Notes:

- N/A
2025-05-15 09:50:58 +02:00
Finn Evers
bba3db9378 docs: Add minimap configuration section (#30724)
This PR adds some documentation about the minimap to the official docs.

**Please note:** The [current preview release
notes](https://zed.dev/releases/preview/0.187.0) refer to the minimap PR
for configuration options. However, `font_size` and `width` were removed
as settings after some discussion but are still referenced in the PR
description, which might be misleading. On the other hand, some of the
available configuration options are not listed in the PR description. It
might be better to refer to the docs or the default settings in order to
avoid confusion.

Release Notes:

- N/A
2025-05-15 08:07:32 +02:00
Conrad Irwin
d01f2a1c9a Don't set the $SHELL env var 2025-05-15 00:19:59 +02:00
tidely
5078f0b5ef client: Remove extra clone, pass big struct by reference (#30716)
Commit titles explain all of the changes

Release Notes:

- N/A
2025-05-15 00:16:25 +02:00
Marshall Bowers
607bfd3b1c component: Replace linkme with inventory (#30705)
This PR replaces the use of `linkme` with `inventory` for the component
preview registration.

Release Notes:

- N/A
2025-05-14 23:29:11 +02:00
Cole Miller
87cb498a41 debugger: Make the stack frame list and module list keyboard-navigable (#30682)
- Switch stack frame list and module list to `UniformList` to access
scrolling behavior
- Implement `menu::` navigation actions

Release Notes:

- Debugger Beta: Added support for menu navigation actions (`ctrl-n`,
`ctrl-p`, etc.) in the stack frame list and module list.
2025-05-14 22:23:59 +02:00
Oleksiy Syvokon
6420df3975 eval: Count execution errors as failures (#30712)
- Evals returning an error (e.g., LLM API format mismatch) were silently
skipped in the aggregated results. Now we count them as a failure (0%
success score).

- Setting the `VERBOSE` environment variable to something non-empty
disables string truncation

Release Notes:

- N/A
2025-05-14 20:44:19 +03:00
Ben Kunkle
83498ebf2b Improve error message around failing to install dev extensions (#30711)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-05-14 17:22:17 +00:00
Ben Kunkle
1fb1fecb0a rust: Add injection for leptos view macro (#30710)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-05-14 16:48:22 +00:00
tidely
bc99a86bb7 Reduce allocations (#30693)
Removes a unnecessary string conversion and some clones

Release Notes:

- N/A
2025-05-14 18:29:28 +02:00
Kirill Bulatov
fcfe4e2c14 Reuse existing language servers for invisible worktrees (#30707)
Closes https://github.com/zed-industries/zed/issues/20767

Before:


https://github.com/user-attachments/assets/6438eb26-796a-4586-9b20-f49d9a133624


After:



https://github.com/user-attachments/assets/b3fc2f8b-2873-443f-8d80-ab4a35cf0c09



Release Notes:

- Fixed external files spawning extra language servers
2025-05-14 16:24:17 +00:00
Rob McBroom
ef511976be Add a separator before Quit in the application menu (#30697)
macOS applications should have a separator between “Show All” and “Quit”
in the application menu.
2025-05-14 16:19:09 +00:00
Marshall Bowers
c80aaca0c5 zed_extension_api: Format dap.wit (#30701)
This PR formats the `dap.wit` file.

Release Notes:

- N/A
2025-05-14 15:05:29 +00:00
Danilo Leal
234d6ce5f5 agent: Fix Markdown codeblock header buttons (#30645)
Closes https://github.com/zed-industries/zed/issues/30592

Release Notes:

- agent: Fixed Markdown codeblock header buttons being pushed by long
paths/file names.
2025-05-14 10:49:02 -03:00
Tristan Hume
96a0568fb7 Add setting to disable the sign in button (#30450)
Designed to pair with #30444 to enable enterprises to make it harder to
sign into the collab server and perhaps accidentally end up sending code
to Zed.

Release Notes:

- N/A

Co-authored-by: Mikayla Maki <mikayla@zed.dev>
2025-05-14 13:39:04 +00:00
Oleksiy Syvokon
b6828e5ce8 agent: Don't duplicate recommended models in all models list (#30692)
Release Notes:

- N/A
2025-05-14 13:21:41 +00:00
Smit Barmase
78d3ce4090 editor: Handle more completion sort cases (#30690)
Adds 3 more test cases where local variable should be preferred over
method, and local method over library methods.

Before / After:

<img height="280" alt="before-rust"
src="https://github.com/user-attachments/assets/72b34ce8-89ff-4c2b-87dc-9e63f855d31e"
/>
<img height="280" alt="after-rust"
src="https://github.com/user-attachments/assets/8e23c9ca-576c-4dc5-8946-fa37554a19e0"
/>

Before / After:

<img height="280" alt="before-react"
src="https://github.com/user-attachments/assets/f7070413-e397-441a-a0c1-16d8ce25aa12"
/>
<img height="280" alt="after-react"
src="https://github.com/user-attachments/assets/7a095954-7844-4a3e-bf59-5420b7ffdb03"
/>

Release Notes:

- N/A
2025-05-14 18:49:39 +05:30
Tristan Hume
d01559f9bc Add setting for enabling/disabling feedback (#30448)
This is useful for enterprises, especially in combination with #30444,
to ensure code never gets sent to Zed.

Release Notes:

- N/A
2025-05-14 15:10:31 +02:00
Ben Brandt
645f662853 workspace: Remove default keybindings for close active dock (#30691)
Release Notes:

- N/A
2025-05-14 12:55:07 +00:00
Oleksiy Syvokon
d42cb111f4 agent: Fix tool use in Gemini (#30689)
Thread doesn't run pending tools when `stop_reason` is not `ToolUse`.
Perhaps we should change that so that it always runs pending tools if
there are some, but for now this change just fixes setting `stop_reason`
for Google models.

Release Notes:

- N/A
2025-05-14 15:43:17 +03:00
Nate Butler
dce6e96c16 debugger: Tidy up dropdown menus (#30679)
Before
![CleanShot 2025-05-14 at 13 22
44@2x](https://github.com/user-attachments/assets/c6c06c5c-571d-4913-a691-161f44bba27c)

After
![CleanShot 2025-05-14 at 13 22
17@2x](https://github.com/user-attachments/assets/0a25a053-81a3-4b96-8963-4b770b1e5b45)

Release Notes:

- N/A
2025-05-14 11:32:51 +00:00
Finn Evers
4280bff10a Reapply "ui: Account for padding of parent container during scrollbar layout" (#30577)
This PR reapplies #27402 which was reverted in
https://github.com/zed-industries/zed/pull/30544 due to the issue
@ConradIrwin reported in
https://github.com/zed-industries/zed/pull/27402#issuecomment-2871745132.
The reported issue is already present on main but not visible, see
https://github.com/zed-industries/zed/pull/27402#issuecomment-2872546903
for more context and reproduction steps.

The fix here was to move the padding for the hover popover up to the
parent container. This does not fix the underlying problem but serves as
workaround without any disadvantages until a better solution is found. I
would currently guess that the underlying issue might be related to some
rem-size calculations for small font sizes or something similar (e.g.
https://github.com/zed-industries/zed/pull/22732 could possibly be
somewhat related).

Notably, the fix here does not cause any difference in layouting (the
following screenshots are actually distinct images), yet fixes the
problem at hand.

### Default font size (`15px`) 

| `main` | This PR |
| --- | --- |
|
![main_large](https://github.com/user-attachments/assets/66d38827-9023-4f78-9ceb-54fb13c21e41)
|![PR](https://github.com/user-attachments/assets/7af82bd2-2732-4cba-8d4b-54605d6ff101)
|

### Smaller font size (`12px`)

| `main` | This PR |
| --- | --- |
|
![pr_large](https://github.com/user-attachments/assets/d43be6e6-6840-422c-baf0-368aab733dac)
|
![PR](https://github.com/user-attachments/assets/43f60b2b-2578-45d2-bcab-44edf2612ce2)
|

Furthermore, for the second scenario, the popover would be scrollable on
main. As there is no scrollbar in the second image for this PR, this no
longer happens with this branch.


Release Notes:

- N/A
2025-05-14 13:26:14 +02:00
Thomas David Baker
ea5b289459 docs: Fix up some invalid JSON in OpenAI configuration example (#30663) 2025-05-14 11:19:01 +00:00
Finn Evers
09503333af project_settings: Fix default settings values for DiagnosticsSettings (#30686)
Follow-up to #30565

This PR fixes the default settings values for the `DiagnosticsSettings`.
The issue here was that due to the `#[derive(Default)]`, `button` would
be false by default, which unintentionally hid the diagnostics button by
default. The `#[serde(default = `default_true`)]` would only apply iff
the diagnostics key was already present in the user's settings. Thus, if
you have

```json
{
    "diagnostics": {...}
}
```

in your settings, the button would show (given it was not disabled).
However, if the key was not present, the button was not shown: Due to
the derived default for the entire struct, the value would be false.

This PR fixes this by implementing the default instead and moving the
`#[serde(default)]` up to the level of the struct.
I also did the same for the inline diagnostics settings, which already
had a default impl and thus only needed the serde default on the struct
instead of on all the struct fields.

Lastly, I simplified the title bar settings, since the serde attributes
previously had no effect anyway (deserialization happened in the
`TitlebarSettingsContent`, so these attributes had no effect) and we can
remove the `TitlebarSettingsContent` as well as the attributes if we
implement a proper default implementation instead.

Release Notes:

- Fixed the diagnostics status bar button being hidden by default.
2025-05-14 07:13:51 -04:00
Joseph T. Lyons
775370fd7d Bump Zed to v0.188 (#30685)
Release Notes:

-N/A
2025-05-14 10:41:09 +00:00
Anthony Eid
1077f2771e debugger: Fix launch picker program arg not using relative paths (#30680)
Release Notes:

- N/A
2025-05-14 09:51:13 +00:00
Anthony Eid
f4eea0db2e debugger: Fix panics when debugging with inline values or confirming in console (#30677)
The first panic was caused by an unwrap that assumed a file would always
have a root syntax node.

The second was caused by a double lease panic when clicking enter in the
debug console while there was a completion menu open

Release Notes:

- N/A
2025-05-14 09:50:42 +00:00
Conrad Irwin
ed361ff6a2 Rename debug: commands to dev: (#30675)
Closes #ISSUE

Release Notes:

- Breaking change: The actions used while developing Zed have been
renamed from `debug:` to `dev:` to avoid confusion with the new debugger
feature:
- - `dev::OpenDebugAdapterLogs`
- - `dev::OpenSyntaxTreeView`
- - `dev::OpenThemePreview`
- - `dev::OpenLanguageServerLogs`
- - `dev::OpenKeyContextView`
2025-05-14 11:15:27 +02:00
Umesh Yadav
7f9a365d8f docs: Fix shfmt github url (#30667)
Closes #30661 

Release Notes:

- N/A
2025-05-14 10:47:33 +02:00
Oleksiy Syvokon
255d8f7cf8 agent: Overwrite files more cautiously (#30649)
1. The `edit_file` tool tended to use `create_or_overwrite` a bit too
often, leading to corruption of long files. This change replaces the
boolean flag with an `EditFileMode` enum, which helps Agent make a more
deliberate choice when overwriting files.

With this change, the pass rate of the new eval increased from 10% to
100%.

2. eval: Added ability to run eval on top of an existing thread. Threads
can now be loaded from JSON files in the `SerializedThread` format,
which makes it easy to use real threads as starting points for
tests/evals.

3. Don't try to restore tool cards when running in headless or eval mode
-- we don't have a window to properly do this.

Release Notes:

- N/A
2025-05-14 10:40:44 +03:00
张小白
22f76ac1a7 windows: Remove unneeded ranges for replace_and_mark_text_in_range (#30668)
Release Notes:

- N/A
2025-05-14 07:26:10 +00:00
260 changed files with 6081 additions and 3388 deletions

View File

@@ -29,8 +29,8 @@ body:
id: environment
attributes:
label: Zed Version and System Specs
description: 'Open Zed, and in the command palette select "zed: Copy System Specs Into Clipboard"'
description: 'Open Zed, and in the command palette select "zed: copy system specs into clipboard"'
placeholder: |
Output of "zed: Copy System Specs Into Clipboard"
Output of "zed: copy system specs into clipboard"
validations:
required: true

View File

@@ -29,8 +29,8 @@ body:
id: environment
attributes:
label: Zed Version and System Specs
description: 'Open Zed, and in the command palette select "zed: Copy System Specs Into Clipboard"'
description: 'Open Zed, and in the command palette select "zed: copy system specs into clipboard"'
placeholder: |
Output of "zed: Copy System Specs Into Clipboard"
Output of "zed: copy system specs into clipboard"
validations:
required: true

View File

@@ -28,8 +28,8 @@ body:
id: environment
attributes:
label: Zed Version and System Specs
description: 'Open Zed, and in the command palette select "zed: Copy System Specs Into Clipboard"'
description: 'Open Zed, and in the command palette select "zed: copy system specs into clipboard"'
placeholder: |
Output of "zed: Copy System Specs Into Clipboard"
Output of "zed: copy system specs into clipboard"
validations:
required: true

View File

@@ -28,8 +28,8 @@ body:
id: environment
attributes:
label: Zed Version and System Specs
description: 'Open Zed, and in the command palette select "zed: Copy System Specs Into Clipboard"'
description: 'Open Zed, and in the command palette select "zed: copy system specs into clipboard"'
placeholder: |
Output of "zed: Copy System Specs Into Clipboard"
Output of "zed: copy system specs into clipboard"
validations:
required: true

View File

@@ -49,8 +49,8 @@ body:
attributes:
label: Zed Version and System Specs
description: |
Open Zed, from the command palette select "zed: Copy System Specs Into Clipboard"
Open Zed, from the command palette select "zed: copy system specs into clipboard"
placeholder: |
Output of "zed: Copy System Specs Into Clipboard"
Output of "zed: copy system specs into clipboard"
validations:
required: true

View File

@@ -26,9 +26,9 @@ body:
id: environment
attributes:
label: Zed Version and System Specs
description: 'Open Zed, and in the command palette select "zed: Copy System Specs Into Clipboard"'
description: 'Open Zed, and in the command palette select "zed: copy system specs into clipboard"'
placeholder: |
Output of "zed: Copy System Specs Into Clipboard"
Output of "zed: copy system specs into clipboard"
validations:
required: true
- type: textarea

View File

@@ -2,16 +2,14 @@
{
"label": "Debug Zed (CodeLLDB)",
"adapter": "CodeLLDB",
"program": "$ZED_WORKTREE_ROOT/target/debug/zed",
"request": "launch",
"cwd": "$ZED_WORKTREE_ROOT"
"program": "target/debug/zed",
"request": "launch"
},
{
"label": "Debug Zed (GDB)",
"adapter": "GDB",
"program": "$ZED_WORKTREE_ROOT/target/debug/zed",
"program": "target/debug/zed",
"request": "launch",
"cwd": "$ZED_WORKTREE_ROOT",
"initialize_args": {
"stopAtBeginningOfMainSubprogram": true
}

75
Cargo.lock generated
View File

@@ -81,12 +81,12 @@ dependencies = [
"http_client",
"indexed_docs",
"indoc",
"inventory",
"itertools 0.14.0",
"jsonschema",
"language",
"language_model",
"language_model_selector",
"linkme",
"log",
"lsp",
"markdown",
@@ -661,6 +661,7 @@ dependencies = [
"component",
"derive_more",
"editor",
"environment",
"feature_flags",
"fs",
"futures 0.3.31",
@@ -674,7 +675,6 @@ dependencies = [
"language",
"language_model",
"language_models",
"linkme",
"log",
"markdown",
"open",
@@ -2793,6 +2793,7 @@ dependencies = [
"anyhow",
"async-recursion 0.3.2",
"async-tungstenite",
"base64 0.22.1",
"chrono",
"clock",
"cocoa 0.26.0",
@@ -2804,6 +2805,7 @@ dependencies = [
"gpui_tokio",
"http_client",
"http_client_tls",
"httparse",
"log",
"parking_lot",
"paths",
@@ -2824,6 +2826,7 @@ dependencies = [
"time",
"tiny_http",
"tokio",
"tokio-native-tls",
"tokio-socks",
"url",
"util",
@@ -3177,7 +3180,7 @@ version = "0.1.0"
dependencies = [
"collections",
"gpui",
"linkme",
"inventory",
"parking_lot",
"strum 0.27.1",
"theme",
@@ -3251,6 +3254,7 @@ dependencies = [
"anyhow",
"async-trait",
"collections",
"environment",
"futures 0.3.31",
"gpui",
"log",
@@ -3303,6 +3307,7 @@ dependencies = [
"ctor",
"editor",
"env_logger 0.11.8",
"environment",
"fs",
"futures 0.3.31",
"gpui",
@@ -3976,6 +3981,7 @@ dependencies = [
"collections",
"dap-types",
"env_logger 0.11.8",
"environment",
"fs",
"futures 0.3.31",
"gpui",
@@ -4327,7 +4333,6 @@ dependencies = [
"gpui",
"indoc",
"language",
"linkme",
"log",
"lsp",
"markdown",
@@ -4851,6 +4856,21 @@ dependencies = [
"log",
]
[[package]]
name = "environment"
version = "0.1.0"
dependencies = [
"anyhow",
"collections",
"paths",
"serde_json",
"smol",
"tokio",
"util",
"which 6.0.3",
"workspace-hack",
]
[[package]]
name = "envy"
version = "0.4.2"
@@ -4956,6 +4976,7 @@ dependencies = [
"dirs 4.0.0",
"dotenv",
"env_logger 0.11.8",
"environment",
"extension",
"fs",
"futures 0.3.31",
@@ -5052,6 +5073,7 @@ dependencies = [
"async-trait",
"collections",
"dap",
"environment",
"fs",
"futures 0.3.31",
"gpui",
@@ -5067,6 +5089,7 @@ dependencies = [
"task",
"toml 0.8.20",
"util",
"wasi-preview1-component-adapter-provider",
"wasm-encoder 0.221.3",
"wasmparser 0.221.3",
"wit-component 0.221.3",
@@ -5110,6 +5133,7 @@ dependencies = [
"ctor",
"dap",
"env_logger 0.11.8",
"environment",
"extension",
"fs",
"futures 0.3.31",
@@ -5161,6 +5185,7 @@ dependencies = [
"fuzzy",
"gpui",
"language",
"log",
"num-format",
"picker",
"project",
@@ -5941,6 +5966,7 @@ dependencies = [
"async-trait",
"collections",
"derive_more",
"environment",
"futures 0.3.31",
"git2",
"gpui",
@@ -6025,7 +6051,6 @@ dependencies = [
"language",
"language_model",
"linkify",
"linkme",
"log",
"markdown",
"menu",
@@ -8162,26 +8187,6 @@ dependencies = [
"memchr",
]
[[package]]
name = "linkme"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22d227772b5999ddc0690e733f734f95ca05387e329c4084fe65678c51198ffe"
dependencies = [
"linkme-impl",
]
[[package]]
name = "linkme-impl"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71a98813fa0073a317ed6a8055dcd4722a49d9b862af828ee68449adb799b6be"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.100",
]
[[package]]
name = "linux-raw-sys"
version = "0.4.15"
@@ -9053,6 +9058,8 @@ dependencies = [
"async-trait",
"async-watch",
"async_zip",
"collections",
"environment",
"futures 0.3.31",
"http_client",
"log",
@@ -9104,7 +9111,6 @@ dependencies = [
"component",
"db",
"gpui",
"linkme",
"rpc",
"settings",
"sum_tree",
@@ -10817,6 +10823,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"collections",
"environment",
"fs",
"gpui",
"language",
@@ -10940,6 +10947,7 @@ dependencies = [
"dap",
"dap_adapters",
"env_logger 0.11.8",
"environment",
"extension",
"fancy-regex 0.14.0",
"fs",
@@ -11822,6 +11830,7 @@ dependencies = [
"askpass",
"async-trait",
"collections",
"environment",
"fs",
"futures 0.3.31",
"gpui",
@@ -11924,6 +11933,7 @@ dependencies = [
"command_palette_hooks",
"editor",
"env_logger 0.11.8",
"environment",
"feature_flags",
"file_icons",
"futures 0.3.31",
@@ -14061,6 +14071,7 @@ dependencies = [
"collections",
"editor",
"env_logger 0.11.8",
"environment",
"futures 0.3.31",
"gpui",
"http_client",
@@ -15685,7 +15696,6 @@ dependencies = [
"gpui",
"icons",
"itertools 0.14.0",
"linkme",
"menu",
"serde",
"settings",
@@ -15706,7 +15716,6 @@ dependencies = [
"component",
"editor",
"gpui",
"linkme",
"settings",
"theme",
"ui",
@@ -15718,7 +15727,6 @@ name = "ui_macros"
version = "0.1.0"
dependencies = [
"convert_case 0.8.0",
"linkme",
"proc-macro2",
"quote",
"syn 1.0.109",
@@ -16237,6 +16245,12 @@ dependencies = [
"wit-bindgen-rt 0.39.0",
]
[[package]]
name = "wasi-preview1-component-adapter-provider"
version = "29.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcd9f21bbde82ba59e415a8725e6ad0d0d7e9e460b1a3ccbca5bdee952c1a324"
[[package]]
name = "wasite"
version = "0.1.0"
@@ -16939,7 +16953,6 @@ dependencies = [
"gpui",
"install_cli",
"language",
"linkme",
"picker",
"project",
"schemars",
@@ -18540,7 +18553,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.187.0"
version = "0.188.0"
dependencies = [
"activity_indicator",
"agent",

View File

@@ -43,6 +43,7 @@ members = [
"crates/deepseek",
"crates/diagnostics",
"crates/docs_preprocessor",
"crates/environment",
"crates/editor",
"crates/eval",
"crates/extension",
@@ -250,6 +251,7 @@ debugger_ui = { path = "crates/debugger_ui" }
deepseek = { path = "crates/deepseek" }
diagnostics = { path = "crates/diagnostics" }
editor = { path = "crates/editor" }
environment = { path = "crates/environment" }
extension = { path = "crates/extension" }
extension_host = { path = "crates/extension_host" }
extensions_ui = { path = "crates/extensions_ui" }
@@ -465,7 +467,6 @@ jupyter-websocket-client = { git = "https://github.com/ConradIrwin/runtimed" ,r
libc = "0.2"
libsqlite3-sys = { version = "0.30.1", features = ["bundled"] }
linkify = "0.10.0"
linkme = "0.3.31"
log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] }
lsp-types = { git = "https://github.com/zed-industries/lsp-types", rev = "c9c189f1c5dd53c624a419ce35bc77ad6a908d18" }
markup5ever_rcdom = "0.3.0"
@@ -595,6 +596,7 @@ url = "2.2"
urlencoding = "2.1.2"
uuid = { version = "1.1.2", features = ["v4", "v5", "v7", "serde"] }
walkdir = "2.3"
wasi-preview1-component-adapter-provider = "29"
wasm-encoder = "0.221"
wasmparser = "0.221"
wasmtime = { version = "29", default-features = false, features = [
@@ -788,6 +790,9 @@ let_underscore_future = "allow"
# running afoul of the borrow checker.
too_many_arguments = "allow"
# We often have large enum variants yet we rarely actually bother with splitting them up.
large_enum_variant = "allow"
[workspace.metadata.cargo-machete]
ignored = [
"bindgen",
@@ -795,7 +800,6 @@ ignored = [
"prost_build",
"serde",
"component",
"linkme",
"documented",
"workspace-hack",
]

View File

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

View File

@@ -538,7 +538,6 @@
"ctrl-alt-b": "workspace::ToggleRightDock",
"ctrl-b": "workspace::ToggleLeftDock",
"ctrl-j": "workspace::ToggleBottomDock",
"ctrl-w": "workspace::CloseActiveDock",
"ctrl-alt-y": "workspace::CloseAllDocks",
"shift-find": "pane::DeploySearch",
"ctrl-shift-f": "pane::DeploySearch",
@@ -767,7 +766,7 @@
"alt-ctrl-r": "project_panel::RevealInFileManager",
"ctrl-shift-enter": "project_panel::OpenWithSystem",
"shift-find": "project_panel::NewSearchInDirectory",
"ctrl-shift-f": "project_panel::NewSearchInDirectory",
"ctrl-alt-shift-f": "project_panel::NewSearchInDirectory",
"shift-down": "menu::SelectNext",
"shift-up": "menu::SelectPrevious",
"escape": "menu::Cancel"

View File

@@ -608,7 +608,6 @@
"cmd-b": "workspace::ToggleLeftDock",
"cmd-r": "workspace::ToggleRightDock",
"cmd-j": "workspace::ToggleBottomDock",
"cmd-w": "workspace::CloseActiveDock",
"alt-cmd-y": "workspace::CloseAllDocks",
"cmd-shift-f": "pane::DeploySearch",
"cmd-shift-h": ["pane::DeploySearch", { "replace_enabled": true }],
@@ -826,7 +825,7 @@
"alt-cmd-r": "project_panel::RevealInFileManager",
"ctrl-shift-enter": "project_panel::OpenWithSystem",
"cmd-alt-backspace": ["project_panel::Delete", { "skip_prompt": false }],
"cmd-shift-f": "project_panel::NewSearchInDirectory",
"cmd-alt-shift-f": "project_panel::NewSearchInDirectory",
"shift-down": "menu::SelectNext",
"shift-up": "menu::SelectPrevious",
"escape": "menu::Cancel"

View File

@@ -335,7 +335,9 @@
// Whether to show onboarding banners in the titlebar.
"show_onboarding_banner": true,
// Whether to show user picture in the titlebar.
"show_user_picture": true
"show_user_picture": true,
// Whether to show the sign in button in the titlebar.
"show_sign_in": true
},
// Scrollbar related settings
"scrollbar": {
@@ -756,6 +758,8 @@
"stream_edits": false,
// When enabled, agent edits will be displayed in single-file editors for review
"single_file_review": true,
// When enabled, show voting thumbs for feedback on agent edits.
"enable_feedback": true,
"default_profile": "write",
"profiles": {
"write": {

View File

@@ -47,12 +47,12 @@ heed.workspace = true
html_to_markdown.workspace = true
http_client.workspace = true
indexed_docs.workspace = true
inventory.workspace = true
itertools.workspace = true
jsonschema.workspace = true
language.workspace = true
language_model.workspace = true
language_model_selector.workspace = true
linkme.workspace = true
log.workspace = true
lsp.workspace = true
markdown.workspace = true

View File

@@ -185,12 +185,14 @@ pub(crate) fn default_markdown_style(window: &Window, cx: &App) -> MarkdownStyle
let ui_font_size = TextSize::Default.rems(cx);
let buffer_font_size = TextSize::Small.rems(cx);
let mut text_style = window.text_style();
let line_height = buffer_font_size * 1.75;
text_style.refine(&TextStyleRefinement {
font_family: Some(theme_settings.ui_font.family.clone()),
font_fallbacks: theme_settings.ui_font.fallbacks.clone(),
font_features: Some(theme_settings.ui_font.features.clone()),
font_size: Some(ui_font_size.into()),
line_height: Some(line_height.into()),
color: Some(cx.theme().colors().text),
..Default::default()
});
@@ -383,18 +385,25 @@ fn render_markdown_code_block(
)
} else {
let content = if let Some(parent) = path_range.path.parent() {
let file_name = file_name.to_string_lossy().to_string();
let path = parent.to_string_lossy().to_string();
let path_and_file = format!("{}/{}", path, file_name);
h_flex()
.id(("code-block-header-label", ix))
.ml_1()
.gap_1()
.child(
Label::new(file_name.to_string_lossy().to_string())
.size(LabelSize::Small),
)
.child(
Label::new(parent.to_string_lossy().to_string())
.color(Color::Muted)
.size(LabelSize::Small),
)
.child(Label::new(file_name).size(LabelSize::Small))
.child(Label::new(path).color(Color::Muted).size(LabelSize::Small))
.tooltip(move |window, cx| {
Tooltip::with_meta(
"Jump to File",
None,
path_and_file.clone(),
window,
cx,
)
})
.into_any_element()
} else {
Label::new(path_range.path.to_string_lossy().to_string())
@@ -404,7 +413,7 @@ fn render_markdown_code_block(
};
h_flex()
.id(("code-block-header-label", ix))
.id(("code-block-header-button", ix))
.w_full()
.max_w_full()
.px_1()
@@ -412,7 +421,6 @@ fn render_markdown_code_block(
.cursor_pointer()
.rounded_sm()
.hover(|item| item.bg(cx.theme().colors().element_hover.opacity(0.5)))
.tooltip(Tooltip::text("Jump to File"))
.child(
h_flex()
.gap_0p5()
@@ -462,10 +470,87 @@ fn render_markdown_code_block(
.element_background
.blend(cx.theme().colors().editor_foreground.opacity(0.01));
let control_buttons = h_flex()
.visible_on_hover(CODEBLOCK_CONTAINER_GROUP)
.absolute()
.top_0()
.right_0()
.h_full()
.bg(codeblock_header_bg)
.rounded_tr_md()
.px_1()
.gap_1()
.child(
IconButton::new(
("copy-markdown-code", ix),
if codeblock_was_copied {
IconName::Check
} else {
IconName::Copy
},
)
.icon_color(Color::Muted)
.shape(ui::IconButtonShape::Square)
.tooltip(Tooltip::text("Copy Code"))
.on_click({
let active_thread = active_thread.clone();
let parsed_markdown = parsed_markdown.clone();
let code_block_range = metadata.content_range.clone();
move |_event, _window, cx| {
active_thread.update(cx, |this, cx| {
this.copied_code_block_ids.insert((message_id, ix));
let code = parsed_markdown.source()[code_block_range.clone()].to_string();
cx.write_to_clipboard(ClipboardItem::new_string(code));
cx.spawn(async move |this, cx| {
cx.background_executor().timer(Duration::from_secs(2)).await;
cx.update(|cx| {
this.update(cx, |this, cx| {
this.copied_code_block_ids.remove(&(message_id, ix));
cx.notify();
})
})
.ok();
})
.detach();
});
}
}),
)
.when(can_expand, |header| {
header.child(
IconButton::new(
("expand-collapse-code", ix),
if is_expanded {
IconName::ChevronUp
} else {
IconName::ChevronDown
},
)
.icon_color(Color::Muted)
.shape(ui::IconButtonShape::Square)
.tooltip(Tooltip::text(if is_expanded {
"Collapse Code"
} else {
"Expand Code"
}))
.on_click({
let active_thread = active_thread.clone();
move |_event, _window, cx| {
active_thread.update(cx, |this, cx| {
this.toggle_codeblock_expanded(message_id, ix);
cx.notify();
});
}
}),
)
});
let codeblock_header = h_flex()
.py_1()
.pl_1p5()
.pr_1()
.relative()
.p_1()
.gap_1()
.justify_between()
.border_b_1()
@@ -473,79 +558,7 @@ fn render_markdown_code_block(
.bg(codeblock_header_bg)
.rounded_t_md()
.children(label)
.child(
h_flex()
.visible_on_hover(CODEBLOCK_CONTAINER_GROUP)
.gap_1()
.child(
IconButton::new(
("copy-markdown-code", ix),
if codeblock_was_copied {
IconName::Check
} else {
IconName::Copy
},
)
.icon_color(Color::Muted)
.shape(ui::IconButtonShape::Square)
.tooltip(Tooltip::text("Copy Code"))
.on_click({
let active_thread = active_thread.clone();
let parsed_markdown = parsed_markdown.clone();
let code_block_range = metadata.content_range.clone();
move |_event, _window, cx| {
active_thread.update(cx, |this, cx| {
this.copied_code_block_ids.insert((message_id, ix));
let code =
parsed_markdown.source()[code_block_range.clone()].to_string();
cx.write_to_clipboard(ClipboardItem::new_string(code));
cx.spawn(async move |this, cx| {
cx.background_executor().timer(Duration::from_secs(2)).await;
cx.update(|cx| {
this.update(cx, |this, cx| {
this.copied_code_block_ids.remove(&(message_id, ix));
cx.notify();
})
})
.ok();
})
.detach();
});
}
}),
)
.when(can_expand, |header| {
header.child(
IconButton::new(
("expand-collapse-code", ix),
if is_expanded {
IconName::ChevronUp
} else {
IconName::ChevronDown
},
)
.icon_color(Color::Muted)
.shape(ui::IconButtonShape::Square)
.tooltip(Tooltip::text(if is_expanded {
"Collapse Code"
} else {
"Expand Code"
}))
.on_click({
let active_thread = active_thread.clone();
move |_event, _window, cx| {
active_thread.update(cx, |this, cx| {
this.toggle_codeblock_expanded(message_id, ix);
cx.notify();
});
}
}),
)
}),
);
.child(control_buttons);
v_flex()
.group(CODEBLOCK_CONTAINER_GROUP)
@@ -1709,10 +1722,11 @@ impl ActiveThread {
.on_action(cx.listener(Self::confirm_editing_message))
.capture_action(cx.listener(Self::paste))
.min_h_6()
.flex_grow()
.w_full()
.flex_grow()
.gap_2()
.child(EditorElement::new(
.child(state.context_strip.clone())
.child(div().pt(px(-3.)).px_neg_0p5().child(EditorElement::new(
&state.editor,
EditorStyle {
background: colors.editor_background,
@@ -1721,8 +1735,7 @@ impl ActiveThread {
syntax: cx.theme().syntax().clone(),
..Default::default()
},
))
.child(state.context_strip.clone())
)))
}
fn render_message(&self, ix: usize, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
@@ -1850,7 +1863,8 @@ impl ActiveThread {
.child(open_as_markdown),
)
.into_any_element(),
None => feedback_container
None if AssistantSettings::get_global(cx).enable_feedback =>
feedback_container
.child(
div().visible_on_hover("feedback_container").child(
Label::new(
@@ -1893,6 +1907,9 @@ impl ActiveThread {
.child(open_as_markdown),
)
.into_any_element(),
None => feedback_container
.child(h_flex().child(open_as_markdown))
.into_any_element(),
};
let message_is_empty = message.should_display_content();
@@ -1906,16 +1923,6 @@ impl ActiveThread {
v_flex()
.w_full()
.gap_1()
.when(!message_is_empty, |parent| {
parent.child(div().min_h_6().child(self.render_message_content(
message_id,
rendered_message,
has_tool_uses,
workspace.clone(),
window,
cx,
)))
})
.when(!added_context.is_empty(), |parent| {
parent.child(h_flex().flex_wrap().gap_1().children(
added_context.into_iter().map(|added_context| {
@@ -1934,6 +1941,16 @@ impl ActiveThread {
}),
))
})
.when(!message_is_empty, |parent| {
parent.child(div().pt_0p5().min_h_6().child(self.render_message_content(
message_id,
rendered_message,
has_tool_uses,
workspace.clone(),
window,
cx,
)))
})
.into_any_element()
}
});
@@ -1959,6 +1976,7 @@ impl ActiveThread {
h_flex()
.p_2p5()
.gap_1()
.items_end()
.children(message_content)
.when_some(editing_message_state, |this, state| {
let focus_handle = state.editor.focus_handle(cx).clone();
@@ -1972,6 +1990,7 @@ impl ActiveThread {
)
.shape(ui::IconButtonShape::Square)
.icon_color(Color::Error)
.icon_size(IconSize::Small)
.tooltip({
let focus_handle = focus_handle.clone();
move |window, cx| {
@@ -1989,11 +2008,12 @@ impl ActiveThread {
.child(
IconButton::new(
"confirm-edit-message",
IconName::Check,
IconName::Return,
)
.disabled(state.editor.read(cx).is_empty(cx))
.shape(ui::IconButtonShape::Square)
.icon_color(Color::Success)
.icon_color(Color::Muted)
.icon_size(IconSize::Small)
.tooltip({
let focus_handle = focus_handle.clone();
move |window, cx| {
@@ -2013,9 +2033,6 @@ impl ActiveThread {
)
}),
)
.when(editing_message_state.is_none(), |this| {
this.tooltip(Tooltip::text("Click To Edit"))
})
.on_click(cx.listener({
let message_segments = message.segments.clone();
move |this, _, window, cx| {
@@ -2056,6 +2073,16 @@ impl ActiveThread {
let panel_background = cx.theme().colors().panel_background;
let backdrop = div()
.id("backdrop")
.stop_mouse_events_except_scroll()
.absolute()
.inset_0()
.size_full()
.bg(panel_background)
.opacity(0.8)
.on_click(cx.listener(Self::handle_cancel_click));
v_flex()
.w_full()
.map(|parent| {
@@ -2225,15 +2252,7 @@ impl ActiveThread {
})
.when(after_editing_message, |parent| {
// Backdrop to dim out the whole thread below the editing user message
parent.relative().child(
div()
.stop_mouse_events_except_scroll()
.absolute()
.inset_0()
.size_full()
.bg(panel_background)
.opacity(0.8),
)
parent.relative().child(backdrop)
})
.into_any()
}
@@ -2348,6 +2367,7 @@ impl ActiveThread {
move |el, range, metadata, _, cx| {
let can_expand = metadata.line_count
>= MAX_UNCOLLAPSED_LINES_IN_CODE_BLOCK;
if !can_expand {
return el;
}
@@ -2355,6 +2375,7 @@ impl ActiveThread {
let is_expanded = active_thread
.read(cx)
.is_codeblock_expanded(message_id, range.start);
if is_expanded {
return el;
}
@@ -3377,14 +3398,14 @@ impl ActiveThread {
self.expanded_code_blocks
.get(&(message_id, ix))
.copied()
.unwrap_or(false)
.unwrap_or(true)
}
pub fn toggle_codeblock_expanded(&mut self, message_id: MessageId, ix: usize) {
let is_expanded = self
.expanded_code_blocks
.entry((message_id, ix))
.or_insert(false);
.or_insert(true);
*is_expanded = !*is_expanded;
}
}
@@ -3400,6 +3421,7 @@ impl Render for ActiveThread {
v_flex()
.size_full()
.relative()
.bg(cx.theme().colors().panel_background)
.on_mouse_move(cx.listener(|this, _, _, cx| {
this.show_scrollbar = true;
this.hide_scrollbar_later(cx);

View File

@@ -49,7 +49,7 @@ pub use crate::context::{ContextLoadResult, LoadedContext};
pub use crate::inline_assistant::InlineAssistant;
use crate::slash_command_settings::SlashCommandSettings;
pub use crate::thread::{Message, MessageSegment, Thread, ThreadEvent};
pub use crate::thread_store::{TextThreadStore, ThreadStore};
pub use crate::thread_store::{SerializedThread, TextThreadStore, ThreadStore};
pub use agent_diff::{AgentDiffPane, AgentDiffToolbar};
pub use context_store::ContextStore;
pub use ui::preview::{all_agent_previews, get_agent_preview};

View File

@@ -18,8 +18,8 @@ use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageMod
use project::context_server_store::{ContextServerStatus, ContextServerStore};
use settings::{Settings, update_settings_file};
use ui::{
Disclosure, Divider, DividerColor, ElevationIndex, Indicator, Scrollbar, ScrollbarState,
Switch, SwitchColor, Tooltip, prelude::*,
Disclosure, ElevationIndex, Indicator, Scrollbar, ScrollbarState, Switch, SwitchColor, Tooltip,
prelude::*,
};
use util::ResultExt as _;
use zed_actions::ExtensionCategoryFilter;
@@ -142,7 +142,7 @@ impl AgentConfiguration {
.expanded_provider_configurations
.get(&provider.id())
.copied()
.unwrap_or(true);
.unwrap_or(false);
v_flex()
.pt_3()
@@ -201,12 +201,12 @@ impl AgentConfiguration {
.on_click(cx.listener({
let provider_id = provider.id().clone();
move |this, _event, _window, _cx| {
let is_open = this
let is_expanded = this
.expanded_provider_configurations
.entry(provider_id.clone())
.or_insert(true);
.or_insert(false);
*is_open = !*is_open;
*is_expanded = !*is_expanded;
}
})),
),
@@ -214,9 +214,9 @@ impl AgentConfiguration {
)
.when(is_expanded, |parent| match configuration_view {
Some(configuration_view) => parent.child(configuration_view),
None => parent.child(div().child(Label::new(format!(
None => parent.child(Label::new(format!(
"No configuration view for {provider_name}",
)))),
))),
})
}
@@ -230,7 +230,8 @@ impl AgentConfiguration {
.p(DynamicSpacing::Base16.rems(cx))
.pr(DynamicSpacing::Base20.rems(cx))
.gap_4()
.flex_1()
.border_b_1()
.border_color(cx.theme().colors().border)
.child(
v_flex()
.gap_0p5()
@@ -331,7 +332,8 @@ impl AgentConfiguration {
.p(DynamicSpacing::Base16.rems(cx))
.pr(DynamicSpacing::Base20.rems(cx))
.gap_2p5()
.flex_1()
.border_b_1()
.border_color(cx.theme().colors().border)
.child(Headline::new("General Settings"))
.child(self.render_command_permission(cx))
.child(self.render_single_file_review(cx))
@@ -344,18 +346,17 @@ impl AgentConfiguration {
) -> impl IntoElement {
let context_server_ids = self.context_server_store.read(cx).all_server_ids().clone();
const SUBHEADING: &str = "Connect to context servers via the Model Context Protocol either via Zed extensions or directly.";
v_flex()
.p(DynamicSpacing::Base16.rems(cx))
.pr(DynamicSpacing::Base20.rems(cx))
.gap_2()
.flex_1()
.border_b_1()
.border_color(cx.theme().colors().border)
.child(
v_flex()
.gap_0p5()
.child(Headline::new("Model Context Protocol (MCP) Servers"))
.child(Label::new(SUBHEADING).color(Color::Muted)),
.child(Label::new("Connect to context servers via the Model Context Protocol either via Zed extensions or directly.").color(Color::Muted)),
)
.children(
context_server_ids.into_iter().map(|context_server_id| {
@@ -630,9 +631,7 @@ impl Render for AgentConfiguration {
.size_full()
.overflow_y_scroll()
.child(self.render_general_settings_section(cx))
.child(Divider::horizontal().color(DividerColor::Border))
.child(self.render_context_servers_section(window, cx))
.child(Divider::horizontal().color(DividerColor::Border))
.child(self.render_provider_configuration_section(cx)),
)
.child(

View File

@@ -30,7 +30,6 @@ pub(crate) struct ConfigureContextServerModal {
context_server_store: Entity<ContextServerStore>,
}
#[allow(clippy::large_enum_variant)]
enum Configuration {
NotAvailable,
Required(ConfigurationRequiredState),

View File

@@ -2135,6 +2135,7 @@ impl AgentPanel {
v_flex()
.size_full()
.bg(cx.theme().colors().panel_background)
.when(recent_history.is_empty(), |this| {
let configuration_error_ref = &configuration_error;
this.child(

View File

@@ -942,8 +942,8 @@ impl MentionLink {
format!("[@{}]({}:{})", title, Self::THREAD, id)
}
ThreadContextEntry::Context { path, title } => {
let filename = path.file_name().unwrap_or_default();
let escaped_filename = urlencoding::encode(&filename.to_string_lossy()).to_string();
let filename = path.file_name().unwrap_or_default().to_string_lossy();
let escaped_filename = urlencoding::encode(&filename);
format!(
"[@{}]({}:{}{})",
title,

View File

@@ -84,6 +84,12 @@ impl ContextStrip {
}
}
/// Whether or not the context strip has items to display
pub fn has_context_items(&self, cx: &App) -> bool {
self.context_store.read(cx).context().next().is_some()
|| self.suggested_context(cx).is_some()
}
fn added_contexts(&self, cx: &App) -> Vec<AddedContext> {
if let Some(workspace) = self.workspace.upgrade() {
let project = workspace.read(cx).project().read(cx);
@@ -104,14 +110,14 @@ impl ContextStrip {
}
}
fn suggested_context(&self, cx: &Context<Self>) -> Option<SuggestedContext> {
fn suggested_context(&self, cx: &App) -> Option<SuggestedContext> {
match self.suggest_context_kind {
SuggestContextKind::File => self.suggested_file(cx),
SuggestContextKind::Thread => self.suggested_thread(cx),
}
}
fn suggested_file(&self, cx: &Context<Self>) -> Option<SuggestedContext> {
fn suggested_file(&self, cx: &App) -> Option<SuggestedContext> {
let workspace = self.workspace.upgrade()?;
let active_item = workspace.read(cx).active_item(cx)?;
@@ -138,7 +144,7 @@ impl ContextStrip {
})
}
fn suggested_thread(&self, cx: &Context<Self>) -> Option<SuggestedContext> {
fn suggested_thread(&self, cx: &App) -> Option<SuggestedContext> {
if !self.context_picker.read(cx).allow_threads() {
return None;
}

View File

@@ -451,7 +451,7 @@ impl<T: 'static> PromptEditor<T> {
editor.move_to_end(&Default::default(), window, cx)
});
}
} else {
} else if self.context_strip.read(cx).has_context_items(cx) {
self.context_strip.focus_handle(cx).focus(window);
}
}

View File

@@ -401,7 +401,7 @@ impl MessageEditor {
fn move_up(&mut self, _: &MoveUp, window: &mut Window, cx: &mut Context<Self>) {
if self.context_picker_menu_handle.is_deployed() {
cx.propagate();
} else {
} else if self.context_strip.read(cx).has_context_items(cx) {
self.context_strip.focus_handle(cx).focus(window);
}
}

View File

@@ -23,7 +23,7 @@ use std::sync::Arc;
use telemetry_events::{AssistantEventData, AssistantKind, AssistantPhase};
use terminal_view::TerminalView;
use ui::prelude::*;
use util::ResultExt;
use util::{ResultExt, get_system_shell};
use workspace::{Toast, Workspace, notifications::NotificationId};
pub fn init(
@@ -191,7 +191,7 @@ impl TerminalInlineAssistant {
};
self.prompt_history.retain(|prompt| *prompt != user_prompt);
self.prompt_history.push_back(user_prompt.clone());
self.prompt_history.push_back(user_prompt);
if self.prompt_history.len() > PROMPT_HISTORY_MAX_LEN {
self.prompt_history.pop_front();
}
@@ -230,7 +230,7 @@ impl TerminalInlineAssistant {
) -> Result<Task<LanguageModelRequest>> {
let assist = self.assists.get(&assist_id).context("invalid assist")?;
let shell = std::env::var("SHELL").ok();
let shell = get_system_shell();
let (latest_output, working_directory) = assist
.terminal
.update(cx, |terminal, cx| {
@@ -251,7 +251,7 @@ impl TerminalInlineAssistant {
.context("invalid assist")?
.read(cx)
.prompt(cx),
shell.as_deref(),
Some(&shell),
working_directory.as_deref(),
&latest_output,
)?;

View File

@@ -458,7 +458,7 @@ impl Thread {
tools: Entity<ToolWorkingSet>,
prompt_builder: Arc<PromptBuilder>,
project_context: SharedProjectContext,
window: &mut Window,
window: Option<&mut Window>, // None in headless mode
cx: &mut Context<Self>,
) -> Self {
let next_message_id = MessageId(
@@ -2583,7 +2583,7 @@ impl Thread {
.read(cx)
.current_user()
.map(|user| user.github_login.clone());
let client = self.project.read(cx).client().clone();
let client = self.project.read(cx).client();
let serialize_task = self.serialize(cx);
cx.background_executor()

View File

@@ -260,10 +260,7 @@ impl ThreadHistory {
}
});
self.search_state = SearchState::Searching {
query: query.clone(),
_task: task,
};
self.search_state = SearchState::Searching { query, _task: task };
cx.notify();
}

View File

@@ -386,6 +386,25 @@ impl ThreadStore {
})
}
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(
&self,
id: &ThreadId,
@@ -411,7 +430,7 @@ impl ThreadStore {
this.tools.clone(),
this.prompt_builder.clone(),
this.project_context.clone(),
window,
Some(window),
cx,
)
})

View File

@@ -54,15 +54,19 @@ impl ToolUseState {
/// 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: &mut Window,
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 {
@@ -107,12 +111,17 @@ impl ToolUseState {
},
);
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);
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);
}
}
}
}
@@ -416,16 +425,17 @@ impl ToolUseState {
let content = match tool_result {
ToolResultContent::Text(text) => {
let truncated = truncate_lines_to_byte_limit(&text, tool_output_limit);
LanguageModelToolResultContent::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
)
.into(),
)
};
LanguageModelToolResultContent::Text(text.into())
}
ToolResultContent::Image(language_model_image) => {
if language_model_image.estimate_tokens() < tool_output_limit {

View File

@@ -1,8 +1,8 @@
use std::sync::OnceLock;
use collections::HashMap;
use component::ComponentId;
use gpui::{App, Entity, WeakEntity};
use linkme::distributed_slice;
use std::sync::OnceLock;
use ui::{AnyElement, Component, ComponentScope, Window};
use workspace::Workspace;
@@ -12,9 +12,15 @@ use crate::ActiveThread;
pub type PreviewFn =
fn(WeakEntity<Workspace>, Entity<ActiveThread>, &mut Window, &mut App) -> Option<AnyElement>;
/// Distributed slice for preview registration functions
#[distributed_slice]
pub static __ALL_AGENT_PREVIEWS: [fn() -> (ComponentId, PreviewFn)] = [..];
pub struct AgentPreviewFn(fn() -> (ComponentId, PreviewFn));
impl AgentPreviewFn {
pub const fn new(f: fn() -> (ComponentId, PreviewFn)) -> Self {
Self(f)
}
}
inventory::collect!(AgentPreviewFn);
/// Trait that must be implemented by components that provide agent previews.
pub trait AgentPreview: Component + Sized {
@@ -36,16 +42,14 @@ pub trait AgentPreview: Component + Sized {
#[macro_export]
macro_rules! register_agent_preview {
($type:ty) => {
#[linkme::distributed_slice($crate::ui::preview::__ALL_AGENT_PREVIEWS)]
static __REGISTER_AGENT_PREVIEW: fn() -> (
component::ComponentId,
$crate::ui::preview::PreviewFn,
) = || {
(
<$type as component::Component>::id(),
<$type as $crate::ui::preview::AgentPreview>::agent_preview,
)
};
inventory::submit! {
$crate::ui::preview::AgentPreviewFn::new(|| {
(
<$type as component::Component>::id(),
<$type as $crate::ui::preview::AgentPreview>::agent_preview,
)
})
}
};
}
@@ -56,8 +60,8 @@ static AGENT_PREVIEW_REGISTRY: OnceLock<HashMap<ComponentId, PreviewFn>> = OnceL
fn get_or_init_registry() -> &'static HashMap<ComponentId, PreviewFn> {
AGENT_PREVIEW_REGISTRY.get_or_init(|| {
let mut map = HashMap::default();
for register_fn in __ALL_AGENT_PREVIEWS.iter() {
let (id, preview_fn) = register_fn();
for register_fn in inventory::iter::<AgentPreviewFn>() {
let (id, preview_fn) = (register_fn.0)();
map.insert(id, preview_fn);
}
map

View File

@@ -163,8 +163,10 @@ impl AskPassSession {
#[cfg(unix)]
fn get_shell_safe_zed_path() -> anyhow::Result<String> {
let zed_path = std::env::current_exe()
.context("Failed to figure out current executable path for use in askpass")?
.context("Failed to determine current executable path for use in askpass")?
.to_string_lossy()
// see https://github.com/rust-lang/rust/issues/69343
.trim_end_matches(" (deleted)")
.to_string();
// NOTE: this was previously enabled, however, it caused errors when it shouldn't have

View File

@@ -3044,7 +3044,7 @@ fn invoked_slash_command_fold_placeholder(
.gap_2()
.bg(cx.theme().colors().surface_background)
.rounded_sm()
.child(Label::new(format!("/{}", command.name.clone())))
.child(Label::new(format!("/{}", command.name)))
.map(|parent| match &command.status {
InvokedSlashCommandStatus::Running(_) => {
parent.child(Icon::new(IconName::ArrowCircle).with_animation(

View File

@@ -94,6 +94,7 @@ pub struct AssistantSettings {
pub single_file_review: bool,
pub model_parameters: Vec<LanguageModelParameters>,
pub preferred_completion_mode: CompletionMode,
pub enable_feedback: bool,
}
impl AssistantSettings {
@@ -261,6 +262,7 @@ impl AssistantSettingsContent {
single_file_review: None,
model_parameters: Vec::new(),
preferred_completion_mode: None,
enable_feedback: None,
},
VersionedAssistantSettingsContent::V2(ref settings) => settings.clone(),
},
@@ -291,6 +293,7 @@ impl AssistantSettingsContent {
single_file_review: None,
model_parameters: Vec::new(),
preferred_completion_mode: None,
enable_feedback: None,
},
None => AssistantSettingsContentV2::default(),
}
@@ -573,6 +576,7 @@ impl Default for VersionedAssistantSettingsContent {
single_file_review: None,
model_parameters: Vec::new(),
preferred_completion_mode: None,
enable_feedback: None,
})
}
}
@@ -647,6 +651,10 @@ pub struct AssistantSettingsContentV2 {
///
/// Default: normal
preferred_completion_mode: Option<CompletionMode>,
/// Whether to show thumb buttons for feedback in the agent panel.
///
/// Default: true
enable_feedback: Option<bool>,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Default)]
@@ -684,7 +692,7 @@ impl JsonSchema for LanguageModelProviderSetting {
schemars::schema::SchemaObject {
enum_values: Some(vec![
"anthropic".into(),
"bedrock".into(),
"amazon-bedrock".into(),
"google".into(),
"lmstudio".into(),
"ollama".into(),
@@ -853,6 +861,7 @@ impl Settings for AssistantSettings {
&mut settings.preferred_completion_mode,
value.preferred_completion_mode,
);
merge(&mut settings.enable_feedback, value.enable_feedback);
settings
.model_parameters
@@ -989,6 +998,7 @@ mod tests {
notify_when_agent_waiting: None,
stream_edits: None,
single_file_review: None,
enable_feedback: None,
model_parameters: Vec::new(),
preferred_completion_mode: None,
},

View File

@@ -49,6 +49,37 @@ impl ActionLog {
is_created: bool,
cx: &mut Context<Self>,
) -> &mut TrackedBuffer {
let status = if is_created {
if let Some(tracked) = self.tracked_buffers.remove(&buffer) {
match tracked.status {
TrackedBufferStatus::Created {
existing_file_content,
} => TrackedBufferStatus::Created {
existing_file_content,
},
TrackedBufferStatus::Modified | TrackedBufferStatus::Deleted => {
TrackedBufferStatus::Created {
existing_file_content: Some(tracked.diff_base),
}
}
}
} else if buffer
.read(cx)
.file()
.map_or(false, |file| file.disk_state().exists())
{
TrackedBufferStatus::Created {
existing_file_content: Some(buffer.read(cx).as_rope().clone()),
}
} else {
TrackedBufferStatus::Created {
existing_file_content: None,
}
}
} else {
TrackedBufferStatus::Modified
};
let tracked_buffer = self
.tracked_buffers
.entry(buffer.clone())
@@ -60,36 +91,21 @@ impl ActionLog {
let text_snapshot = buffer.read(cx).text_snapshot();
let diff = cx.new(|cx| BufferDiff::new(&text_snapshot, cx));
let (diff_update_tx, diff_update_rx) = mpsc::unbounded();
let base_text;
let status;
let diff_base;
let unreviewed_changes;
if is_created {
let existing_file_content = if buffer
.read(cx)
.file()
.map_or(false, |file| file.disk_state().exists())
{
Some(text_snapshot.as_rope().clone())
} else {
None
};
base_text = Rope::default();
status = TrackedBufferStatus::Created {
existing_file_content,
};
diff_base = Rope::default();
unreviewed_changes = Patch::new(vec![Edit {
old: 0..1,
new: 0..text_snapshot.max_point().row + 1,
}])
} else {
base_text = buffer.read(cx).as_rope().clone();
status = TrackedBufferStatus::Modified;
diff_base = buffer.read(cx).as_rope().clone();
unreviewed_changes = Patch::default();
}
TrackedBuffer {
buffer: buffer.clone(),
base_text,
diff_base,
unreviewed_changes,
snapshot: text_snapshot.clone(),
status,
@@ -184,7 +200,7 @@ impl ActionLog {
.context("buffer not tracked")?;
let rebase = cx.background_spawn({
let mut base_text = tracked_buffer.base_text.clone();
let mut base_text = tracked_buffer.diff_base.clone();
let old_snapshot = tracked_buffer.snapshot.clone();
let new_snapshot = buffer_snapshot.clone();
let unreviewed_changes = tracked_buffer.unreviewed_changes.clone();
@@ -210,7 +226,7 @@ impl ActionLog {
))
})??;
let (new_base_text, new_base_text_rope) = rebase.await;
let (new_base_text, new_diff_base) = rebase.await;
let diff_snapshot = BufferDiff::update_diff(
diff.clone(),
buffer_snapshot.clone(),
@@ -229,24 +245,23 @@ impl ActionLog {
.background_spawn({
let diff_snapshot = diff_snapshot.clone();
let buffer_snapshot = buffer_snapshot.clone();
let new_base_text_rope = new_base_text_rope.clone();
let new_diff_base = new_diff_base.clone();
async move {
let mut unreviewed_changes = Patch::default();
for hunk in diff_snapshot.hunks_intersecting_range(
Anchor::MIN..Anchor::MAX,
&buffer_snapshot,
) {
let old_range = new_base_text_rope
let old_range = new_diff_base
.offset_to_point(hunk.diff_base_byte_range.start)
..new_base_text_rope
.offset_to_point(hunk.diff_base_byte_range.end);
..new_diff_base.offset_to_point(hunk.diff_base_byte_range.end);
let new_range = hunk.range.start..hunk.range.end;
unreviewed_changes.push(point_to_row_edit(
Edit {
old: old_range,
new: new_range,
},
&new_base_text_rope,
&new_diff_base,
&buffer_snapshot.as_rope(),
));
}
@@ -264,7 +279,7 @@ impl ActionLog {
.tracked_buffers
.get_mut(&buffer)
.context("buffer not tracked")?;
tracked_buffer.base_text = new_base_text_rope;
tracked_buffer.diff_base = new_diff_base;
tracked_buffer.snapshot = buffer_snapshot;
tracked_buffer.unreviewed_changes = unreviewed_changes;
cx.notify();
@@ -283,7 +298,6 @@ impl ActionLog {
/// Mark a buffer as edited, so we can refresh it in the context
pub fn buffer_created(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
self.edited_since_project_diagnostics_check = true;
self.tracked_buffers.remove(&buffer);
self.track_buffer_internal(buffer.clone(), true, cx);
}
@@ -346,11 +360,11 @@ impl ActionLog {
true
} else {
let old_range = tracked_buffer
.base_text
.diff_base
.point_to_offset(Point::new(edit.old.start, 0))
..tracked_buffer.base_text.point_to_offset(cmp::min(
..tracked_buffer.diff_base.point_to_offset(cmp::min(
Point::new(edit.old.end, 0),
tracked_buffer.base_text.max_point(),
tracked_buffer.diff_base.max_point(),
));
let new_range = tracked_buffer
.snapshot
@@ -359,7 +373,7 @@ impl ActionLog {
Point::new(edit.new.end, 0),
tracked_buffer.snapshot.max_point(),
));
tracked_buffer.base_text.replace(
tracked_buffer.diff_base.replace(
old_range,
&tracked_buffer
.snapshot
@@ -417,7 +431,7 @@ impl ActionLog {
}
TrackedBufferStatus::Deleted => {
buffer.update(cx, |buffer, cx| {
buffer.set_text(tracked_buffer.base_text.to_string(), cx)
buffer.set_text(tracked_buffer.diff_base.to_string(), cx)
});
let save = self
.project
@@ -464,14 +478,14 @@ impl ActionLog {
if revert {
let old_range = tracked_buffer
.base_text
.diff_base
.point_to_offset(Point::new(edit.old.start, 0))
..tracked_buffer.base_text.point_to_offset(cmp::min(
..tracked_buffer.diff_base.point_to_offset(cmp::min(
Point::new(edit.old.end, 0),
tracked_buffer.base_text.max_point(),
tracked_buffer.diff_base.max_point(),
));
let old_text = tracked_buffer
.base_text
.diff_base
.chunks_in_range(old_range)
.collect::<String>();
edits_to_revert.push((new_range, old_text));
@@ -492,7 +506,7 @@ impl ActionLog {
TrackedBufferStatus::Deleted => false,
_ => {
tracked_buffer.unreviewed_changes.clear();
tracked_buffer.base_text = tracked_buffer.snapshot.as_rope().clone();
tracked_buffer.diff_base = tracked_buffer.snapshot.as_rope().clone();
tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx);
true
}
@@ -655,7 +669,7 @@ enum TrackedBufferStatus {
struct TrackedBuffer {
buffer: Entity<Buffer>,
base_text: Rope,
diff_base: Rope,
unreviewed_changes: Patch<u32>,
status: TrackedBufferStatus,
version: clock::Global,
@@ -1094,6 +1108,86 @@ mod tests {
);
}
#[gpui::test(iterations = 10)]
async fn test_overwriting_previously_edited_files(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
path!("/dir"),
json!({
"file1": "Lorem ipsum dolor"
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let file_path = project
.read_with(cx, |project, cx| project.find_project_path("dir/file1", cx))
.unwrap();
let buffer = project
.update(cx, |project, cx| project.open_buffer(file_path, cx))
.await
.unwrap();
cx.update(|cx| {
action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
buffer.update(cx, |buffer, cx| buffer.append(" sit amet consecteur", cx));
action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
});
project
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
.await
.unwrap();
cx.run_until_parked();
assert_eq!(
unreviewed_hunks(&action_log, cx),
vec![(
buffer.clone(),
vec![HunkStatus {
range: Point::new(0, 0)..Point::new(0, 37),
diff_status: DiffHunkStatusKind::Modified,
old_text: "Lorem ipsum dolor".into(),
}],
)]
);
cx.update(|cx| {
action_log.update(cx, |log, cx| log.buffer_created(buffer.clone(), cx));
buffer.update(cx, |buffer, cx| buffer.set_text("rewritten", cx));
action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
});
project
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
.await
.unwrap();
cx.run_until_parked();
assert_eq!(
unreviewed_hunks(&action_log, cx),
vec![(
buffer.clone(),
vec![HunkStatus {
range: Point::new(0, 0)..Point::new(0, 9),
diff_status: DiffHunkStatusKind::Added,
old_text: "".into(),
}],
)]
);
action_log
.update(cx, |log, cx| {
log.reject_edits_in_ranges(buffer.clone(), vec![2..5], cx)
})
.await
.unwrap();
cx.run_until_parked();
assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
assert_eq!(
buffer.read_with(cx, |buffer, _cx| buffer.text()),
"Lorem ipsum dolor"
);
}
#[gpui::test(iterations = 10)]
async fn test_deleting_files(cx: &mut TestAppContext) {
init_test(cx);
@@ -1601,7 +1695,7 @@ mod tests {
cx.run_until_parked();
action_log.update(cx, |log, cx| {
let tracked_buffer = log.tracked_buffers.get(&buffer).unwrap();
let mut old_text = tracked_buffer.base_text.clone();
let mut old_text = tracked_buffer.diff_base.clone();
let new_text = buffer.read(cx).as_rope();
for edit in tracked_buffer.unreviewed_changes.edits() {
let old_start = old_text.point_to_offset(Point::new(edit.new.start, 0));

View File

@@ -25,6 +25,7 @@ collections.workspace = true
component.workspace = true
derive_more.workspace = true
editor.workspace = true
environment.workspace = true
feature_flags.workspace = true
futures.workspace = true
gpui.workspace = true
@@ -35,7 +36,6 @@ indoc.workspace = true
itertools.workspace = true
language.workspace = true
language_model.workspace = true
linkme.workspace = true
log.workspace = true
markdown.workspace = true
open.workspace = true

View File

@@ -42,7 +42,7 @@ use crate::list_directory_tool::ListDirectoryTool;
use crate::now_tool::NowTool;
use crate::thinking_tool::ThinkingTool;
pub use edit_file_tool::EditFileToolInput;
pub use edit_file_tool::{EditFileMode, EditFileToolInput};
pub use find_path_tool::FindPathToolInput;
pub use open_tool::OpenTool;
pub use read_file_tool::{ReadFileTool, ReadFileToolInput};

View File

@@ -1,5 +1,9 @@
use super::*;
use crate::{ReadFileToolInput, edit_file_tool::EditFileToolInput, grep_tool::GrepToolInput};
use crate::{
ReadFileToolInput,
edit_file_tool::{EditFileMode, EditFileToolInput},
grep_tool::GrepToolInput,
};
use Role::*;
use anyhow::anyhow;
use assistant_tool::ToolRegistry;
@@ -11,7 +15,7 @@ use gpui::{AppContext, TestAppContext};
use indoc::{formatdoc, indoc};
use language_model::{
LanguageModelRegistry, LanguageModelRequestTool, LanguageModelToolResult,
LanguageModelToolResultContent, LanguageModelToolUse, LanguageModelToolUseId,
LanguageModelToolResultContent, LanguageModelToolUse, LanguageModelToolUseId, SelectedModel,
};
use project::Project;
use rand::prelude::*;
@@ -21,6 +25,7 @@ use std::{
cmp::Reverse,
fmt::{self, Display},
io::Write as _,
str::FromStr,
sync::mpsc,
};
use util::path;
@@ -71,7 +76,7 @@ fn eval_extract_handle_command_output() {
EditFileToolInput {
display_description: edit_description.into(),
path: input_file_path.into(),
create_or_overwrite: false,
mode: EditFileMode::Edit,
},
)],
),
@@ -127,7 +132,7 @@ fn eval_delete_run_git_blame() {
EditFileToolInput {
display_description: edit_description.into(),
path: input_file_path.into(),
create_or_overwrite: false,
mode: EditFileMode::Edit,
},
)],
),
@@ -182,7 +187,7 @@ fn eval_translate_doc_comments() {
EditFileToolInput {
display_description: edit_description.into(),
path: input_file_path.into(),
create_or_overwrite: false,
mode: EditFileMode::Edit,
},
)],
),
@@ -297,7 +302,7 @@ fn eval_use_wasi_sdk_in_compile_parser_to_wasm() {
EditFileToolInput {
display_description: edit_description.into(),
path: input_file_path.into(),
create_or_overwrite: false,
mode: EditFileMode::Edit,
},
)],
),
@@ -372,7 +377,7 @@ fn eval_disable_cursor_blinking() {
EditFileToolInput {
display_description: edit_description.into(),
path: input_file_path.into(),
create_or_overwrite: false,
mode: EditFileMode::Edit,
},
)],
),
@@ -566,7 +571,7 @@ fn eval_from_pixels_constructor() {
EditFileToolInput {
display_description: edit_description.into(),
path: input_file_path.into(),
create_or_overwrite: false,
mode: EditFileMode::Edit,
},
)],
),
@@ -643,7 +648,7 @@ fn eval_zode() {
EditFileToolInput {
display_description: edit_description.into(),
path: input_file_path.into(),
create_or_overwrite: true,
mode: EditFileMode::Create,
},
),
],
@@ -888,7 +893,7 @@ fn eval_add_overwrite_test() {
EditFileToolInput {
display_description: edit_description.into(),
path: input_file_path.into(),
create_or_overwrite: false,
mode: EditFileMode::Edit,
},
),
],
@@ -1212,7 +1217,7 @@ fn report_progress(evaluated_count: usize, failed_count: usize, iterations: usiz
passed_count as f64 / evaluated_count as f64
};
print!(
"\r\x1b[KEvaluated {}/{} ({:.2}%)",
"\r\x1b[KEvaluated {}/{} ({:.2}% passed)",
evaluated_count,
iterations,
passed_ratio * 100.0
@@ -1251,13 +1256,21 @@ impl EditAgentTest {
fs.insert_tree("/root", json!({})).await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let agent_model = SelectedModel::from_str(
&std::env::var("ZED_AGENT_MODEL")
.unwrap_or("anthropic/claude-3-7-sonnet-latest".into()),
)
.unwrap();
let judge_model = SelectedModel::from_str(
&std::env::var("ZED_JUDGE_MODEL")
.unwrap_or("anthropic/claude-3-7-sonnet-latest".into()),
)
.unwrap();
let (agent_model, judge_model) = cx
.update(|cx| {
cx.spawn(async move |cx| {
let agent_model =
Self::load_model("anthropic", "claude-3-7-sonnet-latest", cx).await;
let judge_model =
Self::load_model("anthropic", "claude-3-7-sonnet-latest", cx).await;
let agent_model = Self::load_model(&agent_model, cx).await;
let judge_model = Self::load_model(&judge_model, cx).await;
(agent_model.unwrap(), judge_model.unwrap())
})
})
@@ -1272,15 +1285,17 @@ impl EditAgentTest {
}
async fn load_model(
provider: &str,
id: &str,
selected_model: &SelectedModel,
cx: &mut AsyncApp,
) -> Result<Arc<dyn LanguageModel>> {
let (provider, model) = cx.update(|cx| {
let models = LanguageModelRegistry::read_global(cx);
let model = models
.available_models(cx)
.find(|model| model.provider_id().0 == provider && model.id().0 == id)
.find(|model| {
model.provider_id() == selected_model.provider
&& model.id() == selected_model.model
})
.unwrap();
let provider = models.provider(&model.provider_id()).unwrap();
(provider, model)

View File

@@ -1249,7 +1249,7 @@ pub struct ActiveDiagnosticGroup {
}
#[derive(Debug, PartialEq, Eq)]
#[allow(clippy::large_enum_variant)]
pub(crate) enum ActiveDiagnostic {
None,
All,

View File

@@ -76,12 +76,22 @@ pub struct EditFileToolInput {
/// </example>
pub path: PathBuf,
/// If true, this tool will recreate the file from scratch.
/// If false, this tool will produce granular edits to an existing file.
/// The mode of operation on the file. Possible values:
/// - 'edit': Make granular edits to an existing file.
/// - 'create': Create a new file if it doesn't exist.
/// - 'overwrite': Replace the entire contents of an existing file.
///
/// When a file already exists or you just created it, always prefer editing
/// When a file already exists or you just created it, prefer editing
/// it as opposed to recreating it from scratch.
pub create_or_overwrite: bool,
pub mode: EditFileMode,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum EditFileMode {
Edit,
Create,
Overwrite,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
@@ -195,7 +205,11 @@ impl Tool for EditFileTool {
.as_ref()
.map_or(false, |file| file.disk_state().exists())
})?;
if !input.create_or_overwrite && !exists {
let create_or_overwrite = match input.mode {
EditFileMode::Create | EditFileMode::Overwrite => true,
_ => false,
};
if !create_or_overwrite && !exists {
return Err(anyhow!("{} not found", input.path.display()));
}
@@ -207,7 +221,7 @@ impl Tool for EditFileTool {
})
.await;
let (output, mut events) = if input.create_or_overwrite {
let (output, mut events) = if create_or_overwrite {
edit_agent.overwrite(
buffer.clone(),
input.display_description.clone(),
@@ -876,7 +890,7 @@ mod tests {
let input = serde_json::to_value(EditFileToolInput {
display_description: "Some edit".into(),
path: "root/nonexistent_file.txt".into(),
create_or_overwrite: false,
mode: EditFileMode::Edit,
})
.unwrap();
Arc::new(EditFileTool)

View File

@@ -148,11 +148,14 @@ impl Tool for TerminalTool {
Some(dir) => project.update(cx, |project, cx| {
project.directory_environment(dir.as_path().into(), cx)
}),
None => Task::ready(None).shared(),
None => cx
.background_executor()
.spawn(async move { environment::in_home_dir().await })
.shared(),
};
let env = cx.spawn(async move |_| {
let mut env = env.await.unwrap_or_default();
let mut env = env.await;
if cfg!(unix) {
env.insert("PAGER".into(), "cat".into());
}

View File

@@ -19,6 +19,7 @@ test-support = ["clock/test-support", "collections/test-support", "gpui/test-sup
anyhow.workspace = true
async-recursion = "0.3"
async-tungstenite = { workspace = true, features = ["tokio", "tokio-rustls-manual-roots"] }
base64.workspace = true
chrono = { workspace = true, features = ["serde"] }
clock.workspace = true
collections.workspace = true
@@ -29,6 +30,7 @@ gpui.workspace = true
gpui_tokio.workspace = true
http_client.workspace = true
http_client_tls.workspace = true
httparse = "1.10"
log.workspace = true
paths.workspace = true
parking_lot.workspace = true
@@ -47,6 +49,7 @@ text.workspace = true
thiserror.workspace = true
time.workspace = true
tiny_http = "0.8"
tokio-native-tls = "0.3"
tokio-socks = { version = "0.5.2", default-features = false, features = ["futures-io"] }
url.workspace = true
util.workspace = true

View File

@@ -1,7 +1,7 @@
#[cfg(any(test, feature = "test-support"))]
pub mod test;
mod socks;
mod proxy;
pub mod telemetry;
pub mod user;
pub mod zed_urls;
@@ -24,13 +24,13 @@ use gpui::{App, AsyncApp, Entity, Global, Task, WeakEntity, actions};
use http_client::{AsyncBody, HttpClient, HttpClientWithUrl};
use parking_lot::RwLock;
use postage::watch;
use proxy::connect_proxy_stream;
use rand::prelude::*;
use release_channel::{AppVersion, ReleaseChannel};
use rpc::proto::{AnyTypedEnvelope, EnvelopedMessage, PeerId, RequestMessage};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
use socks::connect_socks_proxy_stream;
use std::pin::Pin;
use std::{
any::TypeId,
@@ -1156,7 +1156,7 @@ impl Client {
let handle = cx.update(|cx| gpui_tokio::Tokio::handle(cx)).ok().unwrap();
let _guard = handle.enter();
match proxy {
Some(proxy) => connect_socks_proxy_stream(&proxy, rpc_host).await?,
Some(proxy) => connect_proxy_stream(&proxy, rpc_host).await?,
None => Box::new(TcpStream::connect(rpc_host).await?),
}
};

View File

@@ -0,0 +1,66 @@
//! client proxy
mod http_proxy;
mod socks_proxy;
use anyhow::{Context, Result, anyhow};
use http_client::Url;
use http_proxy::{HttpProxyType, connect_http_proxy_stream, parse_http_proxy};
use socks_proxy::{SocksVersion, connect_socks_proxy_stream, parse_socks_proxy};
pub(crate) async fn connect_proxy_stream(
proxy: &Url,
rpc_host: (&str, u16),
) -> Result<Box<dyn AsyncReadWrite>> {
let Some(((proxy_domain, proxy_port), proxy_type)) = parse_proxy_type(proxy) else {
// If parsing the proxy URL fails, we must avoid falling back to an insecure connection.
// SOCKS proxies are often used in contexts where security and privacy are critical,
// so any fallback could expose users to significant risks.
return Err(anyhow!("Parsing proxy url failed"));
};
// Connect to proxy and wrap protocol later
let stream = tokio::net::TcpStream::connect((proxy_domain.as_str(), proxy_port))
.await
.context("Failed to connect to proxy")?;
let proxy_stream = match proxy_type {
ProxyType::SocksProxy(proxy) => connect_socks_proxy_stream(stream, proxy, rpc_host).await?,
ProxyType::HttpProxy(proxy) => {
connect_http_proxy_stream(stream, proxy, rpc_host, &proxy_domain).await?
}
};
Ok(proxy_stream)
}
enum ProxyType<'t> {
SocksProxy(SocksVersion<'t>),
HttpProxy(HttpProxyType<'t>),
}
fn parse_proxy_type<'t>(proxy: &'t Url) -> Option<((String, u16), ProxyType<'t>)> {
let scheme = proxy.scheme();
let host = proxy.host()?.to_string();
let port = proxy.port_or_known_default()?;
let proxy_type = match scheme {
scheme if scheme.starts_with("socks") => {
Some(ProxyType::SocksProxy(parse_socks_proxy(scheme, proxy)))
}
scheme if scheme.starts_with("http") => {
Some(ProxyType::HttpProxy(parse_http_proxy(scheme, proxy)))
}
_ => None,
}?;
Some(((host, port), proxy_type))
}
pub(crate) trait AsyncReadWrite:
tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static
{
}
impl<T: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static> AsyncReadWrite
for T
{
}

View File

@@ -0,0 +1,171 @@
use anyhow::{Context, Result};
use base64::Engine;
use httparse::{EMPTY_HEADER, Response};
use tokio::{
io::{AsyncBufReadExt, AsyncWriteExt, BufStream},
net::TcpStream,
};
use tokio_native_tls::{TlsConnector, native_tls};
use url::Url;
use super::AsyncReadWrite;
pub(super) enum HttpProxyType<'t> {
HTTP(Option<HttpProxyAuthorization<'t>>),
HTTPS(Option<HttpProxyAuthorization<'t>>),
}
pub(super) struct HttpProxyAuthorization<'t> {
username: &'t str,
password: &'t str,
}
pub(super) fn parse_http_proxy<'t>(scheme: &str, proxy: &'t Url) -> HttpProxyType<'t> {
let auth = proxy.password().map(|password| HttpProxyAuthorization {
username: proxy.username(),
password,
});
if scheme.starts_with("https") {
HttpProxyType::HTTPS(auth)
} else {
HttpProxyType::HTTP(auth)
}
}
pub(crate) async fn connect_http_proxy_stream(
stream: TcpStream,
http_proxy: HttpProxyType<'_>,
rpc_host: (&str, u16),
proxy_domain: &str,
) -> Result<Box<dyn AsyncReadWrite>> {
match http_proxy {
HttpProxyType::HTTP(auth) => http_connect(stream, rpc_host, auth).await,
HttpProxyType::HTTPS(auth) => https_connect(stream, rpc_host, auth, proxy_domain).await,
}
.context("error connecting to http/https proxy")
}
async fn http_connect<T>(
stream: T,
target: (&str, u16),
auth: Option<HttpProxyAuthorization<'_>>,
) -> Result<Box<dyn AsyncReadWrite>>
where
T: AsyncReadWrite,
{
let mut stream = BufStream::new(stream);
let request = make_request(target, auth);
stream.write_all(request.as_bytes()).await?;
stream.flush().await?;
check_response(&mut stream).await?;
Ok(Box::new(stream))
}
async fn https_connect<T>(
stream: T,
target: (&str, u16),
auth: Option<HttpProxyAuthorization<'_>>,
proxy_domain: &str,
) -> Result<Box<dyn AsyncReadWrite>>
where
T: AsyncReadWrite,
{
let tls_connector = TlsConnector::from(native_tls::TlsConnector::new()?);
let stream = tls_connector.connect(proxy_domain, stream).await?;
http_connect(stream, target, auth).await
}
fn make_request(target: (&str, u16), auth: Option<HttpProxyAuthorization<'_>>) -> String {
let (host, port) = target;
let mut request = format!(
"CONNECT {host}:{port} HTTP/1.1\r\nHost: {host}:{port}\r\nProxy-Connection: Keep-Alive\r\n"
);
if let Some(HttpProxyAuthorization { username, password }) = auth {
let auth =
base64::prelude::BASE64_STANDARD.encode(format!("{username}:{password}").as_bytes());
let auth = format!("Proxy-Authorization: Basic {auth}\r\n");
request.push_str(&auth);
}
request.push_str("\r\n");
request
}
async fn check_response<T>(stream: &mut BufStream<T>) -> Result<()>
where
T: AsyncReadWrite,
{
let response = recv_response(stream).await?;
let mut dummy_headers = [EMPTY_HEADER; MAX_RESPONSE_HEADERS];
let mut parser = Response::new(&mut dummy_headers);
parser.parse(response.as_bytes())?;
match parser.code {
Some(code) => {
if code == 200 {
Ok(())
} else {
Err(anyhow::anyhow!(
"Proxy connection failed with HTTP code: {code}"
))
}
}
None => Err(anyhow::anyhow!(
"Proxy connection failed with no HTTP code: {}",
parser.reason.unwrap_or("Unknown reason")
)),
}
}
const MAX_RESPONSE_HEADER_LENGTH: usize = 4096;
const MAX_RESPONSE_HEADERS: usize = 16;
async fn recv_response<T>(stream: &mut BufStream<T>) -> Result<String>
where
T: AsyncReadWrite,
{
let mut response = String::new();
loop {
if stream.read_line(&mut response).await? == 0 {
return Err(anyhow::anyhow!("End of stream"));
}
if MAX_RESPONSE_HEADER_LENGTH < response.len() {
return Err(anyhow::anyhow!("Maximum response header length exceeded"));
}
if response.ends_with("\r\n\r\n") {
return Ok(response);
}
}
}
#[cfg(test)]
mod tests {
use url::Url;
use super::{HttpProxyAuthorization, HttpProxyType, parse_http_proxy};
#[test]
fn test_parse_http_proxy() {
let proxy = Url::parse("http://proxy.example.com:1080").unwrap();
let scheme = proxy.scheme();
let version = parse_http_proxy(scheme, &proxy);
assert!(matches!(version, HttpProxyType::HTTP(None)))
}
#[test]
fn test_parse_http_proxy_with_auth() {
let proxy = Url::parse("http://username:password@proxy.example.com:1080").unwrap();
let scheme = proxy.scheme();
let version = parse_http_proxy(scheme, &proxy);
assert!(matches!(
version,
HttpProxyType::HTTP(Some(HttpProxyAuthorization {
username: "username",
password: "password"
}))
))
}
}

View File

@@ -1,15 +1,19 @@
//! socks proxy
use anyhow::{Context, Result, anyhow};
use http_client::Url;
use anyhow::{Context, Result};
use tokio::net::TcpStream;
use tokio_socks::tcp::{Socks4Stream, Socks5Stream};
use url::Url;
use super::AsyncReadWrite;
/// Identification to a Socks V4 Proxy
struct Socks4Identification<'a> {
pub(super) struct Socks4Identification<'a> {
user_id: &'a str,
}
/// Authorization to a Socks V5 Proxy
struct Socks5Authorization<'a> {
pub(super) struct Socks5Authorization<'a> {
username: &'a str,
password: &'a str,
}
@@ -18,45 +22,50 @@ struct Socks5Authorization<'a> {
///
/// V4 allows idenfication using a user_id
/// V5 allows authorization using a username and password
enum SocksVersion<'a> {
pub(super) enum SocksVersion<'a> {
V4(Option<Socks4Identification<'a>>),
V5(Option<Socks5Authorization<'a>>),
}
pub(crate) async fn connect_socks_proxy_stream(
proxy: &Url,
pub(super) fn parse_socks_proxy<'t>(scheme: &str, proxy: &'t Url) -> SocksVersion<'t> {
if scheme.starts_with("socks4") {
let identification = match proxy.username() {
"" => None,
username => Some(Socks4Identification { user_id: username }),
};
SocksVersion::V4(identification)
} else {
let authorization = proxy.password().map(|password| Socks5Authorization {
username: proxy.username(),
password,
});
SocksVersion::V5(authorization)
}
}
pub(super) async fn connect_socks_proxy_stream(
stream: TcpStream,
socks_version: SocksVersion<'_>,
rpc_host: (&str, u16),
) -> Result<Box<dyn AsyncReadWrite>> {
let Some((socks_proxy, version)) = parse_socks_proxy(proxy) else {
// If parsing the proxy URL fails, we must avoid falling back to an insecure connection.
// SOCKS proxies are often used in contexts where security and privacy are critical,
// so any fallback could expose users to significant risks.
return Err(anyhow!("Parsing proxy url failed"));
};
// Connect to proxy and wrap protocol later
let stream = tokio::net::TcpStream::connect(socks_proxy)
.await
.context("Failed to connect to socks proxy")?;
let socks: Box<dyn AsyncReadWrite> = match version {
match socks_version {
SocksVersion::V4(None) => {
let socks = Socks4Stream::connect_with_socket(stream, rpc_host)
.await
.context("error connecting to socks")?;
Box::new(socks)
Ok(Box::new(socks))
}
SocksVersion::V4(Some(Socks4Identification { user_id })) => {
let socks = Socks4Stream::connect_with_userid_and_socket(stream, rpc_host, user_id)
.await
.context("error connecting to socks")?;
Box::new(socks)
Ok(Box::new(socks))
}
SocksVersion::V5(None) => {
let socks = Socks5Stream::connect_with_socket(stream, rpc_host)
.await
.context("error connecting to socks")?;
Box::new(socks)
Ok(Box::new(socks))
}
SocksVersion::V5(Some(Socks5Authorization { username, password })) => {
let socks = Socks5Stream::connect_with_password_and_socket(
@@ -64,44 +73,9 @@ pub(crate) async fn connect_socks_proxy_stream(
)
.await
.context("error connecting to socks")?;
Box::new(socks)
Ok(Box::new(socks))
}
};
Ok(socks)
}
fn parse_socks_proxy(proxy: &Url) -> Option<((String, u16), SocksVersion<'_>)> {
let scheme = proxy.scheme();
let socks_version = if scheme.starts_with("socks4") {
let identification = match proxy.username() {
"" => None,
username => Some(Socks4Identification { user_id: username }),
};
SocksVersion::V4(identification)
} else if scheme.starts_with("socks") {
let authorization = proxy.password().map(|password| Socks5Authorization {
username: proxy.username(),
password,
});
SocksVersion::V5(authorization)
} else {
return None;
};
let host = proxy.host()?.to_string();
let port = proxy.port_or_known_default()?;
Some(((host, port), socks_version))
}
pub(crate) trait AsyncReadWrite:
tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static
{
}
impl<T: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static> AsyncReadWrite
for T
{
}
}
#[cfg(test)]
@@ -113,20 +87,18 @@ mod tests {
#[test]
fn parse_socks4() {
let proxy = Url::parse("socks4://proxy.example.com:1080").unwrap();
let scheme = proxy.scheme();
let ((host, port), version) = parse_socks_proxy(&proxy).unwrap();
assert_eq!(host, "proxy.example.com");
assert_eq!(port, 1080);
let version = parse_socks_proxy(scheme, &proxy);
assert!(matches!(version, SocksVersion::V4(None)))
}
#[test]
fn parse_socks4_with_identification() {
let proxy = Url::parse("socks4://userid@proxy.example.com:1080").unwrap();
let scheme = proxy.scheme();
let ((host, port), version) = parse_socks_proxy(&proxy).unwrap();
assert_eq!(host, "proxy.example.com");
assert_eq!(port, 1080);
let version = parse_socks_proxy(scheme, &proxy);
assert!(matches!(
version,
SocksVersion::V4(Some(Socks4Identification { user_id: "userid" }))
@@ -136,20 +108,18 @@ mod tests {
#[test]
fn parse_socks5() {
let proxy = Url::parse("socks5://proxy.example.com:1080").unwrap();
let scheme = proxy.scheme();
let ((host, port), version) = parse_socks_proxy(&proxy).unwrap();
assert_eq!(host, "proxy.example.com");
assert_eq!(port, 1080);
let version = parse_socks_proxy(scheme, &proxy);
assert!(matches!(version, SocksVersion::V5(None)))
}
#[test]
fn parse_socks5_with_authorization() {
let proxy = Url::parse("socks5://username:password@proxy.example.com:1080").unwrap();
let scheme = proxy.scheme();
let ((host, port), version) = parse_socks_proxy(&proxy).unwrap();
assert_eq!(host, "proxy.example.com");
assert_eq!(port, 1080);
let version = parse_socks_proxy(scheme, &proxy);
assert!(matches!(
version,
SocksVersion::V5(Some(Socks5Authorization {
@@ -158,19 +128,4 @@ mod tests {
}))
))
}
/// If parsing the proxy URL fails, we must avoid falling back to an insecure connection.
/// SOCKS proxies are often used in contexts where security and privacy are critical,
/// so any fallback could expose users to significant risks.
#[tokio::test]
async fn fails_on_bad_proxy() {
// Should fail connecting because http is not a valid Socks proxy scheme
let proxy = Url::parse("http://localhost:2313").unwrap();
let result = connect_socks_proxy_stream(&proxy, ("test", 1080)).await;
match result {
Err(e) => assert_eq!(e.to_string(), "Parsing proxy url failed"),
Ok(_) => panic!("Connecting on bad proxy should fail"),
};
}
}

View File

@@ -137,18 +137,14 @@ pub fn os_version() -> String {
log::error!("Failed to load /etc/os-release, /usr/lib/os-release");
"".to_string()
};
let mut name = "unknown".to_string();
let mut version = "unknown".to_string();
let mut name = "unknown";
let mut version = "unknown";
for line in content.lines() {
if line.starts_with("ID=") {
name = line.trim_start_matches("ID=").trim_matches('"').to_string();
}
if line.starts_with("VERSION_ID=") {
version = line
.trim_start_matches("VERSION_ID=")
.trim_matches('"')
.to_string();
match line.split_once('=') {
Some(("ID", val)) => name = val.trim_matches('"'),
Some(("VERSION_ID", val)) => version = val.trim_matches('"'),
_ => {}
}
}
@@ -222,7 +218,7 @@ impl Telemetry {
cx.background_spawn({
let state = state.clone();
let os_version = os_version();
state.lock().os_version = Some(os_version.clone());
state.lock().os_version = Some(os_version);
async move {
if let Some(tempfile) = File::create(Self::log_file_path()).log_err() {
state.lock().log_file = Some(tempfile);
@@ -369,7 +365,7 @@ impl Telemetry {
telemetry::event!(
"Editor Edited",
duration = duration,
environment = environment.to_string(),
environment = environment,
is_via_ssh = is_via_ssh
);
}
@@ -431,9 +427,8 @@ impl Telemetry {
if state.flush_events_task.is_none() {
let this = self.clone();
let executor = self.executor.clone();
state.flush_events_task = Some(self.executor.spawn(async move {
executor.timer(FLUSH_INTERVAL).await;
this.executor.timer(FLUSH_INTERVAL).await;
this.flush_events().detach();
}));
}
@@ -484,12 +479,12 @@ impl Telemetry {
self: &Arc<Self>,
// We take in the JSON bytes buffer so we can reuse the existing allocation.
mut json_bytes: Vec<u8>,
event_request: EventRequestBody,
event_request: &EventRequestBody,
) -> Result<Request<AsyncBody>> {
json_bytes.clear();
serde_json::to_writer(&mut json_bytes, &event_request)?;
serde_json::to_writer(&mut json_bytes, event_request)?;
let checksum = calculate_json_checksum(&json_bytes).unwrap_or("".to_string());
let checksum = calculate_json_checksum(&json_bytes).unwrap_or_default();
Ok(Request::builder()
.method(Method::POST)
@@ -506,7 +501,7 @@ impl Telemetry {
pub fn flush_events(self: &Arc<Self>) -> Task<()> {
let mut state = self.state.lock();
state.first_event_date_time = None;
let mut events = mem::take(&mut state.events_queue);
let events = mem::take(&mut state.events_queue);
state.flush_events_task.take();
drop(state);
if events.is_empty() {
@@ -519,7 +514,7 @@ impl Telemetry {
let mut json_bytes = Vec::new();
if let Some(file) = &mut this.state.lock().log_file {
for event in &mut events {
for event in &events {
json_bytes.clear();
serde_json::to_writer(&mut json_bytes, event)?;
file.write_all(&json_bytes)?;
@@ -546,7 +541,7 @@ impl Telemetry {
}
};
let request = this.build_request(json_bytes, request_body)?;
let request = this.build_request(json_bytes, &request_body)?;
let response = this.http_client.send(request).await?;
if response.status() != 200 {
log::error!("Failed to send events: HTTP {:?}", response.status());

View File

@@ -543,7 +543,7 @@ pub struct MembershipUpdated {
/// The result of setting a member's role.
#[derive(Debug)]
#[allow(clippy::large_enum_variant)]
pub enum SetMemberRoleResult {
InviteUpdated(Channel),
MembershipUpdated(MembershipUpdated),

View File

@@ -36,6 +36,7 @@ use util::{ResultExt as _, maybe};
const VERSION: &str = env!("CARGO_PKG_VERSION");
const REVISION: Option<&'static str> = option_env!("GITHUB_SHA");
#[expect(clippy::result_large_err)]
#[tokio::main]
async fn main() -> Result<()> {
if let Err(error) = env::load_dotenv() {

View File

@@ -36,8 +36,8 @@ fn room_participants(room: &Entity<Room>, cx: &mut TestAppContext) -> RoomPartic
room.read_with(cx, |room, _| {
let mut remote = room
.remote_participants()
.iter()
.map(|(_, participant)| participant.user.github_login.clone())
.values()
.map(|participant| participant.user.github_login.clone())
.collect::<Vec<_>>();
let mut pending = room
.pending_participants()

View File

@@ -1059,7 +1059,7 @@ impl Render for ChatPanel {
.child(
Label::new(format!(
"@{}",
user_being_replied_to.github_login.clone()
user_being_replied_to.github_login
))
.size(LabelSize::Small)
.weight(FontWeight::BOLD),

View File

@@ -7,11 +7,11 @@ use crate::notifications::collab_notification::CollabNotification;
pub struct CollabNotificationStory;
impl Render for CollabNotificationStory {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let window_container = |width, height| div().w(px(width)).h(px(height));
Story::container()
.child(Story::title_for::<CollabNotification>())
Story::container(cx)
.child(Story::title_for::<CollabNotification>(cx))
.child(
StorySection::new().child(StoryItem::new(
"Incoming Call Notification",

View File

@@ -14,7 +14,7 @@ path = "src/component.rs"
[dependencies]
collections.workspace = true
gpui.workspace = true
linkme.workspace = true
inventory.workspace = true
parking_lot.workspace = true
strum.workspace = true
theme.workspace = true

View File

@@ -9,13 +9,12 @@
mod component_layout;
pub use component_layout::*;
use std::sync::LazyLock;
pub use component_layout::*;
use collections::HashMap;
use gpui::{AnyElement, App, SharedString, Window};
use linkme::distributed_slice;
use parking_lot::RwLock;
use strum::{Display, EnumString};
@@ -24,12 +23,27 @@ pub fn components() -> ComponentRegistry {
}
pub fn init() {
let component_fns: Vec<_> = __ALL_COMPONENTS.iter().cloned().collect();
for f in component_fns {
f();
for f in inventory::iter::<ComponentFn>() {
(f.0)();
}
}
pub struct ComponentFn(fn());
impl ComponentFn {
pub const fn new(f: fn()) -> Self {
Self(f)
}
}
inventory::collect!(ComponentFn);
/// Private internals for macros.
#[doc(hidden)]
pub mod __private {
pub use inventory;
}
pub fn register_component<T: Component>() {
let id = T::id();
let metadata = ComponentMetadata {
@@ -46,9 +60,6 @@ pub fn register_component<T: Component>() {
data.components.insert(id, metadata);
}
#[distributed_slice]
pub static __ALL_COMPONENTS: [fn()] = [..];
pub static COMPONENT_DATA: LazyLock<RwLock<ComponentRegistry>> =
LazyLock::new(|| RwLock::new(ComponentRegistry::default()));

View File

@@ -15,6 +15,7 @@ path = "src/context_server.rs"
anyhow.workspace = true
async-trait.workspace = true
collections.workspace = true
environment.workspace = true
futures.workspace = true
gpui.workspace = true
log.workspace = true

View File

@@ -23,10 +23,14 @@ pub struct StdioTransport {
impl StdioTransport {
pub fn new(binary: ModelContextServerBinary, cx: &AsyncApp) -> Result<Self> {
let mut command = util::command::new_smol_command(&binary.executable);
let mut env = environment::inherited();
if let Some(binary_env) = binary.env.clone() {
env.extend(binary_env);
}
let mut command = util::command::new_smol_command(&binary.executable, &env);
command
.args(&binary.args)
.envs(binary.env.unwrap_or_default())
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())

View File

@@ -29,6 +29,7 @@ chrono.workspace = true
client.workspace = true
collections.workspace = true
command_palette_hooks.workspace = true
environment.workspace = true
fs.workspace = true
futures.workspace = true
gpui.workspace = true

View File

@@ -405,8 +405,11 @@ impl Copilot {
cx.notify();
}
fn build_env(&self, copilot_settings: &CopilotSettings) -> Option<HashMap<String, String>> {
let proxy_url = copilot_settings.proxy.clone()?;
fn build_env(&self, copilot_settings: &CopilotSettings) -> HashMap<String, String> {
let mut env = environment::inherited();
let Some(proxy_url) = copilot_settings.proxy.clone() else {
return env;
};
let no_verify = copilot_settings.proxy_no_verify;
let http_or_https_proxy = if proxy_url.starts_with("http:") {
"HTTP_PROXY"
@@ -416,17 +419,16 @@ impl Copilot {
log::error!(
"Unsupported protocol scheme for language server proxy (must be http or https)"
);
return None;
return env;
};
let mut env = HashMap::default();
env.insert(http_or_https_proxy.to_string(), proxy_url);
if let Some(true) = no_verify {
env.insert("NODE_TLS_REJECT_UNAUTHORIZED".to_string(), "0".to_string());
};
Some(env)
env
}
#[cfg(any(test, feature = "test-support"))]
@@ -466,7 +468,7 @@ impl Copilot {
new_server_id: LanguageServerId,
fs: Arc<dyn Fs>,
node_runtime: NodeRuntime,
env: Option<HashMap<String, String>>,
project_env: HashMap<String, String>,
this: WeakEntity<Self>,
awaiting_sign_in_after_start: bool,
cx: &mut AsyncApp,
@@ -478,7 +480,7 @@ impl Copilot {
let binary = LanguageServerBinary {
path: node_path,
arguments,
env,
env: Some(project_env.clone()),
};
let root_path = if cfg!(target_os = "windows") {
@@ -496,6 +498,7 @@ impl Copilot {
root_path,
None,
Default::default(),
project_env,
cx,
)?;

View File

@@ -30,6 +30,7 @@ async-trait.workspace = true
client.workspace = true
collections.workspace = true
dap-types.workspace = true
environment.workspace = true
fs.workspace = true
futures.workspace = true
gpui.workspace = true

View File

@@ -364,7 +364,7 @@ pub async fn download_adapter_from_github(
futures::io::copy(response.body_mut(), &mut file).await?;
// we cannot check the status as some adapter include files with names that trigger `Illegal byte sequence`
util::command::new_smol_command("unzip")
util::command::new_smol_command("unzip", &environment::inherited())
.arg(&zip_path)
.arg("-d")
.arg(&version_path)

View File

@@ -547,7 +547,7 @@ impl TcpTransport {
let host = connection_args.host;
let port = connection_args.port;
let mut command = util::command::new_std_command(&binary.command);
let mut command = util::command::new_std_command(&binary.command, &binary.envs);
util::set_pre_exec_to_start_new_session(&mut command);
let mut command = smol::process::Command::from(command);
@@ -556,7 +556,6 @@ impl TcpTransport {
}
command.args(&binary.arguments);
command.envs(&binary.envs);
command
.stdin(Stdio::null())
@@ -643,7 +642,7 @@ pub struct StdioTransport {
impl StdioTransport {
#[allow(dead_code, reason = "This is used in non test builds of Zed")]
async fn start(binary: &DebugAdapterBinary, _: AsyncApp) -> Result<(TransportPipe, Self)> {
let mut command = util::command::new_std_command(&binary.command);
let mut command = util::command::new_std_command(&binary.command, &binary.envs);
util::set_pre_exec_to_start_new_session(&mut command);
let mut command = smol::process::Command::from(command);
@@ -652,7 +651,6 @@ impl StdioTransport {
}
command.args(&binary.arguments);
command.envs(&binary.envs);
command
.stdin(Stdio::piped())

View File

@@ -39,6 +39,7 @@ impl DebugAdapter for RubyDebugAdapter {
) -> Result<DebugAdapterBinary> {
let adapter_path = paths::debug_adapters_dir().join(self.name().as_ref());
let mut rdbg_path = adapter_path.join("rdbg");
let env = delegate.shell_env().await;
if !delegate.fs().is_file(&rdbg_path).await {
match delegate.which("rdbg".as_ref()) {
Some(path) => rdbg_path = path,
@@ -46,7 +47,7 @@ impl DebugAdapter for RubyDebugAdapter {
delegate.output_to_console(
"rdbg not found on path, trying `gem install debug`".to_string(),
);
let output = new_smol_command("gem")
let output = new_smol_command("gem", &env)
.arg("install")
.arg("--no-document")
.arg("--bindir")

View File

@@ -684,7 +684,7 @@ impl Render for DapLogView {
}
}
actions!(debug, [OpenDebuggerAdapterLogs]);
actions!(dev, [OpenDebugAdapterLogs]);
pub fn init(cx: &mut App) {
let log_store = cx.new(|cx| LogStore::new(cx));
@@ -702,7 +702,7 @@ pub fn init(cx: &mut App) {
}
let log_store = log_store.clone();
workspace.register_action(move |workspace, _: &OpenDebuggerAdapterLogs, window, cx| {
workspace.register_action(move |workspace, _: &OpenDebugAdapterLogs, window, cx| {
let project = workspace.project().read(cx);
if project.is_local() {
workspace.add_item_to_active_pane(

View File

@@ -1,5 +1,6 @@
use crate::persistence::DebuggerPaneItem;
use crate::session::DebugSession;
use crate::session::running::RunningState;
use crate::{
ClearAllBreakpoints, Continue, Detach, FocusBreakpointList, FocusConsole, FocusFrames,
FocusLoadedSources, FocusModules, FocusTerminal, FocusVariables, Pause, Restart,
@@ -30,7 +31,7 @@ use settings::Settings;
use std::any::TypeId;
use std::sync::Arc;
use task::{DebugScenario, TaskContext};
use ui::{ContextMenu, Divider, DropdownMenu, Tooltip, prelude::*};
use ui::{ContextMenu, Divider, Tooltip, prelude::*};
use workspace::SplitDirection;
use workspace::{
Pane, Workspace,
@@ -87,7 +88,20 @@ impl DebugPanel {
})
}
fn filter_action_types(&self, cx: &mut App) {
pub(crate) fn sessions(&self) -> Vec<Entity<DebugSession>> {
self.sessions.clone()
}
pub fn active_session(&self) -> Option<Entity<DebugSession>> {
self.active_session.clone()
}
pub(crate) fn running_state(&self, cx: &mut App) -> Option<Entity<RunningState>> {
self.active_session()
.map(|session| session.read(cx).running_state().clone())
}
pub(crate) fn filter_action_types(&self, cx: &mut App) {
let (has_active_session, supports_restart, support_step_back, status) = self
.active_session()
.map(|item| {
@@ -273,7 +287,7 @@ impl DebugPanel {
.detach_and_log_err(cx);
}
async fn register_session(
pub(crate) async fn register_session(
this: WeakEntity<Self>,
session: Entity<Session>,
cx: &mut AsyncWindowContext,
@@ -342,7 +356,7 @@ impl DebugPanel {
Ok(debug_session)
}
fn handle_restart_request(
pub(crate) fn handle_restart_request(
&mut self,
mut curr_session: Entity<Session>,
window: &mut Window,
@@ -416,11 +430,12 @@ impl DebugPanel {
.detach_and_log_err(cx);
}
pub fn active_session(&self) -> Option<Entity<DebugSession>> {
self.active_session.clone()
}
fn close_session(&mut self, entity_id: EntityId, window: &mut Window, cx: &mut Context<Self>) {
pub(crate) fn close_session(
&mut self,
entity_id: EntityId,
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(session) = self
.sessions
.iter()
@@ -474,93 +489,8 @@ impl DebugPanel {
})
.detach();
}
fn sessions_drop_down_menu(
&self,
active_session: &Entity<DebugSession>,
window: &mut Window,
cx: &mut Context<Self>,
) -> DropdownMenu {
let sessions = self.sessions.clone();
let weak = cx.weak_entity();
let label = active_session.read(cx).label_element(cx);
DropdownMenu::new_with_element(
"debugger-session-list",
label,
ContextMenu::build(window, cx, move |mut this, _, cx| {
let context_menu = cx.weak_entity();
for session in sessions.into_iter() {
let weak_session = session.downgrade();
let weak_session_id = weak_session.entity_id();
this = this.custom_entry(
{
let weak = weak.clone();
let context_menu = context_menu.clone();
move |_, cx| {
weak_session
.read_with(cx, |session, cx| {
let context_menu = context_menu.clone();
let id: SharedString =
format!("debug-session-{}", session.session_id(cx).0)
.into();
h_flex()
.w_full()
.group(id.clone())
.justify_between()
.child(session.label_element(cx))
.child(
IconButton::new(
"close-debug-session",
IconName::Close,
)
.visible_on_hover(id.clone())
.icon_size(IconSize::Small)
.on_click({
let weak = weak.clone();
move |_, window, cx| {
weak.update(cx, |panel, cx| {
panel.close_session(
weak_session_id,
window,
cx,
);
})
.ok();
context_menu
.update(cx, |this, cx| {
this.cancel(
&Default::default(),
window,
cx,
);
})
.ok();
}
}),
)
.into_any_element()
})
.unwrap_or_else(|_| div().into_any_element())
}
},
{
let weak = weak.clone();
move |window, cx| {
weak.update(cx, |panel, cx| {
panel.activate_session(session.clone(), window, cx);
})
.ok();
}
},
);
}
this
}),
)
}
fn deploy_context_menu(
pub(crate) fn deploy_context_menu(
&mut self,
position: Point<Pixels>,
window: &mut Window,
@@ -611,7 +541,11 @@ impl DebugPanel {
}
}
fn top_controls_strip(&self, window: &mut Window, cx: &mut Context<Self>) -> Option<Div> {
pub(crate) fn top_controls_strip(
&mut self,
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<Div> {
let active_session = self.active_session.clone();
let focus_handle = self.focus_handle.clone();
let is_side = self.position(window, cx).axis() == gpui::Axis::Horizontal;
@@ -651,12 +585,12 @@ impl DebugPanel {
active_session
.as_ref()
.map(|session| session.read(cx).running_state()),
|this, running_session| {
|this, running_state| {
let thread_status =
running_session.read(cx).thread_status(cx).unwrap_or(
running_state.read(cx).thread_status(cx).unwrap_or(
project::debugger::session::ThreadStatus::Exited,
);
let capabilities = running_session.read(cx).capabilities(cx);
let capabilities = running_state.read(cx).capabilities(cx);
this.map(|this| {
if thread_status == ThreadStatus::Running {
this.child(
@@ -667,7 +601,7 @@ impl DebugPanel {
.icon_size(IconSize::XSmall)
.shape(ui::IconButtonShape::Square)
.on_click(window.listener_for(
&running_session,
&running_state,
|this, _, _window, cx| {
this.pause_thread(cx);
},
@@ -694,7 +628,7 @@ impl DebugPanel {
.icon_size(IconSize::XSmall)
.shape(ui::IconButtonShape::Square)
.on_click(window.listener_for(
&running_session,
&running_state,
|this, _, _window, cx| this.continue_thread(cx),
))
.disabled(thread_status != ThreadStatus::Stopped)
@@ -718,7 +652,7 @@ impl DebugPanel {
.icon_size(IconSize::XSmall)
.shape(ui::IconButtonShape::Square)
.on_click(window.listener_for(
&running_session,
&running_state,
|this, _, _window, cx| {
this.step_over(cx);
},
@@ -742,7 +676,7 @@ impl DebugPanel {
.icon_size(IconSize::XSmall)
.shape(ui::IconButtonShape::Square)
.on_click(window.listener_for(
&running_session,
&running_state,
|this, _, _window, cx| {
this.step_out(cx);
},
@@ -769,7 +703,7 @@ impl DebugPanel {
.icon_size(IconSize::XSmall)
.shape(ui::IconButtonShape::Square)
.on_click(window.listener_for(
&running_session,
&running_state,
|this, _, _window, cx| {
this.step_in(cx);
},
@@ -819,7 +753,7 @@ impl DebugPanel {
|| thread_status == ThreadStatus::Ended,
)
.on_click(window.listener_for(
&running_session,
&running_state,
|this, _, _window, cx| {
this.toggle_ignore_breakpoints(cx);
},
@@ -842,7 +776,7 @@ impl DebugPanel {
IconButton::new("debug-restart", IconName::DebugRestart)
.icon_size(IconSize::XSmall)
.on_click(window.listener_for(
&running_session,
&running_state,
|this, _, _window, cx| {
this.restart_session(cx);
},
@@ -864,7 +798,7 @@ impl DebugPanel {
IconButton::new("debug-stop", IconName::Power)
.icon_size(IconSize::XSmall)
.on_click(window.listener_for(
&running_session,
&running_state,
|this, _, _window, cx| {
this.stop_thread(cx);
},
@@ -898,7 +832,7 @@ impl DebugPanel {
IconButton::new("debug-disconnect", IconName::DebugDetach)
.icon_size(IconSize::XSmall)
.on_click(window.listener_for(
&running_session,
&running_state,
|this, _, _, cx| {
this.detach_client(cx);
},
@@ -932,30 +866,42 @@ impl DebugPanel {
.as_ref()
.map(|session| session.read(cx).running_state())
.cloned(),
|this, session| {
this.child(
session.update(cx, |this, cx| {
this.thread_dropdown(window, cx)
}),
)
|this, running_state| {
this.children({
let running_state = running_state.clone();
let threads =
running_state.update(cx, |running_state, cx| {
let session = running_state.session();
session
.update(cx, |session, cx| session.threads(cx))
});
self.render_thread_dropdown(
&running_state,
threads,
window,
cx,
)
})
.when(!is_side, |this| this.gap_2().child(Divider::vertical()))
},
),
)
.child(
h_flex()
.when_some(active_session.as_ref(), |this, session| {
let context_menu =
self.sessions_drop_down_menu(session, window, cx);
this.child(context_menu).gap_2().child(Divider::vertical())
})
.children(self.render_session_menu(
self.active_session(),
self.running_state(cx),
window,
cx,
))
.when(!is_side, |this| this.child(new_session_button())),
),
),
)
}
fn activate_pane_in_direction(
pub(crate) fn activate_pane_in_direction(
&mut self,
direction: SplitDirection,
window: &mut Window,
@@ -970,7 +916,7 @@ impl DebugPanel {
}
}
fn activate_item(
pub(crate) fn activate_item(
&mut self,
item: DebuggerPaneItem,
window: &mut Window,
@@ -985,7 +931,7 @@ impl DebugPanel {
}
}
fn activate_session(
pub(crate) fn activate_session(
&mut self,
session_item: Entity<DebugSession>,
window: &mut Window,

View File

@@ -13,6 +13,7 @@ use workspace::{ItemHandle, ShutdownDebugAdapters, Workspace};
pub mod attach_modal;
pub mod debugger_panel;
mod dropdown_menus;
mod new_session_modal;
mod persistence;
pub(crate) mod session;

View File

@@ -0,0 +1,186 @@
use gpui::Entity;
use project::debugger::session::{ThreadId, ThreadStatus};
use ui::{ContextMenu, DropdownMenu, DropdownStyle, Indicator, prelude::*};
use crate::{
debugger_panel::DebugPanel,
session::{DebugSession, running::RunningState},
};
impl DebugPanel {
fn dropdown_label(label: impl Into<SharedString>) -> Label {
Label::new(label).size(LabelSize::Small)
}
pub fn render_session_menu(
&mut self,
active_session: Option<Entity<DebugSession>>,
running_state: Option<Entity<RunningState>>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<impl IntoElement> {
if let Some(running_state) = running_state {
let sessions = self.sessions().clone();
let weak = cx.weak_entity();
let running_state = running_state.read(cx);
let label = if let Some(active_session) = active_session {
active_session.read(cx).session(cx).read(cx).label()
} else {
SharedString::new_static("Unknown Session")
};
let is_terminated = running_state.session().read(cx).is_terminated();
let session_state_indicator = {
if is_terminated {
Some(Indicator::dot().color(Color::Error))
} else {
match running_state.thread_status(cx).unwrap_or_default() {
project::debugger::session::ThreadStatus::Stopped => {
Some(Indicator::dot().color(Color::Conflict))
}
_ => Some(Indicator::dot().color(Color::Success)),
}
}
};
let trigger = h_flex()
.gap_2()
.when_some(session_state_indicator, |this, indicator| {
this.child(indicator)
})
.justify_between()
.child(
DebugPanel::dropdown_label(label)
.when(is_terminated, |this| this.strikethrough()),
)
.into_any_element();
Some(
DropdownMenu::new_with_element(
"debugger-session-list",
trigger,
ContextMenu::build(window, cx, move |mut this, _, cx| {
let context_menu = cx.weak_entity();
for session in sessions.into_iter() {
let weak_session = session.downgrade();
let weak_session_id = weak_session.entity_id();
this = this.custom_entry(
{
let weak = weak.clone();
let context_menu = context_menu.clone();
move |_, cx| {
weak_session
.read_with(cx, |session, cx| {
let context_menu = context_menu.clone();
let id: SharedString = format!(
"debug-session-{}",
session.session_id(cx).0
)
.into();
h_flex()
.w_full()
.group(id.clone())
.justify_between()
.child(session.label_element(cx))
.child(
IconButton::new(
"close-debug-session",
IconName::Close,
)
.visible_on_hover(id.clone())
.icon_size(IconSize::Small)
.on_click({
let weak = weak.clone();
move |_, window, cx| {
weak.update(cx, |panel, cx| {
panel.close_session(
weak_session_id,
window,
cx,
);
})
.ok();
context_menu
.update(cx, |this, cx| {
this.cancel(
&Default::default(),
window,
cx,
);
})
.ok();
}
}),
)
.into_any_element()
})
.unwrap_or_else(|_| div().into_any_element())
}
},
{
let weak = weak.clone();
move |window, cx| {
weak.update(cx, |panel, cx| {
panel.activate_session(session.clone(), window, cx);
})
.ok();
}
},
);
}
this
}),
)
.style(DropdownStyle::Ghost),
)
} else {
None
}
}
pub(crate) fn render_thread_dropdown(
&self,
running_state: &Entity<RunningState>,
threads: Vec<(dap::Thread, ThreadStatus)>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<DropdownMenu> {
let running_state = running_state.clone();
let running_state_read = running_state.read(cx);
let thread_id = running_state_read.thread_id();
let session = running_state_read.session();
let session_id = session.read(cx).session_id();
let session_terminated = session.read(cx).is_terminated();
let selected_thread_name = threads
.iter()
.find(|(thread, _)| thread_id.map(|id| id.0) == Some(thread.id))
.map(|(thread, _)| thread.name.clone());
if let Some(selected_thread_name) = selected_thread_name {
let trigger = DebugPanel::dropdown_label(selected_thread_name).into_any_element();
Some(
DropdownMenu::new_with_element(
("thread-list", session_id.0),
trigger,
ContextMenu::build_eager(window, cx, move |mut this, _, _| {
for (thread, _) in threads {
let running_state = running_state.clone();
let thread_id = thread.id;
this = this.entry(thread.name, None, move |window, cx| {
running_state.update(cx, |running_state, cx| {
running_state.select_thread(ThreadId(thread_id), window, cx);
});
});
}
this
}),
)
.disabled(session_terminated)
.style(DropdownStyle::Ghost),
)
} else {
None
}
}
}

View File

@@ -131,6 +131,8 @@ impl NewSessionModal {
this.custom_mode.update(cx, |custom, cx| {
custom.load(active_cwd, window, cx);
});
this.debugger = None;
}
this.launch_picker.update(cx, |picker, cx| {
@@ -802,36 +804,10 @@ impl CustomMode {
command
};
let program = if let Some(program) = program.strip_prefix('~') {
format!(
"$ZED_WORKTREE_ROOT{}{}",
std::path::MAIN_SEPARATOR,
&program
)
} else if !program.starts_with(std::path::MAIN_SEPARATOR) {
format!(
"$ZED_WORKTREE_ROOT{}{}",
std::path::MAIN_SEPARATOR,
&program
)
} else {
program
};
let path = if path.starts_with('~') && !path.is_empty() {
format!(
"$ZED_WORKTREE_ROOT{}{}",
std::path::MAIN_SEPARATOR,
&path[1..]
)
} else if !path.starts_with(std::path::MAIN_SEPARATOR) && !path.is_empty() {
format!("$ZED_WORKTREE_ROOT{}{}", std::path::MAIN_SEPARATOR, &path)
} else {
path
};
let args = args.collect::<Vec<_>>();
let (program, path) = resolve_paths(program, path);
task::LaunchRequest {
program,
cwd: path.is_empty().not().then(|| PathBuf::from(path)),
@@ -1117,7 +1093,7 @@ impl PickerDelegate for DebugScenarioDelegate {
.get(self.selected_index())
.and_then(|match_candidate| self.candidates.get(match_candidate.candidate_id).cloned());
let Some((_, debug_scenario)) = debug_scenario else {
let Some((_, mut debug_scenario)) = debug_scenario else {
return;
};
@@ -1132,6 +1108,19 @@ impl PickerDelegate for DebugScenarioDelegate {
})
.unwrap_or_default();
if let Some(launch_config) =
debug_scenario
.request
.as_mut()
.and_then(|request| match request {
DebugRequest::Launch(launch) => Some(launch),
_ => None,
})
{
let (program, _) = resolve_paths(launch_config.program.clone(), String::new());
launch_config.program = program;
}
self.debug_panel
.update(cx, |panel, cx| {
panel.start_session(debug_scenario, task_context, None, worktree_id, window, cx);
@@ -1184,3 +1173,35 @@ impl PickerDelegate for DebugScenarioDelegate {
)
}
}
fn resolve_paths(program: String, path: String) -> (String, String) {
let program = if let Some(program) = program.strip_prefix('~') {
format!(
"$ZED_WORKTREE_ROOT{}{}",
std::path::MAIN_SEPARATOR,
&program
)
} else if !program.starts_with(std::path::MAIN_SEPARATOR) {
format!(
"$ZED_WORKTREE_ROOT{}{}",
std::path::MAIN_SEPARATOR,
&program
)
} else {
program
};
let path = if path.starts_with('~') && !path.is_empty() {
format!(
"$ZED_WORKTREE_ROOT{}{}",
std::path::MAIN_SEPARATOR,
&path[1..]
)
} else if !path.starts_with(std::path::MAIN_SEPARATOR) && !path.is_empty() {
format!("$ZED_WORKTREE_ROOT{}{}", std::path::MAIN_SEPARATOR, &path)
} else {
path
};
(program, path)
}

View File

@@ -1,7 +1,6 @@
pub mod running;
use std::{cell::OnceCell, sync::OnceLock};
use crate::{StackTraceView, debugger_panel::DebugPanel, persistence::SerializedLayout};
use dap::client::SessionId;
use gpui::{
App, Axis, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity,
@@ -11,14 +10,13 @@ use project::debugger::session::Session;
use project::worktree_store::WorktreeStore;
use rpc::proto;
use running::RunningState;
use std::{cell::OnceCell, sync::OnceLock};
use ui::{Indicator, prelude::*};
use workspace::{
CollaboratorId, FollowableItem, ViewId, Workspace,
item::{self, Item},
};
use crate::{StackTraceView, debugger_panel::DebugPanel, persistence::SerializedLayout};
pub struct DebugSession {
remote_id: Option<workspace::ViewId>,
running_state: Entity<RunningState>,
@@ -159,7 +157,11 @@ impl DebugSession {
.gap_2()
.when_some(icon, |this, indicator| this.child(indicator))
.justify_between()
.child(Label::new(label).when(is_terminated, |this| this.strikethrough()))
.child(
Label::new(label)
.size(LabelSize::Small)
.when(is_terminated, |this| this.strikethrough()),
)
.into_any_element()
}
}

View File

@@ -43,11 +43,10 @@ use task::{
};
use terminal_view::TerminalView;
use ui::{
ActiveTheme, AnyElement, App, ButtonCommon as _, Clickable as _, Context, ContextMenu,
Disableable, DropdownMenu, FluentBuilder, IconButton, IconName, IconSize, InteractiveElement,
IntoElement, Label, LabelCommon as _, ParentElement, Render, SharedString,
StatefulInteractiveElement, Styled, Tab, Tooltip, VisibleOnHover, VisualContext, Window, div,
h_flex, v_flex,
ActiveTheme, AnyElement, App, ButtonCommon as _, Clickable as _, Context, FluentBuilder,
IconButton, IconName, IconSize, InteractiveElement, IntoElement, Label, LabelCommon as _,
ParentElement, Render, SharedString, StatefulInteractiveElement, Styled, Tab, Tooltip,
VisibleOnHover, VisualContext, Window, div, h_flex, v_flex,
};
use util::ResultExt;
use variable_list::VariableList;
@@ -78,6 +77,12 @@ pub struct RunningState {
_schedule_serialize: Option<Task<()>>,
}
impl RunningState {
pub(crate) fn thread_id(&self) -> Option<ThreadId> {
self.thread_id
}
}
impl Render for RunningState {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let zoomed_pane = self
@@ -515,7 +520,7 @@ impl Focusable for DebugTerminal {
}
impl RunningState {
pub fn new(
pub(crate) fn new(
session: Entity<Session>,
project: Entity<Project>,
workspace: WeakEntity<Workspace>,
@@ -1214,10 +1219,16 @@ impl RunningState {
}
}
pub(crate) fn go_to_selected_stack_frame(&self, window: &Window, cx: &mut Context<Self>) {
pub(crate) fn go_to_selected_stack_frame(&self, window: &mut Window, cx: &mut Context<Self>) {
if self.thread_id.is_some() {
self.stack_frame_list
.update(cx, |list, cx| list.go_to_selected_stack_frame(window, cx));
.update(cx, |list, cx| {
let Some(stack_frame_id) = list.opened_stack_frame_id() else {
return Task::ready(Ok(()));
};
list.go_to_stack_frame(stack_frame_id, window, cx)
})
.detach();
}
}
@@ -1234,7 +1245,7 @@ impl RunningState {
}
pub(crate) fn selected_stack_frame_id(&self, cx: &App) -> Option<dap::StackFrameId> {
self.stack_frame_list.read(cx).selected_stack_frame_id()
self.stack_frame_list.read(cx).opened_stack_frame_id()
}
pub(crate) fn stack_frame_list(&self) -> &Entity<StackFrameList> {
@@ -1311,7 +1322,12 @@ impl RunningState {
.map(|id| self.session().read(cx).thread_status(id))
}
fn select_thread(&mut self, thread_id: ThreadId, window: &mut Window, cx: &mut Context<Self>) {
pub(crate) fn select_thread(
&mut self,
thread_id: ThreadId,
window: &mut Window,
cx: &mut Context<Self>,
) {
if self.thread_id.is_some_and(|id| id == thread_id) {
return;
}
@@ -1448,38 +1464,6 @@ impl RunningState {
});
}
pub(crate) fn thread_dropdown(
&self,
window: &mut Window,
cx: &mut Context<'_, RunningState>,
) -> DropdownMenu {
let state = cx.entity();
let session_terminated = self.session.read(cx).is_terminated();
let threads = self.session.update(cx, |this, cx| this.threads(cx));
let selected_thread_name = threads
.iter()
.find(|(thread, _)| self.thread_id.map(|id| id.0) == Some(thread.id))
.map(|(thread, _)| thread.name.clone())
.unwrap_or("Threads".to_owned());
DropdownMenu::new(
("thread-list", self.session_id.0),
selected_thread_name,
ContextMenu::build_eager(window, cx, move |mut this, _, _| {
for (thread, _) in threads {
let state = state.clone();
let thread_id = thread.id;
this = this.entry(thread.name, None, move |window, cx| {
state.update(cx, |state, cx| {
state.select_thread(ThreadId(thread_id), window, cx);
});
});
}
this
}),
)
.disabled(session_terminated)
}
fn default_pane_layout(
project: Entity<Project>,
workspace: &WeakEntity<Workspace>,

View File

@@ -21,8 +21,8 @@ use project::{
use ui::{
App, Clickable, Color, Context, Div, Icon, IconButton, IconName, Indicator, InteractiveElement,
IntoElement, Label, LabelCommon, LabelSize, ListItem, ParentElement, Render, RenderOnce,
Scrollbar, ScrollbarState, SharedString, StatefulInteractiveElement, Styled, Window, div,
h_flex, px, v_flex,
Scrollbar, ScrollbarState, SharedString, StatefulInteractiveElement, Styled, Tooltip, Window,
div, h_flex, px, v_flex,
};
use util::{ResultExt, maybe};
use workspace::Workspace;
@@ -259,6 +259,11 @@ impl LineBreakpoint {
dir, name, line
)))
.cursor_pointer()
.tooltip(Tooltip::text(if breakpoint.state.is_enabled() {
"Disable Breakpoint"
} else {
"Enable Breakpoint"
}))
.on_click({
let weak = weak.clone();
let path = path.clone();
@@ -290,6 +295,9 @@ impl LineBreakpoint {
)))
.start_slot(indicator)
.rounded()
.on_secondary_mouse_down(|_, _, cx| {
cx.stop_propagation();
})
.end_hover_slot(
IconButton::new(
SharedString::from(format!(
@@ -423,12 +431,20 @@ impl ExceptionBreakpoint {
self.id
)))
.rounded()
.on_secondary_mouse_down(|_, _, cx| {
cx.stop_propagation();
})
.start_slot(
div()
.id(SharedString::from(format!(
"exception-breakpoint-ui-item-{}-click-handler",
self.id
)))
.tooltip(Tooltip::text(if self.is_enabled {
"Disable Exception Breakpoint"
} else {
"Enable Exception Breakpoint"
}))
.on_click(move |_, _, cx| {
list.update(cx, |this, cx| {
this.session.update(cx, |this, cx| {

View File

@@ -150,8 +150,9 @@ impl Console {
pub fn evaluate(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) {
let expression = self.query_bar.update(cx, |editor, cx| {
let expression = editor.text(cx);
editor.clear(window, cx);
cx.defer_in(window, |editor, window, cx| {
editor.clear(window, cx);
});
expression
});
@@ -161,7 +162,7 @@ impl Console {
.evaluate(
expression,
Some(dap::EvaluateArgumentsContext::Repl),
self.stack_frame_list.read(cx).selected_stack_frame_id(),
self.stack_frame_list.read(cx).opened_stack_frame_id(),
None,
cx,
)
@@ -364,7 +365,7 @@ impl ConsoleQueryBarCompletionProvider {
new_text: string_match.string.clone(),
label: CodeLabel {
filter_range: 0..string_match.string.len(),
text: format!("{} {}", string_match.string.clone(), variable_value),
text: format!("{} {}", string_match.string, variable_value),
runs: Vec::new(),
},
icon_path: None,
@@ -388,7 +389,7 @@ impl ConsoleQueryBarCompletionProvider {
) -> Task<Result<Option<Vec<Completion>>>> {
let completion_task = console.update(cx, |console, cx| {
console.session.update(cx, |state, cx| {
let frame_id = console.stack_frame_list.read(cx).selected_stack_frame_id();
let frame_id = console.stack_frame_list.read(cx).opened_stack_frame_id();
state.completions(
CompletionsQuery::new(buffer.read(cx), buffer_position, frame_id),

View File

@@ -1,7 +1,8 @@
use anyhow::anyhow;
use dap::Module;
use gpui::{
AnyElement, Empty, Entity, FocusHandle, Focusable, ListState, MouseButton, Stateful,
Subscription, WeakEntity, list,
AnyElement, Entity, FocusHandle, Focusable, MouseButton, ScrollStrategy, Stateful,
Subscription, Task, UniformListScrollHandle, WeakEntity, uniform_list,
};
use project::{
ProjectItem as _, ProjectPath,
@@ -9,16 +10,17 @@ use project::{
};
use std::{path::Path, sync::Arc};
use ui::{Scrollbar, ScrollbarState, prelude::*};
use util::maybe;
use workspace::Workspace;
pub struct ModuleList {
list: ListState,
invalidate: bool,
scroll_handle: UniformListScrollHandle,
selected_ix: Option<usize>,
session: Entity<Session>,
workspace: WeakEntity<Workspace>,
focus_handle: FocusHandle,
scrollbar_state: ScrollbarState,
entries: Vec<Module>,
_rebuild_task: Task<()>,
_subscription: Subscription,
}
@@ -28,38 +30,43 @@ impl ModuleList {
workspace: WeakEntity<Workspace>,
cx: &mut Context<Self>,
) -> Self {
let weak_entity = cx.weak_entity();
let focus_handle = cx.focus_handle();
let list = ListState::new(
0,
gpui::ListAlignment::Top,
px(1000.),
move |ix, _window, cx| {
weak_entity
.upgrade()
.map(|module_list| module_list.update(cx, |this, cx| this.render_entry(ix, cx)))
.unwrap_or(div().into_any())
},
);
let _subscription = cx.subscribe(&session, |this, _, event, cx| match event {
SessionEvent::Stopped(_) | SessionEvent::Modules => {
this.invalidate = true;
cx.notify();
this.schedule_rebuild(cx);
}
_ => {}
});
Self {
scrollbar_state: ScrollbarState::new(list.clone()),
list,
let scroll_handle = UniformListScrollHandle::new();
let mut this = Self {
scrollbar_state: ScrollbarState::new(scroll_handle.clone()),
scroll_handle,
session,
workspace,
focus_handle,
entries: Vec::new(),
selected_ix: None,
_subscription,
invalidate: true,
}
_rebuild_task: Task::ready(()),
};
this.schedule_rebuild(cx);
this
}
fn schedule_rebuild(&mut self, cx: &mut Context<Self>) {
self._rebuild_task = cx.spawn(async move |this, cx| {
this.update(cx, |this, cx| {
let modules = this
.session
.update(cx, |session, cx| session.modules(cx).to_owned());
this.entries = modules;
cx.notify();
})
.ok();
});
}
fn open_module(&mut self, path: Arc<Path>, window: &mut Window, cx: &mut Context<Self>) {
@@ -111,36 +118,40 @@ impl ModuleList {
anyhow::Ok(())
})
.detach_and_log_err(cx);
.detach();
}
fn render_entry(&mut self, ix: usize, cx: &mut Context<Self>) -> AnyElement {
let Some(module) = maybe!({
self.session
.update(cx, |state, cx| state.modules(cx).get(ix).cloned())
}) else {
return Empty.into_any();
};
let module = self.entries[ix].clone();
v_flex()
.rounded_md()
.w_full()
.group("")
.id(("module-list", ix))
.on_any_mouse_down(|_, _, cx| {
cx.stop_propagation();
})
.when(module.path.is_some(), |this| {
this.on_click({
let path = module.path.as_deref().map(|path| Arc::<Path>::from(Path::new(path)));
let path = module
.path
.as_deref()
.map(|path| Arc::<Path>::from(Path::new(path)));
cx.listener(move |this, _, window, cx| {
this.selected_ix = Some(ix);
if let Some(path) = path.as_ref() {
this.open_module(path.clone(), window, cx);
} else {
log::error!("Wasn't able to find module path, but was still able to click on module list entry");
}
cx.notify();
})
})
})
.p_1()
.hover(|s| s.bg(cx.theme().colors().element_hover))
.when(Some(ix) == self.selected_ix, |s| {
s.bg(cx.theme().colors().element_hover)
})
.child(h_flex().gap_0p5().text_ui_sm(cx).child(module.name.clone()))
.child(
h_flex()
@@ -188,6 +199,96 @@ impl ModuleList {
.cursor_default()
.children(Scrollbar::vertical(self.scrollbar_state.clone()))
}
fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
let Some(ix) = self.selected_ix else { return };
let Some(entry) = self.entries.get(ix) else {
return;
};
let Some(path) = entry.path.as_deref() else {
return;
};
let path = Arc::from(Path::new(path));
self.open_module(path, window, cx);
}
fn select_ix(&mut self, ix: Option<usize>, cx: &mut Context<Self>) {
self.selected_ix = ix;
if let Some(ix) = ix {
self.scroll_handle
.scroll_to_item(ix, ScrollStrategy::Center);
}
cx.notify();
}
fn select_next(&mut self, _: &menu::SelectNext, _window: &mut Window, cx: &mut Context<Self>) {
let ix = match self.selected_ix {
_ if self.entries.len() == 0 => None,
None => Some(0),
Some(ix) => {
if ix == self.entries.len() - 1 {
Some(0)
} else {
Some(ix + 1)
}
}
};
self.select_ix(ix, cx);
}
fn select_previous(
&mut self,
_: &menu::SelectPrevious,
_window: &mut Window,
cx: &mut Context<Self>,
) {
let ix = match self.selected_ix {
_ if self.entries.len() == 0 => None,
None => Some(self.entries.len() - 1),
Some(ix) => {
if ix == 0 {
Some(self.entries.len() - 1)
} else {
Some(ix - 1)
}
}
};
self.select_ix(ix, cx);
}
fn select_first(
&mut self,
_: &menu::SelectFirst,
_window: &mut Window,
cx: &mut Context<Self>,
) {
let ix = if self.entries.len() > 0 {
Some(0)
} else {
None
};
self.select_ix(ix, cx);
}
fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
let ix = if self.entries.len() > 0 {
Some(self.entries.len() - 1)
} else {
None
};
self.select_ix(ix, cx);
}
fn render_list(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
uniform_list(
cx.entity(),
"module-list",
self.entries.len(),
|this, range, _window, cx| range.map(|ix| this.render_entry(ix, cx)).collect(),
)
.track_scroll(self.scroll_handle.clone())
.size_full()
}
}
impl Focusable for ModuleList {
@@ -197,21 +298,17 @@ impl Focusable for ModuleList {
}
impl Render for ModuleList {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
if self.invalidate {
let len = self
.session
.update(cx, |session, cx| session.modules(cx).len());
self.list.reset(len);
self.invalidate = false;
cx.notify();
}
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.track_focus(&self.focus_handle)
.on_action(cx.listener(Self::select_last))
.on_action(cx.listener(Self::select_first))
.on_action(cx.listener(Self::select_next))
.on_action(cx.listener(Self::select_previous))
.on_action(cx.listener(Self::confirm))
.size_full()
.p_1()
.child(list(self.list.clone()).size_full())
.child(self.render_list(window, cx))
.child(self.render_vertical_scrollbar(cx))
}
}

View File

@@ -5,20 +5,18 @@ use std::time::Duration;
use anyhow::{Result, anyhow};
use dap::StackFrameId;
use gpui::{
AnyElement, Entity, EventEmitter, FocusHandle, Focusable, ListState, MouseButton, Stateful,
Subscription, Task, WeakEntity, list,
AnyElement, Entity, EventEmitter, FocusHandle, Focusable, MouseButton, ScrollStrategy,
Stateful, Subscription, Task, UniformListScrollHandle, WeakEntity, uniform_list,
};
use crate::StackTraceView;
use language::PointUtf16;
use project::debugger::breakpoint_store::ActiveStackFrame;
use project::debugger::session::{Session, SessionEvent, StackFrame};
use project::{ProjectItem, ProjectPath};
use ui::{Scrollbar, ScrollbarState, Tooltip, prelude::*};
use util::ResultExt;
use workspace::{ItemHandle, Workspace};
use crate::StackTraceView;
use super::RunningState;
#[derive(Debug)]
@@ -28,19 +26,19 @@ pub enum StackFrameListEvent {
}
pub struct StackFrameList {
list: ListState,
focus_handle: FocusHandle,
_subscription: Subscription,
session: Entity<Session>,
state: WeakEntity<RunningState>,
entries: Vec<StackFrameEntry>,
workspace: WeakEntity<Workspace>,
selected_stack_frame_id: Option<StackFrameId>,
selected_ix: Option<usize>,
opened_stack_frame_id: Option<StackFrameId>,
scrollbar_state: ScrollbarState,
scroll_handle: UniformListScrollHandle,
_refresh_task: Task<()>,
}
#[allow(clippy::large_enum_variant)]
#[derive(Debug, PartialEq, Eq)]
pub enum StackFrameEntry {
Normal(dap::StackFrame),
@@ -55,22 +53,8 @@ impl StackFrameList {
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let weak_entity = cx.weak_entity();
let focus_handle = cx.focus_handle();
let list = ListState::new(
0,
gpui::ListAlignment::Top,
px(1000.),
move |ix, _window, cx| {
weak_entity
.upgrade()
.map(|stack_frame_list| {
stack_frame_list.update(cx, |this, cx| this.render_entry(ix, cx))
})
.unwrap_or(div().into_any())
},
);
let scroll_handle = UniformListScrollHandle::new();
let _subscription =
cx.subscribe_in(&session, window, |this, _, event, window, cx| match event {
@@ -84,15 +68,16 @@ impl StackFrameList {
});
let mut this = Self {
scrollbar_state: ScrollbarState::new(list.clone()),
list,
scrollbar_state: ScrollbarState::new(scroll_handle.clone()),
session,
workspace,
focus_handle,
state,
_subscription,
entries: Default::default(),
selected_stack_frame_id: None,
selected_ix: None,
opened_stack_frame_id: None,
scroll_handle,
_refresh_task: Task::ready(()),
};
this.schedule_refresh(true, window, cx);
@@ -123,7 +108,7 @@ impl StackFrameList {
fn stack_frames(&self, cx: &mut App) -> Vec<StackFrame> {
self.state
.read_with(cx, |state, _| state.thread_id)
.log_err()
.ok()
.flatten()
.map(|thread_id| {
self.session
@@ -140,27 +125,8 @@ impl StackFrameList {
.collect()
}
pub fn selected_stack_frame_id(&self) -> Option<StackFrameId> {
self.selected_stack_frame_id
}
pub(crate) fn select_stack_frame_id(
&mut self,
id: StackFrameId,
window: &Window,
cx: &mut Context<Self>,
) {
if !self.entries.iter().any(|entry| match entry {
StackFrameEntry::Normal(entry) => entry.id == id,
StackFrameEntry::Collapsed(stack_frames) => {
stack_frames.iter().any(|frame| frame.id == id)
}
}) {
return;
}
self.selected_stack_frame_id = Some(id);
self.go_to_selected_stack_frame(window, cx);
pub fn opened_stack_frame_id(&self) -> Option<StackFrameId> {
self.opened_stack_frame_id
}
pub(super) fn schedule_refresh(
@@ -193,13 +159,22 @@ impl StackFrameList {
pub fn build_entries(
&mut self,
select_first_stack_frame: bool,
open_first_stack_frame: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
let old_selected_frame_id = self
.selected_ix
.and_then(|ix| self.entries.get(ix))
.and_then(|entry| match entry {
StackFrameEntry::Normal(stack_frame) => Some(stack_frame.id),
StackFrameEntry::Collapsed(stack_frames) => {
stack_frames.first().map(|stack_frame| stack_frame.id)
}
});
let mut entries = Vec::new();
let mut collapsed_entries = Vec::new();
let mut current_stack_frame = None;
let mut first_stack_frame = None;
let stack_frames = self.stack_frames(cx);
for stack_frame in &stack_frames {
@@ -213,7 +188,7 @@ impl StackFrameList {
entries.push(StackFrameEntry::Collapsed(collapsed_entries.clone()));
}
current_stack_frame.get_or_insert(&stack_frame.dap);
first_stack_frame.get_or_insert(entries.len());
entries.push(StackFrameEntry::Normal(stack_frame.dap.clone()));
}
}
@@ -225,69 +200,60 @@ impl StackFrameList {
}
std::mem::swap(&mut self.entries, &mut entries);
self.list.reset(self.entries.len());
if let Some(current_stack_frame) = current_stack_frame.filter(|_| select_first_stack_frame)
{
self.select_stack_frame(current_stack_frame, true, window, cx)
.detach_and_log_err(cx);
if let Some(ix) = first_stack_frame.filter(|_| open_first_stack_frame) {
self.select_ix(Some(ix), cx);
self.activate_selected_entry(window, cx);
} else if let Some(old_selected_frame_id) = old_selected_frame_id {
let ix = self.entries.iter().position(|entry| match entry {
StackFrameEntry::Normal(frame) => frame.id == old_selected_frame_id,
StackFrameEntry::Collapsed(frames) => {
frames.iter().any(|frame| frame.id == old_selected_frame_id)
}
});
self.selected_ix = ix;
}
cx.emit(StackFrameListEvent::BuiltEntries);
cx.notify();
}
pub fn go_to_selected_stack_frame(&mut self, window: &Window, cx: &mut Context<Self>) {
if let Some(selected_stack_frame_id) = self.selected_stack_frame_id {
let frame = self
.entries
.iter()
.find_map(|entry| match entry {
StackFrameEntry::Normal(dap) => {
if dap.id == selected_stack_frame_id {
Some(dap)
} else {
None
}
}
StackFrameEntry::Collapsed(daps) => {
daps.iter().find(|dap| dap.id == selected_stack_frame_id)
}
})
.cloned();
if let Some(frame) = frame.as_ref() {
self.select_stack_frame(frame, true, window, cx)
.detach_and_log_err(cx);
}
}
}
pub fn select_stack_frame(
pub fn go_to_stack_frame(
&mut self,
stack_frame: &dap::StackFrame,
go_to_stack_frame: bool,
window: &Window,
stack_frame_id: StackFrameId,
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
self.selected_stack_frame_id = Some(stack_frame.id);
cx.emit(StackFrameListEvent::SelectedStackFrameChanged(
stack_frame.id,
));
cx.notify();
if !go_to_stack_frame {
return Task::ready(Ok(()));
let Some(stack_frame) = self
.entries
.iter()
.flat_map(|entry| match entry {
StackFrameEntry::Normal(stack_frame) => std::slice::from_ref(stack_frame),
StackFrameEntry::Collapsed(stack_frames) => stack_frames.as_slice(),
})
.find(|stack_frame| stack_frame.id == stack_frame_id)
.cloned()
else {
return Task::ready(Err(anyhow!("No stack frame for ID")));
};
self.go_to_stack_frame_inner(stack_frame, window, cx)
}
let row = (stack_frame.line.saturating_sub(1)) as u32;
fn go_to_stack_frame_inner(
&mut self,
stack_frame: dap::StackFrame,
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
let stack_frame_id = stack_frame.id;
self.opened_stack_frame_id = Some(stack_frame_id);
let Some(abs_path) = Self::abs_path_from_stack_frame(&stack_frame) else {
return Task::ready(Err(anyhow!("Project path not found")));
};
let stack_frame_id = stack_frame.id;
let row = stack_frame.line.saturating_sub(1) as u32;
cx.emit(StackFrameListEvent::SelectedStackFrameChanged(
stack_frame_id,
));
cx.spawn_in(window, async move |this, cx| {
let (worktree, relative_path) = this
.update(cx, |this, cx| {
@@ -386,11 +352,12 @@ impl StackFrameList {
fn render_normal_entry(
&self,
ix: usize,
stack_frame: &dap::StackFrame,
cx: &mut Context<Self>,
) -> AnyElement {
let source = stack_frame.source.clone();
let is_selected_frame = Some(stack_frame.id) == self.selected_stack_frame_id;
let is_selected_frame = Some(ix) == self.selected_ix;
let path = source.clone().and_then(|s| s.path.or(s.name));
let formatted_path = path.map(|path| format!("{}:{}", path, stack_frame.line,));
@@ -426,12 +393,12 @@ impl StackFrameList {
.when(is_selected_frame, |this| {
this.bg(cx.theme().colors().element_hover)
})
.on_click(cx.listener({
let stack_frame = stack_frame.clone();
move |this, _, window, cx| {
this.select_stack_frame(&stack_frame, true, window, cx)
.detach_and_log_err(cx);
}
.on_any_mouse_down(|_, _, cx| {
cx.stop_propagation();
})
.on_click(cx.listener(move |this, _, window, cx| {
this.selected_ix = Some(ix);
this.activate_selected_entry(window, cx);
}))
.hover(|style| style.bg(cx.theme().colors().element_hover).cursor_pointer())
.child(
@@ -486,20 +453,15 @@ impl StackFrameList {
.into_any()
}
pub fn expand_collapsed_entry(
&mut self,
ix: usize,
stack_frames: &Vec<dap::StackFrame>,
cx: &mut Context<Self>,
) {
self.entries.splice(
ix..ix + 1,
stack_frames
.iter()
.map(|frame| StackFrameEntry::Normal(frame.clone())),
);
self.list.reset(self.entries.len());
cx.notify();
pub(crate) fn expand_collapsed_entry(&mut self, ix: usize) {
let Some(StackFrameEntry::Collapsed(stack_frames)) = self.entries.get_mut(ix) else {
return;
};
let entries = std::mem::take(stack_frames)
.into_iter()
.map(StackFrameEntry::Normal);
self.entries.splice(ix..ix + 1, entries);
self.selected_ix = Some(ix);
}
fn render_collapsed_entry(
@@ -509,6 +471,7 @@ impl StackFrameList {
cx: &mut Context<Self>,
) -> AnyElement {
let first_stack_frame = &stack_frames[0];
let is_selected = Some(ix) == self.selected_ix;
h_flex()
.rounded_md()
@@ -517,11 +480,15 @@ impl StackFrameList {
.group("")
.id(("stack-frame", first_stack_frame.id))
.p_1()
.on_click(cx.listener({
let stack_frames = stack_frames.clone();
move |this, _, _window, cx| {
this.expand_collapsed_entry(ix, &stack_frames, cx);
}
.when(is_selected, |this| {
this.bg(cx.theme().colors().element_hover)
})
.on_any_mouse_down(|_, _, cx| {
cx.stop_propagation();
})
.on_click(cx.listener(move |this, _, window, cx| {
this.selected_ix = Some(ix);
this.activate_selected_entry(window, cx);
}))
.hover(|style| style.bg(cx.theme().colors().element_hover).cursor_pointer())
.child(
@@ -544,7 +511,7 @@ impl StackFrameList {
fn render_entry(&self, ix: usize, cx: &mut Context<Self>) -> AnyElement {
match &self.entries[ix] {
StackFrameEntry::Normal(stack_frame) => self.render_normal_entry(stack_frame, cx),
StackFrameEntry::Normal(stack_frame) => self.render_normal_entry(ix, stack_frame, cx),
StackFrameEntry::Collapsed(stack_frames) => {
self.render_collapsed_entry(ix, stack_frames, cx)
}
@@ -583,15 +550,120 @@ impl StackFrameList {
.cursor_default()
.children(Scrollbar::vertical(self.scrollbar_state.clone()))
}
fn select_ix(&mut self, ix: Option<usize>, cx: &mut Context<Self>) {
self.selected_ix = ix;
if let Some(ix) = self.selected_ix {
self.scroll_handle
.scroll_to_item(ix, ScrollStrategy::Center);
}
cx.notify();
}
fn select_next(&mut self, _: &menu::SelectNext, _window: &mut Window, cx: &mut Context<Self>) {
let ix = match self.selected_ix {
_ if self.entries.len() == 0 => None,
None => Some(0),
Some(ix) => {
if ix == self.entries.len() - 1 {
Some(0)
} else {
Some(ix + 1)
}
}
};
self.select_ix(ix, cx);
}
fn select_previous(
&mut self,
_: &menu::SelectPrevious,
_window: &mut Window,
cx: &mut Context<Self>,
) {
let ix = match self.selected_ix {
_ if self.entries.len() == 0 => None,
None => Some(self.entries.len() - 1),
Some(ix) => {
if ix == 0 {
Some(self.entries.len() - 1)
} else {
Some(ix - 1)
}
}
};
self.select_ix(ix, cx);
}
fn select_first(
&mut self,
_: &menu::SelectFirst,
_window: &mut Window,
cx: &mut Context<Self>,
) {
let ix = if self.entries.len() > 0 {
Some(0)
} else {
None
};
self.select_ix(ix, cx);
}
fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
let ix = if self.entries.len() > 0 {
Some(self.entries.len() - 1)
} else {
None
};
self.select_ix(ix, cx);
}
fn activate_selected_entry(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some(ix) = self.selected_ix else {
return;
};
let Some(entry) = self.entries.get_mut(ix) else {
return;
};
match entry {
StackFrameEntry::Normal(stack_frame) => {
let stack_frame = stack_frame.clone();
self.go_to_stack_frame_inner(stack_frame, window, cx)
.detach_and_log_err(cx)
}
StackFrameEntry::Collapsed(_) => self.expand_collapsed_entry(ix),
}
cx.notify();
}
fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
self.activate_selected_entry(window, cx);
}
fn render_list(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
uniform_list(
cx.entity(),
"stack-frame-list",
self.entries.len(),
|this, range, _window, cx| range.map(|ix| this.render_entry(ix, cx)).collect(),
)
.track_scroll(self.scroll_handle.clone())
.size_full()
}
}
impl Render for StackFrameList {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.track_focus(&self.focus_handle)
.size_full()
.p_1()
.child(list(self.list.clone()).size_full())
.on_action(cx.listener(Self::select_next))
.on_action(cx.listener(Self::select_previous))
.on_action(cx.listener(Self::select_first))
.on_action(cx.listener(Self::select_last))
.on_action(cx.listener(Self::confirm))
.child(self.render_list(window, cx))
.child(self.render_vertical_scrollbar(cx))
}
}

View File

@@ -69,7 +69,7 @@ impl StackTraceView {
.filter(|id| Some(**id) != this.selected_stack_frame_id)
{
this.stack_frame_list.update(cx, |list, cx| {
list.select_stack_frame_id(*stack_frame_id, window, cx);
list.go_to_stack_frame(*stack_frame_id, window, cx).detach();
});
}
}
@@ -82,7 +82,7 @@ impl StackTraceView {
|this, stack_frame_list, event, window, cx| match event {
StackFrameListEvent::BuiltEntries => {
this.selected_stack_frame_id =
stack_frame_list.read(cx).selected_stack_frame_id();
stack_frame_list.read(cx).opened_stack_frame_id();
this.update_excerpts(window, cx);
}
StackFrameListEvent::SelectedStackFrameChanged(selected_frame_id) => {

View File

@@ -168,7 +168,7 @@ async fn test_fetch_initial_stack_frames_and_go_to_stack_frame(
.update(cx, |state, _| state.stack_frame_list().clone());
stack_frame_list.update(cx, |stack_frame_list, cx| {
assert_eq!(Some(1), stack_frame_list.selected_stack_frame_id());
assert_eq!(Some(1), stack_frame_list.opened_stack_frame_id());
assert_eq!(stack_frames, stack_frame_list.dap_stack_frames(cx));
});
});
@@ -373,14 +373,14 @@ async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppC
.unwrap();
stack_frame_list.update(cx, |stack_frame_list, cx| {
assert_eq!(Some(1), stack_frame_list.selected_stack_frame_id());
assert_eq!(Some(1), stack_frame_list.opened_stack_frame_id());
assert_eq!(stack_frames, stack_frame_list.dap_stack_frames(cx));
});
// select second stack frame
stack_frame_list
.update_in(cx, |stack_frame_list, window, cx| {
stack_frame_list.select_stack_frame(&stack_frames[1], true, window, cx)
stack_frame_list.go_to_stack_frame(stack_frames[1].id, window, cx)
})
.await
.unwrap();
@@ -388,7 +388,7 @@ async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppC
cx.run_until_parked();
stack_frame_list.update(cx, |stack_frame_list, cx| {
assert_eq!(Some(2), stack_frame_list.selected_stack_frame_id());
assert_eq!(Some(2), stack_frame_list.opened_stack_frame_id());
assert_eq!(stack_frames, stack_frame_list.dap_stack_frames(cx));
});
@@ -718,11 +718,7 @@ async fn test_collapsed_entries(executor: BackgroundExecutor, cx: &mut TestAppCo
stack_frame_list.entries()
);
stack_frame_list.expand_collapsed_entry(
1,
&vec![stack_frames[1].clone(), stack_frames[2].clone()],
cx,
);
stack_frame_list.expand_collapsed_entry(1);
assert_eq!(
&vec![
@@ -739,11 +735,7 @@ async fn test_collapsed_entries(executor: BackgroundExecutor, cx: &mut TestAppCo
stack_frame_list.entries()
);
stack_frame_list.expand_collapsed_entry(
4,
&vec![stack_frames[4].clone(), stack_frames[5].clone()],
cx,
);
stack_frame_list.expand_collapsed_entry(4);
assert_eq!(
&vec![

View File

@@ -190,7 +190,7 @@ async fn test_basic_fetch_initial_scope_and_variables(
running_state.update(cx, |running_state, cx| {
let (stack_frame_list, stack_frame_id) =
running_state.stack_frame_list().update(cx, |list, _| {
(list.flatten_entries(true), list.selected_stack_frame_id())
(list.flatten_entries(true), list.opened_stack_frame_id())
});
assert_eq!(stack_frames, stack_frame_list);
@@ -431,7 +431,7 @@ async fn test_fetch_variables_for_multiple_scopes(
running_state.update(cx, |running_state, cx| {
let (stack_frame_list, stack_frame_id) =
running_state.stack_frame_list().update(cx, |list, _| {
(list.flatten_entries(true), list.selected_stack_frame_id())
(list.flatten_entries(true), list.opened_stack_frame_id())
});
assert_eq!(Some(1), stack_frame_id);
@@ -1452,7 +1452,7 @@ async fn test_variable_list_only_sends_requests_when_rendering(
running_state.update(cx, |running_state, cx| {
let (stack_frame_list, stack_frame_id) =
running_state.stack_frame_list().update(cx, |list, _| {
(list.flatten_entries(true), list.selected_stack_frame_id())
(list.flatten_entries(true), list.opened_stack_frame_id())
});
assert_eq!(Some(1), stack_frame_id);
@@ -1734,7 +1734,7 @@ async fn test_it_fetches_scopes_variables_when_you_select_a_stack_frame(
running_state.update(cx, |running_state, cx| {
let (stack_frame_list, stack_frame_id) =
running_state.stack_frame_list().update(cx, |list, _| {
(list.flatten_entries(true), list.selected_stack_frame_id())
(list.flatten_entries(true), list.opened_stack_frame_id())
});
let variable_list = running_state.variable_list().read(cx);
@@ -1745,7 +1745,7 @@ async fn test_it_fetches_scopes_variables_when_you_select_a_stack_frame(
running_state
.stack_frame_list()
.read(cx)
.selected_stack_frame_id(),
.opened_stack_frame_id(),
Some(1)
);
@@ -1778,7 +1778,7 @@ async fn test_it_fetches_scopes_variables_when_you_select_a_stack_frame(
running_state
.stack_frame_list()
.update(cx, |stack_frame_list, cx| {
stack_frame_list.select_stack_frame(&stack_frames[1], true, window, cx)
stack_frame_list.go_to_stack_frame(stack_frames[1].id, window, cx)
})
})
.await
@@ -1789,7 +1789,7 @@ async fn test_it_fetches_scopes_variables_when_you_select_a_stack_frame(
running_state.update(cx, |running_state, cx| {
let (stack_frame_list, stack_frame_id) =
running_state.stack_frame_list().update(cx, |list, _| {
(list.flatten_entries(true), list.selected_stack_frame_id())
(list.flatten_entries(true), list.opened_stack_frame_id())
});
let variable_list = running_state.variable_list().read(cx);

View File

@@ -23,7 +23,6 @@ futures.workspace = true
gpui.workspace = true
indoc.workspace = true
language.workspace = true
linkme.workspace = true
log.workspace = true
lsp.workspace = true
markdown.workspace = true

File diff suppressed because it is too large Load Diff

View File

@@ -40,7 +40,6 @@ pub const MENU_ASIDE_X_PADDING: Pixels = px(16.);
pub const MENU_ASIDE_MIN_WIDTH: Pixels = px(260.);
pub const MENU_ASIDE_MAX_WIDTH: Pixels = px(500.);
#[allow(clippy::large_enum_variant)]
pub enum CodeContextMenu {
Completions(CompletionsMenu),
CodeActions(CodeActionsMenu),
@@ -664,12 +663,13 @@ impl CompletionsMenu {
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
enum MatchTier<'a> {
WordStartMatch {
sort_prefix: Reverse<usize>,
sort_fuzzy_bracket: Reverse<usize>,
sort_mixed_case_prefix_length: Reverse<usize>,
sort_snippet: Reverse<i32>,
sort_kind: usize,
sort_fuzzy_bracket: Reverse<usize>,
sort_text: Option<&'a str>,
sort_score: Reverse<OrderedFloat<f64>>,
sort_key: (usize, &'a str),
sort_label: &'a str,
},
OtherMatch {
sort_score: Reverse<OrderedFloat<f64>>,
@@ -680,12 +680,12 @@ impl CompletionsMenu {
// balance the raw fuzzy match score with hints from the language server
// In a fuzzy bracket, matches with a score of 1.0 are prioritized.
// The remaining matches are partitioned into two groups at 2/3 of the max_score.
// The remaining matches are partitioned into two groups at 3/5 of the max_score.
let max_score = matches
.iter()
.map(|mat| mat.string_match.score)
.fold(0.0, f64::max);
let second_bracket_threshold = max_score * (2.0 / 3.0);
let fuzzy_bracket_threshold = max_score * (3.0 / 5.0);
let query_start_lower = query
.and_then(|q| q.chars().next())
@@ -709,9 +709,7 @@ impl CompletionsMenu {
if query_start_doesnt_match_split_words {
MatchTier::OtherMatch { sort_score }
} else {
let sort_fuzzy_bracket = Reverse(if score == 1.0 {
2
} else if score >= second_bracket_threshold {
let sort_fuzzy_bracket = Reverse(if score >= fuzzy_bracket_threshold {
1
} else {
0
@@ -721,7 +719,7 @@ impl CompletionsMenu {
SnippetSortOrder::Bottom => Reverse(if mat.is_snippet { 0 } else { 1 }),
SnippetSortOrder::Inline => Reverse(0),
};
let mixed_case_prefix_length = Reverse(
let sort_mixed_case_prefix_length = Reverse(
query
.map(|q| {
q.chars()
@@ -741,12 +739,13 @@ impl CompletionsMenu {
.unwrap_or(0),
);
MatchTier::WordStartMatch {
sort_prefix: mixed_case_prefix_length,
sort_fuzzy_bracket,
sort_mixed_case_prefix_length,
sort_snippet,
sort_kind: mat.sort_kind,
sort_fuzzy_bracket,
sort_text: mat.sort_text,
sort_score,
sort_key: mat.sort_key,
sort_label: mat.sort_label,
}
}
});
@@ -797,13 +796,14 @@ impl CompletionsMenu {
None
};
let sort_key = completion.sort_key();
let (sort_kind, sort_label) = completion.sort_key();
SortableMatch {
string_match,
is_snippet,
sort_text,
sort_key,
sort_kind,
sort_label,
}
})
.collect();
@@ -828,7 +828,8 @@ pub struct SortableMatch<'a> {
pub string_match: StringMatch,
pub is_snippet: bool,
pub sort_text: Option<&'a str>,
pub sort_key: (usize, &'a str),
pub sort_kind: usize,
pub sort_label: &'a str,
}
#[derive(Clone)]
@@ -926,7 +927,6 @@ impl CodeActionContents {
}
}
#[allow(clippy::large_enum_variant)]
#[derive(Clone)]
pub enum CodeActionsItem {
Task(TaskSourceKind, ResolvedTask),

View File

@@ -282,7 +282,6 @@ struct Transform {
block: Option<Block>,
}
#[allow(clippy::large_enum_variant)]
#[derive(Clone)]
pub enum Block {
Custom(Arc<CustomBlock>),

View File

@@ -107,9 +107,9 @@ pub use items::MAX_TAB_TITLE_LEN;
use itertools::Itertools;
use language::{
AutoindentMode, BracketMatch, BracketPair, Buffer, Capability, CharKind, CodeLabel,
CursorShape, DiagnosticEntry, DiffOptions, EditPredictionsMode, EditPreview, HighlightedText,
IndentKind, IndentSize, Language, OffsetRangeExt, Point, Selection, SelectionGoal, TextObject,
TransactionId, TreeSitterOptions, WordsQuery,
CursorShape, DiagnosticEntry, DiffOptions, DocumentationConfig, EditPredictionsMode,
EditPreview, HighlightedText, IndentKind, IndentSize, Language, OffsetRangeExt, Point,
Selection, SelectionGoal, TextObject, TransactionId, TreeSitterOptions, WordsQuery,
language_settings::{
self, InlayHintSettings, LspInsertMode, RewrapBehavior, WordsCompletionMode,
all_language_settings, language_settings,
@@ -1311,7 +1311,7 @@ pub struct ActiveDiagnosticGroup {
}
#[derive(Debug, PartialEq, Eq)]
#[allow(clippy::large_enum_variant)]
pub(crate) enum ActiveDiagnostic {
None,
All,
@@ -3912,7 +3912,7 @@ impl Editor {
pub fn newline(&mut self, _: &Newline, window: &mut Window, cx: &mut Context<Self>) {
self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction);
self.transact(window, cx, |this, window, cx| {
let (edits, selection_fixup_info): (Vec<_>, Vec<_>) = {
let (edits_with_flags, selection_info): (Vec<_>, Vec<_>) = {
let selections = this.selections.all::<usize>(cx);
let multi_buffer = this.buffer.read(cx);
let buffer = multi_buffer.snapshot(cx);
@@ -3920,17 +3920,21 @@ impl Editor {
.iter()
.map(|selection| {
let start_point = selection.start.to_point(&buffer);
let mut indent =
let mut existing_indent =
buffer.indent_size_for_line(MultiBufferRow(start_point.row));
indent.len = cmp::min(indent.len, start_point.column);
existing_indent.len = cmp::min(existing_indent.len, start_point.column);
let start = selection.start;
let end = selection.end;
let selection_is_empty = start == end;
let language_scope = buffer.language_scope_at(start);
let (comment_delimiter, insert_extra_newline) = if let Some(language) =
&language_scope
{
let insert_extra_newline =
let (
comment_delimiter,
doc_delimiter,
insert_extra_newline,
indent_on_newline,
indent_on_extra_newline,
) = if let Some(language) = &language_scope {
let mut insert_extra_newline =
insert_extra_newline_brackets(&buffer, start..end, language)
|| insert_extra_newline_tree_sitter(&buffer, start..end);
@@ -3950,63 +3954,208 @@ impl Editor {
let (snapshot, range) =
buffer.buffer_line_for_row(MultiBufferRow(start_point.row))?;
let mut index_of_first_non_whitespace = 0;
let num_of_whitespaces = snapshot
.chars_for_range(range.clone())
.take_while(|c| c.is_whitespace())
.count();
let comment_candidate = snapshot
.chars_for_range(range)
.skip_while(|c| {
let should_skip = c.is_whitespace();
if should_skip {
index_of_first_non_whitespace += 1;
}
should_skip
})
.skip(num_of_whitespaces)
.take(max_len_of_delimiter)
.collect::<String>();
let comment_prefix = delimiters.iter().find(|comment_prefix| {
comment_candidate.starts_with(comment_prefix.as_ref())
})?;
let (delimiter, trimmed_len) =
delimiters.iter().find_map(|delimiter| {
let trimmed = delimiter.trim_end();
if comment_candidate.starts_with(trimmed) {
Some((delimiter, trimmed.len()))
} else {
None
}
})?;
let cursor_is_placed_after_comment_marker =
index_of_first_non_whitespace + comment_prefix.len()
<= start_point.column as usize;
num_of_whitespaces + trimmed_len <= start_point.column as usize;
if cursor_is_placed_after_comment_marker {
Some(comment_prefix.clone())
Some(delimiter.clone())
} else {
None
}
});
(comment_delimiter, insert_extra_newline)
let mut indent_on_newline = IndentSize::spaces(0);
let mut indent_on_extra_newline = IndentSize::spaces(0);
let doc_delimiter = maybe!({
if !selection_is_empty {
return None;
}
if !multi_buffer.language_settings(cx).extend_comment_on_newline {
return None;
}
let DocumentationConfig {
start: start_tag,
end: end_tag,
prefix: delimiter,
tab_size: len,
} = language.documentation()?;
let (snapshot, range) =
buffer.buffer_line_for_row(MultiBufferRow(start_point.row))?;
let num_of_whitespaces = snapshot
.chars_for_range(range.clone())
.take_while(|c| c.is_whitespace())
.count();
let cursor_is_after_start_tag = {
let start_tag_len = start_tag.len();
let start_tag_line = snapshot
.chars_for_range(range.clone())
.skip(num_of_whitespaces)
.take(start_tag_len)
.collect::<String>();
if start_tag_line.starts_with(start_tag.as_ref()) {
num_of_whitespaces + start_tag_len
<= start_point.column as usize
} else {
false
}
};
let cursor_is_after_delimiter = {
let delimiter_trim = delimiter.trim_end();
let delimiter_line = snapshot
.chars_for_range(range.clone())
.skip(num_of_whitespaces)
.take(delimiter_trim.len())
.collect::<String>();
if delimiter_line.starts_with(delimiter_trim) {
num_of_whitespaces + delimiter_trim.len()
<= start_point.column as usize
} else {
false
}
};
let cursor_is_before_end_tag_if_exists = {
let num_of_whitespaces_rev = snapshot
.reversed_chars_for_range(range.clone())
.take_while(|c| c.is_whitespace())
.count();
let mut line_iter = snapshot
.reversed_chars_for_range(range)
.skip(num_of_whitespaces_rev);
let end_tag_exists = end_tag
.chars()
.rev()
.all(|char| line_iter.next() == Some(char));
if end_tag_exists {
let max_point = snapshot.line_len(start_point.row) as usize;
let ordering = (num_of_whitespaces_rev
+ end_tag.len()
+ start_point.column as usize)
.cmp(&max_point);
let cursor_is_before_end_tag =
ordering != Ordering::Greater;
if cursor_is_after_start_tag {
if cursor_is_before_end_tag {
insert_extra_newline = true;
}
let cursor_is_at_start_of_end_tag =
ordering == Ordering::Equal;
if cursor_is_at_start_of_end_tag {
indent_on_extra_newline.len = (*len).into();
}
}
cursor_is_before_end_tag
} else {
true
}
};
if (cursor_is_after_start_tag || cursor_is_after_delimiter)
&& cursor_is_before_end_tag_if_exists
{
if cursor_is_after_start_tag {
indent_on_newline.len = (*len).into();
}
Some(delimiter.clone())
} else {
None
}
});
(
comment_delimiter,
doc_delimiter,
insert_extra_newline,
indent_on_newline,
indent_on_extra_newline,
)
} else {
(None, false)
(
None,
None,
false,
IndentSize::default(),
IndentSize::default(),
)
};
let capacity_for_delimiter = comment_delimiter
.as_deref()
.map(str::len)
.unwrap_or_default();
let mut new_text =
String::with_capacity(1 + capacity_for_delimiter + indent.len as usize);
let prevent_auto_indent = doc_delimiter.is_some();
let delimiter = comment_delimiter.or(doc_delimiter);
let capacity_for_delimiter =
delimiter.as_deref().map(str::len).unwrap_or_default();
let mut new_text = String::with_capacity(
1 + capacity_for_delimiter
+ existing_indent.len as usize
+ indent_on_newline.len as usize
+ indent_on_extra_newline.len as usize,
);
new_text.push('\n');
new_text.extend(indent.chars());
if let Some(delimiter) = &comment_delimiter {
new_text.extend(existing_indent.chars());
new_text.extend(indent_on_newline.chars());
if let Some(delimiter) = &delimiter {
new_text.push_str(delimiter);
}
if insert_extra_newline {
new_text = new_text.repeat(2);
new_text.push('\n');
new_text.extend(existing_indent.chars());
new_text.extend(indent_on_extra_newline.chars());
}
let anchor = buffer.anchor_after(end);
let new_selection = selection.map(|_| anchor);
(
(start..end, new_text),
((start..end, new_text), prevent_auto_indent),
(insert_extra_newline, new_selection),
)
})
.unzip()
};
this.edit_with_autoindent(edits, cx);
let mut auto_indent_edits = Vec::new();
let mut edits = Vec::new();
for (edit, prevent_auto_indent) in edits_with_flags {
if prevent_auto_indent {
edits.push(edit);
} else {
auto_indent_edits.push(edit);
}
}
if !edits.is_empty() {
this.edit(edits, cx);
}
if !auto_indent_edits.is_empty() {
this.edit_with_autoindent(auto_indent_edits, cx);
}
let buffer = this.buffer.read(cx).snapshot(cx);
let new_selections = selection_fixup_info
let new_selections = selection_info
.into_iter()
.map(|(extra_newline_inserted, new_selection)| {
let mut cursor = new_selection.end.to_point(&buffer);
@@ -17918,10 +18067,6 @@ impl Editor {
.and_then(|lines| lines.last().map(|line| line.range.start));
self.inline_value_cache.refresh_task = cx.spawn(async move |editor, cx| {
let snapshot = editor
.update(cx, |editor, cx| editor.buffer().read(cx).snapshot(cx))
.ok()?;
let inline_values = editor
.update(cx, |editor, cx| {
let Some(current_execution_position) = current_execution_position else {
@@ -17949,22 +18094,40 @@ impl Editor {
.context("refreshing debugger inlays")
.log_err()?;
let (excerpt_id, buffer_id) = snapshot
.excerpts()
.next()
.map(|excerpt| (excerpt.0, excerpt.1.remote_id()))?;
let mut buffer_inline_values: HashMap<BufferId, Vec<InlayHint>> = HashMap::default();
for (buffer_id, inline_value) in inline_values
.into_iter()
.filter_map(|hint| Some((hint.position.buffer_id?, hint)))
{
buffer_inline_values
.entry(buffer_id)
.or_default()
.push(inline_value);
}
editor
.update(cx, |editor, cx| {
let new_inlays = inline_values
.into_iter()
.map(|debugger_value| {
Inlay::debugger_hint(
post_inc(&mut editor.next_inlay_id),
Anchor::in_buffer(excerpt_id, buffer_id, debugger_value.position),
debugger_value.text(),
)
})
.collect::<Vec<_>>();
let snapshot = editor.buffer.read(cx).snapshot(cx);
let mut new_inlays = Vec::default();
for (excerpt_id, buffer_snapshot, _) in snapshot.excerpts() {
let buffer_id = buffer_snapshot.remote_id();
buffer_inline_values
.get(&buffer_id)
.into_iter()
.flatten()
.for_each(|hint| {
let inlay = Inlay::debugger_hint(
post_inc(&mut editor.next_inlay_id),
Anchor::in_buffer(excerpt_id, buffer_id, hint.position),
hint.text(),
);
new_inlays.push(inlay);
});
}
let mut inlay_ids = new_inlays.iter().map(|inlay| inlay.id).collect();
std::mem::swap(&mut editor.inline_value_cache.inlays, &mut inlay_ids);
@@ -20188,8 +20351,8 @@ impl EditorSnapshot {
let participant_indices = collaboration_hub.user_participant_indices(cx);
let collaborators_by_peer_id = collaboration_hub.collaborators(cx);
let collaborators_by_replica_id = collaborators_by_peer_id
.iter()
.map(|(_, collaborator)| (collaborator.replica_id, collaborator))
.values()
.map(|collaborator| (collaborator.replica_id, collaborator))
.collect::<HashMap<_, _>>();
self.buffer_snapshot
.selections_in_range(range, false)

View File

@@ -52,7 +52,7 @@ use util::{
uri,
};
use workspace::{
CloseAllItems, CloseInactiveItems, NavigationEntry, ViewId,
CloseActiveItem, CloseAllItems, CloseInactiveItems, NavigationEntry, OpenOptions, ViewId,
item::{FollowEvent, FollowableItem, Item, ItemHandle},
};
@@ -2755,7 +2755,7 @@ async fn test_newline_comments(cx: &mut TestAppContext) {
let language = Arc::new(Language::new(
LanguageConfig {
line_comments: vec!["//".into()],
line_comments: vec!["// ".into()],
..LanguageConfig::default()
},
None,
@@ -2770,7 +2770,29 @@ async fn test_newline_comments(cx: &mut TestAppContext) {
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
cx.assert_editor_state(indoc! {"
// Foo
// ˇ
"});
// Ensure that we add comment prefix when existing line contains space
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
cx.assert_editor_state(
indoc! {"
// Foo
//s
// ˇ
"}
.replace("s", " ") // s is used as space placeholder to prevent format on save
.as_str(),
);
// Ensure that we add comment prefix when existing line does not contain space
cx.set_state(indoc! {"
// Foo
//ˇ
"});
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
cx.assert_editor_state(indoc! {"
// Foo
//
// ˇ
"});
// Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
cx.set_state(indoc! {"
@@ -2797,6 +2819,177 @@ async fn test_newline_comments(cx: &mut TestAppContext) {
"});
}
#[gpui::test]
async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
init_test(cx, |settings| {
settings.defaults.tab_size = NonZeroU32::new(4)
});
let language = Arc::new(Language::new(
LanguageConfig {
documentation: Some(language::DocumentationConfig {
start: "/**".into(),
end: "*/".into(),
prefix: "* ".into(),
tab_size: NonZeroU32::new(1).unwrap(),
}),
..LanguageConfig::default()
},
None,
));
{
let mut cx = EditorTestContext::new(cx).await;
cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
cx.set_state(indoc! {"
/**ˇ
"});
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
cx.assert_editor_state(indoc! {"
/**
* ˇ
"});
// Ensure that if cursor is before the comment start,
// we do not actually insert a comment prefix.
cx.set_state(indoc! {"
ˇ/**
"});
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
cx.assert_editor_state(indoc! {"
ˇ/**
"});
// Ensure that if cursor is between it doesn't add comment prefix.
cx.set_state(indoc! {"
/*ˇ*
"});
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
cx.assert_editor_state(indoc! {"
/*
ˇ*
"});
// Ensure that if suffix exists on same line after cursor it adds new line.
cx.set_state(indoc! {"
/**ˇ*/
"});
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
cx.assert_editor_state(indoc! {"
/**
* ˇ
*/
"});
// Ensure that if suffix exists on same line after cursor with space it adds new line.
cx.set_state(indoc! {"
/**ˇ */
"});
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
cx.assert_editor_state(indoc! {"
/**
* ˇ
*/
"});
// Ensure that if suffix exists on same line after cursor with space it adds new line.
cx.set_state(indoc! {"
/** ˇ*/
"});
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
cx.assert_editor_state(
indoc! {"
/**s
* ˇ
*/
"}
.replace("s", " ") // s is used as space placeholder to prevent format on save
.as_str(),
);
// Ensure that delimiter space is preserved when newline on already
// spaced delimiter.
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
cx.assert_editor_state(
indoc! {"
/**s
*s
* ˇ
*/
"}
.replace("s", " ") // s is used as space placeholder to prevent format on save
.as_str(),
);
// Ensure that delimiter space is preserved when space is not
// on existing delimiter.
cx.set_state(indoc! {"
/**
*/
"});
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
cx.assert_editor_state(indoc! {"
/**
*
* ˇ
*/
"});
// Ensure that if suffix exists on same line after cursor it
// doesn't add extra new line if prefix is not on same line.
cx.set_state(indoc! {"
/**
ˇ*/
"});
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
cx.assert_editor_state(indoc! {"
/**
ˇ*/
"});
// Ensure that it detects suffix after existing prefix.
cx.set_state(indoc! {"
/**ˇ/
"});
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
cx.assert_editor_state(indoc! {"
/**
ˇ/
"});
// Ensure that if suffix exists on same line before
// cursor it does not add comment prefix.
cx.set_state(indoc! {"
/** */ˇ
"});
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
cx.assert_editor_state(indoc! {"
/** */
ˇ
"});
// Ensure that if suffix exists on same line before
// cursor it does not add comment prefix.
cx.set_state(indoc! {"
/**
*
*/ˇ
"});
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
cx.assert_editor_state(indoc! {"
/**
*
*/
ˇ
"});
}
// Ensure that comment continuations can be disabled.
update_test_language_settings(cx, |settings| {
settings.defaults.extend_comment_on_newline = Some(false);
});
let mut cx = EditorTestContext::new(cx).await;
cx.set_state(indoc! {"
/**ˇ
"});
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
cx.assert_editor_state(indoc! {"
/**
ˇ
"});
}
#[gpui::test]
fn test_insert_with_old_selections(cx: &mut TestAppContext) {
init_test(cx, |_| {});
@@ -19867,6 +20060,156 @@ async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
});
}
#[gpui::test]
async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
path!("/root"),
json!({
"a": {
"main.rs": "fn main() {}",
},
"foo": {
"bar": {
"external_file.rs": "pub mod external {}",
}
}
}),
)
.await;
let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
language_registry.add(rust_lang());
let _fake_servers = language_registry.register_fake_lsp(
"Rust",
FakeLspAdapter {
..FakeLspAdapter::default()
},
);
let (workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
let worktree_id = workspace.update(cx, |workspace, cx| {
workspace.project().update(cx, |project, cx| {
project.worktrees(cx).next().unwrap().read(cx).id()
})
});
let assert_language_servers_count =
|expected: usize, context: &str, cx: &mut VisualTestContext| {
project.update(cx, |project, cx| {
let current = project
.lsp_store()
.read(cx)
.as_local()
.unwrap()
.language_servers
.len();
assert_eq!(expected, current, "{context}");
});
};
assert_language_servers_count(
0,
"No servers should be running before any file is open",
cx,
);
let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
let main_editor = workspace
.update_in(cx, |workspace, window, cx| {
workspace.open_path(
(worktree_id, "main.rs"),
Some(pane.downgrade()),
true,
window,
cx,
)
})
.unwrap()
.await
.downcast::<Editor>()
.unwrap();
pane.update(cx, |pane, cx| {
let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
open_editor.update(cx, |editor, cx| {
assert_eq!(
editor.display_text(cx),
"fn main() {}",
"Original main.rs text on initial open",
);
});
assert_eq!(open_editor, main_editor);
});
assert_language_servers_count(1, "First *.rs file starts a language server", cx);
let external_editor = workspace
.update_in(cx, |workspace, window, cx| {
workspace.open_abs_path(
PathBuf::from("/root/foo/bar/external_file.rs"),
OpenOptions::default(),
window,
cx,
)
})
.await
.expect("opening external file")
.downcast::<Editor>()
.expect("downcasted external file's open element to editor");
pane.update(cx, |pane, cx| {
let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
open_editor.update(cx, |editor, cx| {
assert_eq!(
editor.display_text(cx),
"pub mod external {}",
"External file is open now",
);
});
assert_eq!(open_editor, external_editor);
});
assert_language_servers_count(
1,
"Second, external, *.rs file should join the existing server",
cx,
);
pane.update_in(cx, |pane, window, cx| {
pane.close_active_item(&CloseActiveItem::default(), window, cx)
})
.unwrap()
.await
.unwrap();
pane.update_in(cx, |pane, window, cx| {
pane.navigate_backward(window, cx);
});
cx.run_until_parked();
pane.update(cx, |pane, cx| {
let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
open_editor.update(cx, |editor, cx| {
assert_eq!(
editor.display_text(cx),
"pub mod external {}",
"External file is open now",
);
});
});
assert_language_servers_count(
1,
"After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
cx,
);
cx.update(|_, cx| {
workspace::reload(&workspace::Reload::default(), cx);
});
assert_language_servers_count(
1,
"After reloading the worktree with local and external files opened, only one project should be started",
cx,
);
}
fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
point..point

View File

@@ -6393,7 +6393,6 @@ pub(crate) struct LineWithInvisibles {
font_size: Pixels,
}
#[allow(clippy::large_enum_variant)]
enum LineFragment {
Text(ShapedLine),
Element {

View File

@@ -884,6 +884,7 @@ impl InfoPopover {
*keyboard_grace = false;
cx.stop_propagation();
})
.p_2()
.when_some(self.parsed_content.clone(), |this, markdown| {
this.child(
div()
@@ -891,7 +892,6 @@ impl InfoPopover {
.overflow_y_scroll()
.max_w(max_size.width)
.max_h(max_size.height)
.p_2()
.track_scroll(&self.scroll_handle)
.child(
MarkdownElement::new(markdown, hover_markdown_style(window, cx))

View File

@@ -103,7 +103,7 @@ async fn lsp_task_context(
Some(TaskContext {
cwd: worktree_abs_path.map(|p| p.to_path_buf()),
project_env: project_env.unwrap_or_default(),
project_env,
..TaskContext::default()
})
}

View File

@@ -0,0 +1,27 @@
[package]
name = "environment"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/environment.rs"
doctest = false
[features]
test-support = []
[dependencies]
anyhow.workspace = true
collections.workspace = true
paths.workspace = true
smol.workspace = true
serde_json.workspace = true
tokio.workspace = true
util.workspace = true
which.workspace = true
workspace-hack.workspace = true

View File

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

View File

@@ -0,0 +1,161 @@
use anyhow::Result;
use collections::HashMap;
use std::path::Path;
use util::ResultExt;
/// The environment that Zed was launched with.
/// Prefer `in_home_dir` outside of a project context, or `in_dir` inside of a project context,
/// as these will be more like the user's shell environment.
pub fn inherited() -> HashMap<String, String> {
std::env::vars().collect()
}
/// The environment read from launching a login shell in the user's home directory.
pub async fn in_home_dir() -> HashMap<String, String> {
static HOME_ENV: tokio::sync::OnceCell<HashMap<String, String>> =
tokio::sync::OnceCell::const_new();
HOME_ENV
.get_or_init(|| async {
match in_dir(paths::home_dir(), false).await.log_err() {
Some(env) => env,
None => inherited(),
}
})
.await
.clone()
}
#[cfg(any(test, feature = "test-support"))]
pub async fn in_dir(_dir: &Path, _load_direnv: bool) -> Result<HashMap<String, String>> {
let fake_env = [("ZED_FAKE_TEST_ENV".into(), "true".into())]
.into_iter()
.collect();
Ok(fake_env)
}
#[cfg(all(target_os = "windows", not(any(test, feature = "test-support"))))]
pub async fn in_dir(_dir: &Path, _load_direnv: bool) -> Result<HashMap<String, String>> {
Ok(Default::default())
}
#[cfg(not(any(target_os = "windows", test, feature = "test-support")))]
/// The environment read from launching a login shell in a specific directory.
pub async fn in_dir(dir: &Path, load_direnv: bool) -> Result<HashMap<String, String>> {
use anyhow::Context;
use std::path::PathBuf;
use util::ResultExt;
use util::parse_env_output;
const MARKER: &str = "ZED_SHELL_START";
let shell = util::get_system_shell();
let shell_path = PathBuf::from(&shell);
let shell_name = shell_path.file_name().and_then(|f| f.to_str());
// What we're doing here is to spawn a shell and then `cd` into
// the project directory to get the env in there as if the user
// `cd`'d into it. We do that because tools like direnv, asdf, ...
// hook into `cd` and only set up the env after that.
//
// If the user selects `Direct` for direnv, it would set an environment
// variable that later uses to know that it should not run the hook.
// We would include in `.envs` call so it is okay to run the hook
// even if direnv direct mode is enabled.
//
// In certain shells we need to execute additional_command in order to
// trigger the behavior of direnv, etc.
let command = match shell_name {
Some("fish") => format!(
"cd '{}'; emit fish_prompt; printf '%s' {MARKER}; /usr/bin/env;",
dir.display()
),
_ => format!(
"cd '{}'; printf '%s' {MARKER}; /usr/bin/env;",
dir.display()
),
};
// csh/tcsh only supports `-l` if it's the only flag. So this won't be a login shell.
// Users must rely on vars from `~/.tcshrc` or `~/.cshrc` and not `.login` as a result.
let args = match shell_name {
Some("tcsh") | Some("csh") => vec!["-i".to_string(), "-c".to_string(), command],
_ => vec![
"-l".to_string(),
"-i".to_string(),
"-c".to_string(),
command,
],
};
let output = smol::unblock(move || {
util::set_pre_exec_to_start_new_session(std::process::Command::new(&shell).args(&args))
.output()
})
.await
.with_context(|| "Failed to spawn login shell to source login environment variables")?;
if !output.status.success() {
anyhow::bail!("Login shell exited with nonzero exit code.");
}
let stdout = String::from_utf8_lossy(&output.stdout);
let Some(env_output_start) = stdout.find(MARKER) else {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!(
"failed to parse output of `env` command in login shell. stdout: {:?}, stderr: {:?}",
stdout,
stderr
);
};
let mut parsed_env = HashMap::default();
let env_output = &stdout[env_output_start + MARKER.len()..];
parse_env_output(env_output, |key, value| {
parsed_env.insert(key, value);
});
if load_direnv {
if let Some(direnv) = load_direnv_environment(&parsed_env, dir).await.log_err() {
for (key, value) in direnv {
parsed_env.insert(key, value);
}
}
}
Ok(parsed_env)
}
#[cfg(not(any(target_os = "windows", test, feature = "test-support")))]
async fn load_direnv_environment(
env: &HashMap<String, String>,
dir: &Path,
) -> Result<HashMap<String, String>> {
let Ok(direnv_path) = which::which("direnv") else {
return Ok(HashMap::default());
};
let direnv_output = smol::process::Command::new(direnv_path)
.args(["export", "json"])
.envs(env)
.env("TERM", "dumb")
.current_dir(dir)
.output()
.await?;
if !direnv_output.status.success() {
anyhow::bail!(
"Loading direnv environment failed ({}), stderr: {}",
direnv_output.status,
String::from_utf8_lossy(&direnv_output.stderr)
);
}
let output = String::from_utf8_lossy(&direnv_output.stdout);
if output.is_empty() {
return Ok(HashMap::default());
}
Ok(serde_json::from_str(&output)?)
}

View File

@@ -31,6 +31,7 @@ clap.workspace = true
client.workspace = true
collections.workspace = true
dirs.workspace = true
environment.workspace = true
dotenv.workspace = true
env_logger.workspace = true
extension.workspace = true

View File

@@ -28,6 +28,17 @@ impl AssertionsReport {
}
}
pub fn error(msg: String) -> Self {
let assert = RanAssertion {
id: "no-unhandled-errors".into(),
result: Err(msg),
};
AssertionsReport {
ran: vec![assert],
max: Some(1),
}
}
pub fn is_empty(&self) -> bool {
self.ran.is_empty()
}
@@ -145,7 +156,9 @@ pub fn print_table_divider() {
}
fn truncate(assertion: &str, max_width: usize) -> String {
if assertion.len() <= max_width {
let is_verbose = std::env::var("VERBOSE").is_ok_and(|v| !v.is_empty());
if assertion.len() <= max_width || is_verbose {
assertion.to_string()
} else {
let mut end_ix = max_width - 1;

View File

@@ -6,7 +6,7 @@ mod ids;
mod instance;
mod tool_metrics;
use assertions::display_error_row;
use assertions::{AssertionsReport, display_error_row};
use instance::{ExampleInstance, JudgeOutput, RunOutput, run_git};
pub(crate) use tool_metrics::*;
@@ -417,7 +417,7 @@ pub fn init(cx: &mut App) -> Arc<AgentAppState> {
tx.send(Some(options)).log_err();
})
.detach();
let node_runtime = NodeRuntime::new(client.http_client(), None, rx);
let node_runtime = NodeRuntime::new(client.http_client(), rx);
let extension_host_proxy = ExtensionHostProxy::global(cx);
@@ -467,11 +467,12 @@ pub fn find_model(
match matching_models.as_slice() {
[model] => Ok(model.clone()),
[] => Err(anyhow!(
"No language model with ID {} was available. Available models: {}",
"No language model with ID {}/{} was available. Available models: {}",
provider_id,
model_id,
model_registry
.available_models(cx)
.map(|model| model.id().0.clone())
.map(|model| format!("{}/{}", model.provider_id().0, model.id().0))
.collect::<Vec<_>>()
.join(", ")
)),
@@ -581,12 +582,15 @@ fn print_report(
Err(err) => {
display_error_row(&mut table_rows, example.repetition, err.to_string())?;
error_count += 1;
programmatic_scores.push(0.0);
diff_scores.push(0.0);
thread_scores.push(0.0);
}
Ok((run_output, judge_output)) => {
cumulative_tool_metrics.merge(&run_output.tool_metrics);
example_cumulative_tool_metrics.merge(&run_output.tool_metrics);
if !run_output.programmatic_assertions.total_count() > 0 {
if run_output.programmatic_assertions.total_count() > 0 {
for assertion in &run_output.programmatic_assertions.ran {
assertions::display_table_row(
&mut table_rows,
@@ -626,6 +630,8 @@ fn print_report(
}
}
let mut all_asserts = Vec::new();
if !table_rows.is_empty() {
assertions::print_table_header();
print!("{}", table_rows);
@@ -634,33 +640,29 @@ fn print_report(
for (example, result) in results.iter() {
if let Ok((run_output, judge_output)) = result {
let asserts = [
run_output.programmatic_assertions.clone(),
judge_output.diff.clone(),
judge_output.thread.clone(),
];
all_asserts.extend_from_slice(&asserts);
assertions::print_table_round_summary(
&example.repetition.to_string(),
[
&run_output.programmatic_assertions,
&judge_output.diff,
&judge_output.thread,
]
.into_iter(),
asserts.iter(),
)
} else if let Err(err) = result {
let assert = AssertionsReport::error(err.to_string());
all_asserts.push(assert.clone());
assertions::print_table_round_summary(
&example.repetition.to_string(),
[assert].iter(),
)
}
}
assertions::print_table_divider();
assertions::print_table_round_summary(
"avg",
results.iter().flat_map(|(_, result)| {
result.iter().flat_map(|(run_output, judge_output)| {
[
&run_output.programmatic_assertions,
&judge_output.diff,
&judge_output.thread,
]
.into_iter()
})
}),
);
assertions::print_table_round_summary("avg", all_asserts.iter());
assertions::print_table_footer();
}
@@ -711,9 +713,9 @@ fn print_report(
.values()
.flat_map(|results| {
results.iter().map(|(example, _)| {
let absolute_path = example.run_directory.join("last.messages.json");
pathdiff::diff_paths(&absolute_path, run_dir)
.unwrap_or_else(|| absolute_path.clone())
let absolute_path = run_dir.join(example.run_directory.join("last.messages.json"));
let cwd = std::env::current_dir().expect("Can't get current dir");
pathdiff::diff_paths(&absolute_path, cwd).unwrap_or_else(|| absolute_path.clone())
})
})
.collect::<Vec<_>>();

View File

@@ -48,6 +48,7 @@ pub struct ExampleMetadata {
pub language_server: Option<LanguageServer>,
pub max_assertions: Option<usize>,
pub profile_id: AgentProfileId,
pub existing_thread_json: Option<String>,
}
#[derive(Clone, Debug)]
@@ -477,12 +478,16 @@ impl Response {
tool_name: &'static str,
cx: &mut ExampleContext,
) -> Result<&ToolUse> {
let result = self.messages.iter().find_map(|msg| {
let result = self.find_tool_call(tool_name);
cx.assert_some(result, format!("called `{}`", tool_name))
}
pub fn find_tool_call(&self, tool_name: &str) -> Option<&ToolUse> {
self.messages.iter().rev().find_map(|msg| {
msg.tool_use
.iter()
.find(|tool_use| tool_use.name == tool_name)
});
cx.assert_some(result, format!("called `{}`", tool_name))
})
}
#[allow(dead_code)]

View File

@@ -21,6 +21,7 @@ impl Example for AddArgToTraitMethod {
}),
max_assertions: None,
profile_id: AgentProfileId::default(),
existing_thread_json: None,
}
}

View File

@@ -22,6 +22,7 @@ impl Example for CodeBlockCitations {
}),
max_assertions: None,
profile_id: AgentProfileId::default(),
existing_thread_json: None,
}
}

View File

@@ -1,7 +1,7 @@
use crate::example::{Example, ExampleContext, ExampleMetadata, JudgeAssertion};
use anyhow::Result;
use assistant_settings::AgentProfileId;
use assistant_tools::EditFileToolInput;
use assistant_tools::{EditFileMode, EditFileToolInput};
use async_trait::async_trait;
pub struct CommentTranslation;
@@ -16,6 +16,7 @@ impl Example for CommentTranslation {
language_server: None,
max_assertions: Some(1),
profile_id: AgentProfileId::default(),
existing_thread_json: None,
}
}
@@ -35,7 +36,7 @@ impl Example for CommentTranslation {
for tool_use in thread.tool_uses_for_message(message.id, cx) {
if tool_use.name == "edit_file" {
let input: EditFileToolInput = serde_json::from_value(tool_use.input)?;
if input.create_or_overwrite {
if !matches!(input.mode, EditFileMode::Edit) {
create_or_overwrite_count += 1;
}
}

View File

@@ -18,6 +18,7 @@ impl Example for FileSearchExample {
language_server: None,
max_assertions: Some(3),
profile_id: AgentProfileId::default(),
existing_thread_json: None,
}
}

View File

@@ -16,6 +16,7 @@ mod add_arg_to_trait_method;
mod code_block_citations;
mod comment_translation;
mod file_search;
mod overwrite_file;
mod planets;
pub fn all(examples_dir: &Path) -> Vec<Rc<dyn Example>> {
@@ -25,6 +26,7 @@ pub fn all(examples_dir: &Path) -> Vec<Rc<dyn Example>> {
Rc::new(code_block_citations::CodeBlockCitations),
Rc::new(planets::Planets),
Rc::new(comment_translation::CommentTranslation),
Rc::new(overwrite_file::FileOverwriteExample),
];
for example_path in list_declarative_examples(examples_dir).unwrap() {
@@ -45,6 +47,7 @@ impl DeclarativeExample {
pub fn load(example_path: &Path) -> Result<Self> {
let name = Self::name_from_path(example_path);
let base: ExampleToml = toml::from_str(&fs::read_to_string(&example_path)?)?;
let example_dir = example_path.parent().unwrap();
let language_server = if base.require_lsp {
Some(crate::example::LanguageServer {
@@ -63,6 +66,14 @@ impl DeclarativeExample {
AgentProfileId::default()
};
let existing_thread_json = if let Some(path) = base.existing_thread_path {
let content = fs::read_to_string(example_dir.join(&path))
.unwrap_or_else(|_| panic!("Failed to read existing thread file: {}", path));
Some(content)
} else {
None
};
let metadata = ExampleMetadata {
name,
url: base.url,
@@ -70,6 +81,7 @@ impl DeclarativeExample {
language_server,
max_assertions: None,
profile_id,
existing_thread_json,
};
Ok(DeclarativeExample {
@@ -110,6 +122,8 @@ pub struct ExampleToml {
pub diff_assertions: BTreeMap<String, String>,
#[serde(default)]
pub thread_assertions: BTreeMap<String, String>,
#[serde(default)]
pub existing_thread_path: Option<String>,
}
#[async_trait(?Send)]

View File

@@ -0,0 +1,49 @@
use anyhow::Result;
use assistant_settings::AgentProfileId;
use assistant_tools::{EditFileMode, EditFileToolInput};
use async_trait::async_trait;
use crate::example::{Example, ExampleContext, ExampleMetadata};
pub struct FileOverwriteExample;
/*
This eval tests a fix for a destructive behavior of the `edit_file` tool.
Previously, it would rewrite existing files too aggressively, which often
resulted in content loss.
Pass rate before the fix: 10%
Pass rate after the fix: 100%
*/
#[async_trait(?Send)]
impl Example for FileOverwriteExample {
fn meta(&self) -> ExampleMetadata {
let thread_json = include_str!("threads/overwrite-file.json");
ExampleMetadata {
name: "file_overwrite".to_string(),
url: "https://github.com/zed-industries/zed.git".to_string(),
revision: "023a60806a8cc82e73bd8d88e63b4b07fc7a0040".to_string(),
language_server: None,
max_assertions: Some(1),
profile_id: AgentProfileId::default(),
existing_thread_json: Some(thread_json.to_string()),
}
}
async fn conversation(&self, cx: &mut ExampleContext) -> Result<()> {
let response = cx.run_turns(1).await?;
let file_overwritten = if let Some(tool_use) = response.find_tool_call("edit_file") {
let input = tool_use.parse_input::<EditFileToolInput>()?;
match input.mode {
EditFileMode::Edit => false,
EditFileMode::Create | EditFileMode::Overwrite => true,
}
} else {
false
};
cx.assert(!file_overwritten, "File should be edited, not overwritten")
}
}

View File

@@ -18,6 +18,7 @@ impl Example for Planets {
language_server: None,
max_assertions: None,
profile_id: AgentProfileId::default(),
existing_thread_json: None,
}
}

View File

@@ -0,0 +1,262 @@
{
"completion_mode": "normal",
"cumulative_token_usage": {
"cache_creation_input_tokens": 18383,
"cache_read_input_tokens": 97250,
"input_tokens": 45,
"output_tokens": 776
},
"detailed_summary_state": "NotGenerated",
"exceeded_window_error": null,
"initial_project_snapshot": {
"timestamp": "2025-05-08T14:31:16.701157512Z",
"unsaved_buffer_paths": [],
"worktree_snapshots": [
{
"git_state": {
"current_branch": null,
"diff": "diff --git a/crates/language_model_selector/src/language_model_selector.rs b/crates/language_model_selector/src/language_model_selector.rs\nindex 6775bee98a..e25c9e1415 100644\n--- a/crates/language_model_selector/src/language_model_selector.rs\n+++ b/crates/language_model_selector/src/language_model_selector.rs\n@@ -410,7 +410,8 @@ impl ModelMatcher {\n }\n \n pub fn is_match(self: &Self, info: &ModelInfo) -> bool {\n- self.matched_ids.contains(&info.model.id().0)\n+ let q = (info.model.provider_id(), info.model.id());\n+ self.matched_models.contains(&q)\n }\n }\n \n",
"head_sha": "9245656485e58a5d6d717d82209bc8c57cb9c539",
"remote_url": "git@github.com:zed-industries/zed.git"
},
"worktree_path": "/home/silver/develop/zed"
}
]
},
"messages": [
{
"context": "\n<context>\nThe following items were attached by the user. They are up-to-date and don't need to be re-read.\n\n<files>\n```rs zed/crates/language_model_selector/src/language_model_selector.rs\nconst TRY_ZED_PRO_URL [L28]\ntype OnModelChanged [L30]\ntype GetActiveModel [L31]\npub struct LanguageModelSelector [L33-37]\n picker [L34]\n _authenticate_all_providers_task [L35]\n _subscriptions [L36]\nimpl LanguageModelSelector [L39-231]\n pub fn new [L40-81]\n fn handle_language_model_registry_event [L83-104]\n fn authenticate_all_providers [L110-154]\n fn all_models [L156-204]\n pub fn active_model [L206-208]\n fn get_active_model_index [L210-230]\nimpl EventEmitter<DismissEvent> for LanguageModelSelector [L233]\nimpl Focusable for LanguageModelSelector [L235-239]\n fn focus_handle [L236-238]\nimpl Render for LanguageModelSelector [L241-245]\n fn render [L242-244]\npub struct LanguageModelSelectorPopoverMenu [L248-258]\n language_model_selector [L253]\n trigger [L254]\n tooltip [L255]\n handle [L256]\n anchor [L257]\nimpl LanguageModelSelectorPopoverMenu<T, TT> [L260-284]\n pub fn new [L265-278]\n pub fn with_handle [L280-283]\nimpl RenderOnce for LanguageModelSelectorPopoverMenu<T, TT> [L286-304]\n fn render [L291-303]\nstruct ModelInfo [L307-310]\n model [L308]\n icon [L309]\npub struct LanguageModelPickerDelegate [L312-319]\n language_model_selector [L313]\n on_model_changed [L314]\n get_active_model [L315]\n all_models [L316]\n filtered_entries [L317]\n selected_index [L318]\nstruct GroupedModels [L321-324]\n recommended [L322]\n other [L323]\nimpl GroupedModels [L326-385]\n pub fn new [L327-342]\n fn entries [L344-370]\n fn model_infos [L372-384]\nenum LanguageModelPickerEntry [L387-390]\n Model [L388]\n Separator [L389]\nstruct ModelMatcher [L392-396]\n models [L393]\n bg_executor [L394]\n candidates [L395]\nimpl ModelMatcher [L398-452]\n fn new [L399-406]\n pub fn fuzzy_search [L408-424]\n pub fn exact_search [L426-438]\n fn make_match_candidates [L440-451]\nimpl PickerDelegate for LanguageModelPickerDelegate [L454-693]\n type ListItem [L455]\n fn match_count [L457-459]\n fn selected_index [L461-463]\n fn set_selected_index [L465-468]\n fn can_select [L470-480]\n fn placeholder_text [L482-484]\n fn update_matches [L486-546]\n fn confirm [L548-560]\n fn dismissed [L562-566]\n fn render_match [L568-636]\n fn render_footer [L638-692]\n\nShowing symbols 1-71 (total symbols: 71)\n\n```\n</files>\n</context>\n",
"creases": [
{
"end": 140,
"icon_path": "icons/file_icons/rust.svg",
"label": "language_model_selector.rs",
"start": 38
}
],
"id": 0,
"role": "user",
"segments": [
{
"text": "I recently introduced fuzzy search in [@language_model_selector.rs](@file:zed/crates/language_model_selector/src/language_model_selector.rs). Can you write unit-test that check it? You can find a test list of models on images. ",
"type": "text"
}
],
"tool_results": [],
"tool_uses": []
},
{
"context": "",
"creases": [],
"id": 1,
"role": "assistant",
"segments": [
{
"text": "I'll help you write unit tests for the fuzzy search functionality in the language model selector. I see that you want to test the fuzzy search with the models shown in the images - both Claude models (3, 3.5, 3.7) and GPT models (GPT-3.5, GPT-4, GPT-4o, etc.).\n\nLet's start by examining the code structure to understand how the fuzzy search works.",
"type": "text"
}
],
"tool_results": [
{
"content": "zed/crates/language_model_selector/Cargo.toml\nzed/crates/language_model_selector/LICENSE-GPL\nzed/crates/language_model_selector/src\n",
"is_error": false,
"output": null,
"tool_use_id": "toolu_01FvYMtHs763GL1wA5mpTpqn"
}
],
"tool_uses": [
{
"id": "toolu_01FvYMtHs763GL1wA5mpTpqn",
"input": {
"path": "zed/crates/language_model_selector"
},
"name": "list_directory"
}
]
},
{
"context": "",
"creases": [],
"id": 2,
"role": "assistant",
"segments": [
{
"text": "Let's look at the structure of the tests directory or see if there are existing tests:",
"type": "text"
}
],
"tool_results": [
{
"content": "zed/crates/language_model_selector/src/language_model_selector.rs\n",
"is_error": false,
"output": null,
"tool_use_id": "toolu_01KSLDsLLrf7tgixB1sge3cm"
}
],
"tool_uses": [
{
"id": "toolu_01KSLDsLLrf7tgixB1sge3cm",
"input": {
"path": "zed/crates/language_model_selector/src"
},
"name": "list_directory"
}
]
},
{
"context": "",
"creases": [],
"id": 3,
"role": "assistant",
"segments": [
{
"text": "Let's check the Cargo.toml to understand the dependencies:",
"type": "text"
}
],
"tool_results": [
{
"content": "[package]\nname = \"language_model_selector\"\nversion = \"0.1.0\"\nedition.workspace = true\npublish.workspace = true\nlicense = \"GPL-3.0-or-later\"\n\n[lints]\nworkspace = true\n\n[lib]\npath = \"src/language_model_selector.rs\"\n\n[dependencies]\ncollections.workspace = true\nfeature_flags.workspace = true\nfuzzy.workspace = true\ngpui.workspace = true\nlanguage_model.workspace = true\nlog.workspace = true\npicker.workspace = true\nproto.workspace = true\nui.workspace = true\nworkspace-hack.workspace = true\nzed_actions.workspace = true\n",
"is_error": false,
"output": null,
"tool_use_id": "toolu_019Je2MLfJhpJr93g5igoRAH"
}
],
"tool_uses": [
{
"id": "toolu_019Je2MLfJhpJr93g5igoRAH",
"input": {
"path": "zed/crates/language_model_selector/Cargo.toml"
},
"name": "read_file"
}
]
},
{
"context": "",
"creases": [],
"id": 4,
"role": "assistant",
"segments": [
{
"text": "Now let's look at the fuzzy search implementation in detail:",
"type": "text"
}
],
"tool_results": [
{
"content": "struct ModelMatcher {\n models: Vec<ModelInfo>,\n bg_executor: BackgroundExecutor,\n candidates: Vec<StringMatchCandidate>,\n}\n\nimpl ModelMatcher {\n fn new(models: Vec<ModelInfo>, bg_executor: BackgroundExecutor) -> ModelMatcher {\n let candidates = Self::make_match_candidates(&models);\n Self {\n models,\n bg_executor,\n candidates,\n }\n }\n\n pub fn fuzzy_search(&self, query: &str) -> Vec<ModelInfo> {\n let matches = self.bg_executor.block(match_strings(\n &self.candidates,\n &query,\n false,\n 100,\n &Default::default(),\n self.bg_executor.clone(),\n ));\n\n let matched_models: Vec<_> = matches\n .into_iter()\n .map(|m| self.models[m.candidate_id].clone())\n .collect();\n\n matched_models\n }\n\n pub fn exact_search(&self, query: &str) -> Vec<ModelInfo> {\n self.models\n .iter()\n .filter(|m| {\n m.model\n .name()\n .0\n .to_lowercase()\n .contains(&query.to_lowercase())\n })\n .cloned()\n .collect::<Vec<_>>()\n }\n\n fn make_match_candidates(model_infos: &Vec<ModelInfo>) -> Vec<StringMatchCandidate> {\n model_infos\n .iter()\n .enumerate()\n .map(|(index, model)| {\n StringMatchCandidate::new(\n index,\n &format!(\"{}/{}\", &model.model.provider_id().0, &model.model.name().0),\n )\n })\n .collect::<Vec<_>>()\n }\n}",
"is_error": false,
"output": null,
"tool_use_id": "toolu_01GpKFEn6hYrXGiS5yBF3urk"
}
],
"tool_uses": [
{
"id": "toolu_01GpKFEn6hYrXGiS5yBF3urk",
"input": {
"end_line": 452,
"path": "zed/crates/language_model_selector/src/language_model_selector.rs",
"start_line": 392
},
"name": "read_file"
}
]
},
{
"context": "",
"creases": [],
"id": 5,
"role": "assistant",
"segments": [
{
"text": "Let's now check if there's an existing test module:",
"type": "text"
}
],
"tool_results": [
{
"content": "No matches found",
"is_error": false,
"output": null,
"tool_use_id": "toolu_01RZAvFvpMq16Dwh6d6cnVzy"
}
],
"tool_uses": [
{
"id": "toolu_01RZAvFvpMq16Dwh6d6cnVzy",
"input": {
"include_pattern": "zed/crates/language_model_selector/**/*.rs",
"regex": "mod tests"
},
"name": "grep"
}
]
},
{
"context": "",
"creases": [],
"id": 6,
"role": "assistant",
"segments": [
{
"text": "Now I'll create a test module for the ModelMatcher's fuzzy search functionality. Let's create this test module:",
"type": "text"
}
],
"tool_results": [],
"tool_uses": []
}
],
"model": {
"model": "claude-3-7-sonnet-latest",
"provider": "zed.dev"
},
"profile": "Write",
"request_token_usage": [
{
"cache_creation_input_tokens": 9581,
"input_tokens": 4,
"output_tokens": 5
},
{
"cache_creation_input_tokens": 204,
"cache_read_input_tokens": 9581,
"input_tokens": 6,
"output_tokens": 5
},
{
"cache_creation_input_tokens": 104,
"cache_read_input_tokens": 9785,
"input_tokens": 6,
"output_tokens": 5
},
{
"cache_creation_input_tokens": 239,
"cache_read_input_tokens": 9889,
"input_tokens": 6,
"output_tokens": 5
},
{
"cache_creation_input_tokens": 569,
"cache_read_input_tokens": 10128,
"input_tokens": 5,
"output_tokens": 5
},
{
"cache_creation_input_tokens": 87,
"cache_read_input_tokens": 10697,
"input_tokens": 5,
"output_tokens": 2
},
{
"cache_creation_input_tokens": 7355,
"cache_read_input_tokens": 10784,
"input_tokens": 5,
"output_tokens": 3
}
],
"summary": "Fuzzy Search Testing Language Model Selector",
"updated_at": "2025-05-08T18:20:34.205405751Z",
"version": "0.2.0"
}

View File

@@ -1,4 +1,4 @@
use agent::{Message, MessageSegment, ThreadStore};
use agent::{Message, MessageSegment, SerializedThread, ThreadStore};
use anyhow::{Context, Result, anyhow, bail};
use assistant_tool::ToolWorkingSet;
use client::proto::LspWorkProgress;
@@ -312,7 +312,14 @@ impl ExampleInstance {
thread_store.update(cx, |thread_store, cx| thread_store.load_profile_by_id(profile_id, cx)).expect("Failed to load profile");
let thread =
thread_store.update(cx, |thread_store, cx| thread_store.create_thread(cx))?;
thread_store.update(cx, |thread_store, cx| {
if let Some(json) = &meta.existing_thread_json {
let serialized = SerializedThread::from_json(json.as_bytes()).expect("Can't read serialized thread");
thread_store.create_thread_from_serialized(serialized, cx)
} else {
thread_store.create_thread(cx)
}
})?;
thread.update(cx, |thread, _cx| {
@@ -795,7 +802,7 @@ pub fn repo_path_for_url(repos_dir: &Path, repo_url: &str) -> PathBuf {
}
pub async fn run_git(repo_path: &Path, args: &[&str]) -> Result<String> {
let output = new_smol_command("git")
let output = new_smol_command("git", &environment::inherited())
.current_dir(repo_path)
.args(args)
.output()

View File

@@ -18,6 +18,7 @@ async-tar.workspace = true
async-trait.workspace = true
collections.workspace = true
dap.workspace = true
environment.workspace = true
fs.workspace = true
futures.workspace = true
gpui.workspace = true
@@ -33,6 +34,7 @@ serde_json.workspace = true
task.workspace = true
toml.workspace = true
util.workspace = true
wasi-preview1-component-adapter-provider.workspace = true
wasm-encoder.workspace = true
wasmparser.workspace = true
wit-component.workspace = true

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