Compare commits

...

79 Commits

Author SHA1 Message Date
Peter Tripp
94b6e7f56b Whoops 2025-05-14 11:22:39 +02:00
Peter Tripp
ed9488a98a Support global system settings: /etc/zed/settings.json 2025-05-14 10:20:55 +02:00
Michael Sloan
25cc05b45c Use Vec instead of SmallVec for glyphs field of ShapedRun (#30664)
This glyphs field is usually larger than 8 elements, and SmallVec is not
efficient when it cannot store the value inline.

This change also adds precise glyphs run preallocation in some places
`ShapedRun` is constructed.

Release Notes:

- N/A
2025-05-14 07:02:38 +00:00
Agus Zubiaga
a4766e296f Add tool result image support to Gemini models (#30647)
Release Notes:

- Add tool result image support to Gemini models
2025-05-14 00:51:31 +00:00
Cole Miller
2f26a860a9 debugger: Fix focus nits (#30547)
- Focus the console's query bar (if it exists) when focusing the console
- Fix incorrect focus handles used for the console and terminal at the
`Subview` level

Release Notes:

- N/A

Co-authored-by: Piotr <piotr@zed.dev>
Co-authored-by: Anthony <anthony@zed.dev>
2025-05-13 22:52:03 +00:00
Anthony Eid
f1fe505649 debugger: Show language icons in debug scenario picker (#30662)
We attempt to resolve the language name in this order

1. Based on debug adapter if they're for a singular language e.g. Delve
2. File extension if it exists
3. If a language name exists within a debug scenario's label

In the future I want to use locators to also determine the language as
well and refresh scenario list when a new scenario has been saved

Release Notes:

- N/A
2025-05-14 00:50:58 +02:00
Piotr Osiewicz
9826b7b5c1 debugger: Add extensions support (#30625)
Closes #ISSUE

Release Notes:

- N/A

---------

Co-authored-by: Anthony <anthony@zed.dev>
2025-05-13 22:42:51 +00:00
Michael Sloan
6fc9036063 Multi-glyph text runs on Linux (#30660)
Release Notes:

- N/A
2025-05-13 22:10:35 +00:00
Bennet Bo Fenner
2b74163a48 context_editor: Allow copying entire line when selection is empty (#30612)
Closes #27879

Release Notes:

- Allow copying entire line when selection is empty in text threads
2025-05-13 21:23:19 +00:00
Michael Sloan
71ea7aee3b Misc optimization/cleanup of use of Cosmic Text on Linux (#30658)
* Use cosmic_text `metadata` attr to write down the `FontId` from the
input run to avoid searching the list of fonts when laying out every
glyph.

* Instead of checking on every glyph if `postscript_name` is an emoji
font, just store `is_known_emoji_font`.

* Clarify why `font_id_for_cosmic_id` is used, and when its use is
valid.

Release Notes:

- N/A
2025-05-13 21:20:41 +00:00
Remco Smits
48b376fdc9 debugger: Fix nits (#30632)
Release Notes:

- N/A

---------

Co-authored-by: Anthony Eid <hello@anthonyeid.me>
2025-05-13 23:13:02 +02:00
Smit Barmase
f98c6fb2cf Update panels serialization from global to per-workspace (#30652)
Closes #27834

This PR changes project panel, outline panel and collab panel
serialization from global to per-workspace, so configurations are
restored only within the same workspace. Handles remote workspaces too.
Opening a new window will start with a fresh panel defaults e.g. width.

Release Notes:

- Improved project panel, outline panel, and collab panel to persist
width on a per-workspace basis. New windows will use the width specified
in the `default_width` setting.
2025-05-14 00:05:42 +05:30
Stanislav Alekseev
1ace5a27bc editor: Fix signature hover popover incorrect width instead of adapting to its content (#30646)
Before:
<img width="935" alt="Screenshot 2025-05-13 at 18 03 21"
src="https://github.com/user-attachments/assets/5320e559-7c60-4ad6-8ab6-99dcbcd1d42e"
/>

After:
<img width="349" alt="Screenshot 2025-05-13 at 18 45 21"
src="https://github.com/user-attachments/assets/98412e13-b879-490a-a1b4-88f97bb84774"
/>
----

Release Notes:

- Fixed issue where signature popover displayed at incorrect width
instead of adapting to its content.

----
cc @smitbarmase
2025-05-13 21:34:11 +05:30
Agus Zubiaga
dd6594621f Add image input support for OpenAI models (#30639)
Release Notes:

- Added input image support for OpenAI models
2025-05-13 17:32:42 +02:00
Anthony Eid
68afe4fdda debugger: Add stack frame multibuffer (#30395)
This PR adds the ability to expand a debugger stack trace into a multi
buffer and view each frame as it's own excerpt.

Release Notes:

- N/A

---------

Co-authored-by: Remco Smits <djsmits12@gmail.com>
2025-05-13 14:55:05 +00:00
Conrad Irwin
6f297132b4 Fix docs on remote extensions (#30631)
Closes #17021

This was implemented a while ago, but I never updated the docs. Sorry.

Release Notes:

- N/A
2025-05-13 16:50:46 +02:00
Joseph T. Lyons
8fe134e361 Add a debugger issue template (#30638)
Release Notes:

- N/A
2025-05-13 13:27:03 +00:00
张小白
7aabbb0426 windows: Properly handle dead char (#30629)
Release Notes:

- N/A
2025-05-13 12:50:21 +00:00
Conrad Irwin
85c6a3dd0c Always have Enter submit in the debug console (#30564)
Release Notes:

- N/A
2025-05-13 14:26:20 +02:00
Conrad Irwin
81dcc12c62 Remove request timeout from DAP (#30567)
Release Notes:

- N/A
2025-05-13 14:25:52 +02:00
Conrad Irwin
1fd8fbe6d1 Show tasks in debugger: start (#30584)
- **Show relevant tasks in debugger: start**
- **Add history too**

Closes #ISSUE

Release Notes:

- N/A

---------

Co-authored-by: Cole <cole@zed.dev>
Co-authored-by: Anthony <anthony@zed.dev>
2025-05-13 14:25:37 +02:00
Smit Barmase
7eb226b3fc docs: Add docs for hover_popover_delay and update hover delay (#30620)
- Add docs for `hover_popover_delay`.
- Set `hover_popover_delay` to `300` from `350` which matches [VSCode's
hover
delay](ed48873ba2/src/vs/editor/common/config/editorOptions.ts (L2219)).

Release Notes:

- Added `hover_popover_delay` to settings which determines time to wait
in milliseconds before showing the informational hover box.
2025-05-13 16:22:28 +05:30
张小白
9426caa061 windows: Implement keyboard_layout_change (#30624)
Part of #29144

Release Notes:

- N/A
2025-05-13 18:02:56 +08:00
Marshall Bowers
7cad943fde agent: Remove unused max monthly spend reached error (#30615)
This PR removes the code for showing the max monthly spend limit reached
error, as it is no longer used.

Release Notes:

- N/A
2025-05-13 09:43:13 +00:00
张小白
29da105dd5 windows: Fix ModifiersChanged event (#30617)
Follow-up #30574

Release Notes:

- N/A
2025-05-13 09:05:24 +00:00
Richard Feldman
8fdf309a4a Have read_file support images (#30435)
This is very basic support for them. There are a number of other TODOs
before this is really a first-class supported feature, so not adding any
release notes for it; for now, this PR just makes it so that if
read_file tries to read a PNG (which has come up in practice), it at
least correctly sends it to Anthropic instead of messing up.

This also lays the groundwork for future PRs for more first-class
support for images in tool calls across more image file formats and LLM
providers.

Release Notes:

- N/A

---------

Co-authored-by: Agus Zubiaga <hi@aguz.me>
Co-authored-by: Agus Zubiaga <agus@zed.dev>
2025-05-13 10:58:00 +02:00
Stanislav Alekseev
f01af006e1 Update nixpkgs, add direnv to gitignore (#30292)
This also moves nixpkgs to use `channels.nixos.org` since those tarballs
are 30mb in size as compared to 45mb github ones

Release Notes:

- N/A 

----

cc @P1n3appl3
2025-05-13 01:45:22 -07:00
Kirill Bulatov
01488c4f91 Fix project search focus not toggling between query and results on ESC (#30613)
Before:


https://github.com/user-attachments/assets/dc5b7ab3-b9bc-4aa3-9f0c-1694c41ec7e7

After:


https://github.com/user-attachments/assets/8087004e-c1fd-4390-9f79-b667e8ba874b


Release Notes:

- Fixed project search focus not toggling between query and results on
ESC
2025-05-13 08:36:18 +00:00
Marshall Bowers
18e911002f zed_extension_api: Fork new version of extension API (#30611)
This PR forks a new version of the `zed_extension_api` in preparation
for new changes.

Release Notes:

- N/A
2025-05-13 08:35:15 +00:00
Kirill Bulatov
54c6d482b6 Remove the minimap from the debugger console (#30610)
Follow-up of https://github.com/zed-industries/zed/pull/26893

Release Notes:

- N/A
2025-05-13 08:09:38 +00:00
Conrad Irwin
32c7fcd78c Fix panic double clicking on debugger resize handle (#30569)
Closes #ISSUE

Co-Authored-By: Cole <cole@zed.dev>

Release Notes:

- N/A
2025-05-13 09:55:54 +02:00
Anthony Eid
fff349a644 debugger: Update new session modal custom view (#30587)
Paths now assume that you're in the cwd if they don't start with a ~ or
/.

Release Notes:

- N/A
2025-05-13 09:47:39 +02:00
Tristan Hume
90c2d17042 Implement global settings file (#30444)
Adds a `global_settings.json` file which can be set up by enterprises
with automation, enabling setting settings like edit provider by default
without interfering with user's settings files.

Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <git@maxdeviant.com>
2025-05-13 08:29:32 +02:00
Julia Ryan
c6e69fae17 Don't parse windows commandlines in debugger launch (#30586)
Release Notes:

- N/A
2025-05-12 22:43:11 -07:00
Smit Barmase
e5d497ee08 editor: Improve snippet completion to show key inline in completion and description as aside (#30603)
Closes #28028

Before:
<img width="742" alt="image"
src="https://github.com/user-attachments/assets/31723970-5420-40ea-a394-4ffa0038925c"
/>

After:
<img width="989" alt="image"
src="https://github.com/user-attachments/assets/0aebc317-a234-4e68-8304-cb479513af15"
/>


Release Notes:

- Improved snippet code completion to show key in completion menu and
description in aside.
2025-05-13 05:28:59 +05:30
Smit Barmase
229f3dab22 editor: Do not show document highlights when selection is spanned more than word (#30602)
Closes #27743

This PR prevents document highlighting when selection start and
selection end do not point to the same word. This is useful in cases
when you select multiple lines or multiple words, in which case you
don't really care about these LSP-specific highlights. This is the same
behavior as VSCode.


https://github.com/user-attachments/assets/f80d6ca3-d5c8-4d7b-9281-c1d6dc6a6e7b

Release Notes:

- Fixed document highlight behavior so it no longer appears when
selecting multiple words or lines, making text selection and selection
highlights more clearer.
2025-05-13 05:15:06 +05:30
Smit Barmase
67f9da0846 editor: Fix code completions menu flashing due variable width (#30598)
Closes #27631

We use `widest_completion_ix` to figure out completion menu width. This
results in flickering between frames as more information about
completion items, such as signatures, is populated asynchronously. There
is no way to know this width or which item will be widest beforehand.
While using a hardcoded value feels like a backward approach, it results
in a far smoother experience. VSCode also uses fixed width for
completion menu.

Before:


https://github.com/user-attachments/assets/0f044bae-fae9-43dc-8d4a-d8e7be8be6c4

After:


https://github.com/user-attachments/assets/21ab475c-7331-4de3-bb01-3986182fc9e4

Release Notes:

- Fixed issue where code completion menu would flicker while typing.
2025-05-13 05:14:48 +05:30
Ben Kunkle
ab455e1c43 Deny unknown keys in settings in JSON schema so user gets warnings but settings still parses (#30583)
Closes #ISSUE

Release Notes:

- Improved checking of Zed settings so that unrecognized keys show
warnings while editing them
2025-05-12 17:48:36 -04:00
Michael Sloan
986d271ea7 Fix panic in linux text rendering + refactor to avoid similar errors (#30601)
See #27808. `font_id_for_cosmic_id` was another path updated
`loaded_fonts_store` but did not push to `features_store`. Solution is
just to have one `Vec` with fields rather than relying on the indices
matching up

Release Notes:

- N/A
2025-05-12 21:41:14 +00:00
Conrad Irwin
98a18e04f7 Fix conflict indices (#30585)
Release Notes:

- Fix a bug where python path could be corrupted
2025-05-12 21:52:45 +02:00
Bennet Bo Fenner
3ea86da16f Copilot fix o1 model (#30581)
Release Notes:

- Fixed an issue where the `o1` model would not work when using Copilot
Chat
2025-05-12 15:27:24 +00:00
Bennet Bo Fenner
3173f87dc3 agent: Restore find path tool card after restart (#30580)
Release Notes:

- N/A
2025-05-12 17:11:40 +02:00
Ron Harel
6592314984 editor: Trim indent guides at last non-empty line (#29482)
Closes #26274

Adjust the end position of indent guides to prevent them from extending
through empty space.
Also corrected old test values ​​that seemed to have adapted to the
indentation's behavior.

Release Notes:

- Fixed indentation guides extending beyond the final scope in a file.
2025-05-12 17:04:46 +02:00
THELOSTSOUL
93b6fdb8e5 assistant_tools: Make terminal tool work on Windows (#30497)
Release Notes:

- N/A
2025-05-12 23:02:33 +08:00
Bennet Bo Fenner
e79d1b27b1 agent: Restore web search tool card after restart (#30578)
Release Notes:

- N/A
2025-05-12 14:59:38 +00:00
Smit Barmase
1a0eedb787 Fix migrate banner not showing markdown on file changes (#30575)
Fixes case where on file (settings/keymap) changes banner would appear
but markdown was not visible.
Regression caused by refactor happened in
https://github.com/zed-industries/zed/pull/30456.

Release Notes:

- N/A
2025-05-12 20:16:18 +05:30
Umesh Yadav
8db0333b04 Fix out-of-bounds panic in fuzzy matcher with Unicode/multibyte characters (#30546)
This PR fixes a crash in the fuzzy matcher that occurred when handling
Unicode or multibyte characters (such as Turkish `İ` or `ş`). The issue
was caused by the matcher attempting to index beyond the end of internal
arrays when lowercased Unicode characters expanded into multiple
codepoints, resulting in an out-of-bounds panic.

#### Root Cause

The loop in `recursive_score_match` used an upper bound (`limit`)
derived from `self.last_positions[query_idx]`, which could exceed the
actual length of the arrays being indexed, especially with multibyte
Unicode input.

#### Solution

The fix clamps the loop’s upper bound to the maximum valid index for the
arrays being accessed:
```rust
let max_valid_index = (prefix.len() + path_lowercased.len()).saturating_sub(1);
let safe_limit = limit.min(max_valid_index);
for j in path_idx..=safe_limit { ... }
```
This ensures all indexing is safe and prevents panics.

Closes #30269 

Release Notes:

- N/A

---------

Signed-off-by: Umesh Yadav <git@umesh.dev>
2025-05-12 14:43:14 +00:00
Danilo Leal
a13c8b70dd docs: Update the Text Threads page (#30576)
We had some broken links and outdated content here.

Release Notes:

- N/A
2025-05-12 11:23:50 -03:00
Danilo Leal
ddc649bdb8 agent: Don't rely only on color to communicate MCP server status (#30573)
The MCP server item in the settings view has an indicator that used to
only use colors to communicate the connection status. From an
accessibility standpoint, relying on just colors is never a good idea;
there should always be a supporting element that complements color for
communicating a certain thing. In this case, I added a tooltip, when you
hover over the indicator dot, that clearly words out the status.

Release Notes:

- agent: Improved clarity of MCP server connection status in the
Settings view.
2025-05-12 11:21:22 -03:00
张小白
33c896c23d windows: Fix ctrl-click open hovered URL (#30574)
Closes #30452

Release Notes:

- N/A
2025-05-12 22:15:50 +08:00
Bennet Bo Fenner
19b6c4444e zeta: Do not show usage for copilot/supermaven (#30563)
Follow up to #29952

Release Notes:

- Fix an issue where zeta usage would show up when using Copilot as an
edit prediction provider
2025-05-12 14:03:50 +00:00
Bennet Bo Fenner
8e39281699 docs: Document context_servers setting (#30570)
Release Notes:

- N/A
2025-05-12 13:53:22 +00:00
Anthony Eid
8294981ab5 debugger: Improve saving scenarios through new session modal (#30566)
- A loading icon is displayed while a scenario is being saved
- Saving a scenario doesn't take you to debug.json unless a user clicks
on the arrow icons that shows up after a successful save
- An error icon where show when a scenario fails to save
- Fixed a bug where scenario's failed to save when there was no .zed
directory in the user's worktree


Release Notes:

- N/A
2025-05-12 13:39:45 +00:00
Kirill Bulatov
a3105c92a4 Allow to hide more buttons with the settings (#30565)
* project search button in the status bar
```jsonc
"search": {
  "button": false
},
```

* project diagnostics button in the status bar
```jsonc
"diagnostics": {
  "button": false
}
```

* project name and host buttons in the title bar
```jsonc
"title_bar": {
    "show_project_items": false
}
```

* git branch button in the title bar
```jsonc
"title_bar": {
    "show_branch_name": false
}
```

Before:
<img width="1728" alt="before"
src="https://github.com/user-attachments/assets/4b13b431-3ac1-43b3-8ac7-469e5a9ccf7e"
/>

After:
<img width="1728" alt="after"
src="https://github.com/user-attachments/assets/baf2765a-e27b-47a3-8897-89152b7a7c95"
/>


Release Notes:

- Added more settings to hide buttons from Zed UI
2025-05-12 13:34:52 +00:00
Umesh Yadav
a6c3d49bb9 language_models: Add vision support for Copilot Chat models (#30155)
Problem Statement:
Support for image analysis (vision) is currently restricted to Anthropic
and Gemini models. This limits users who wish to leverage vision
capabilities available in other models, such as Copilot, for tasks like
attaching image context within the agent message editor.

Proposed Change:
This PR extends vision support to include Copilot models that are
already equipped with vision capabilities. This integration will allow
users within VS Code to attach and analyze images using supported
Copilot models via the agent message editor.

Scope Limitation:

This PR does not implement controls within the message editor to ensure
that image context (e.g., through copy-paste or attachment) is
exclusively enabled or prompted only when a vision-supported model is
active. Long term the message editor should have access to each models
vision capability and stop the users from attaching images by either
greying out the context saying it's not support or not work through both
copy paste and file/directory search.

Closes #30076 

Release Notes:

- Add vision support for Copilot Chat models

---------

Co-authored-by: Bennet Bo Fenner <bennet@zed.dev>
2025-05-12 13:11:38 +00:00
AidanV
5a38bbbd22 vim: Add :w <filename> command (#29256)
Closes https://github.com/zed-industries/zed/issues/10920

Release Notes:

- vim: Adds support for `:w[rite] <filename>`

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-05-12 15:09:18 +02:00
Conrad Irwin
196586e352 Fix deadlock loading node from the command line (#30561)
Before this change the the load env task never completed, leading to the
node runtime lock being held permanently.

Release Notes:

- N/A
2025-05-12 12:50:25 +00:00
Marshall Bowers
a1d8e50ec1 bedrock: Fix Claude 3.5 Haiku support (#30560)
This PR corrects a mistake introduced in
https://github.com/zed-industries/zed/pull/28523.

https://github.com/zed-industries/zed/pull/28523#issuecomment-2872369707

Release Notes:

- N/A
2025-05-12 12:45:35 +00:00
Conrad Irwin
24bc9fd0a0 Fix completions in debugger panel (#30545)
Release Notes:

- N/A
2025-05-12 14:39:06 +02:00
d1y
03f02804e5 Highlight shebang in TypeScript and JavaScript (#30531)
After:

![image](https://github.com/user-attachments/assets/8ae1049d-96c7-45e2-b905-1f0fba7f862c)

Before:

![image](https://github.com/user-attachments/assets/56317b12-d745-45f4-a7b6-880507884bae)


Release Notes:

- Typescript and javascript highlight shebang-line
2025-05-12 13:56:38 +02:00
Danilo Leal
41b0a5cf10 agent: Add menu item in the panel menu for zooming in feature (#30554)
Release Notes:

- agent: Added a menu item in the panel's menu for the zooming in/out
feature.
2025-05-12 08:46:00 -03:00
Danilo Leal
739236e968 agent: Fix message editor expand binding (#30553)
As of https://github.com/zed-industries/zed/pull/30504, we now can zoom
in the whole panel, which uses the `shift-escape` keybinding. We were
also using the same binding for the message editor expansion, which was
caused a conflict. Now, the message editor expansion requires an
additional key (`alt`) to work.

Release Notes:

- agent: Fixed conflicting keybinding between message editor and panel
zoom.
2025-05-12 08:45:52 -03:00
Liam
f14e48d202 language_models: Dynamically detect Copilot Chat models (#29027)
I noticed the discussion in #28881, and had thought of exactly the same
a few days prior.

This implementation should preserve existing functionality fairly well.

I've added a dependency (serde_with) to allow the deserializer to skip
models which cannot be deserialized, which could occur if a future
provider, for instance, is added. Without this modification, such a
change could break all models. If extra dependencies aren't desired, a
manual implementation could be used instead.

- Closes #29369 

Release Notes:

- Dynamically detect available Copilot Chat models, including all models
with tool support

---------

Co-authored-by: AidanV <aidanvanduyne@gmail.com>
Co-authored-by: imumesh18 <umesh4257@gmail.com>
Co-authored-by: Bennet Bo Fenner <bennet@zed.dev>
Co-authored-by: Agus Zubiaga <hi@aguz.me>
2025-05-12 11:28:41 +00:00
william341
634b275931 gpui: Fix cosmic-text raster_bounds calculation (#30552)
Closes #30526.

This PR makes the CacheKey used by raster_bounds and rasterize_glyph the
same, as they had not used the same sub pixel shift previously. Fixing
this resolves both the alignment and text-rendering issues introduced in
`ddf8d07`.

Release Notes:

- Fixed text rendering issues on Linux.
2025-05-12 11:10:40 +00:00
tidely
8000151aa9 zed: Reduce clones (#30550)
A collection of small patches that reduce clones. Mostly by using owned
iterators where possible.

Release Notes:

- N/A
2025-05-12 10:09:23 +00:00
Conrad Irwin
f0f0a52793 Revert "ui: Account for padding of parent container during scrollbar layout (#27402)" (#30544)
This reverts commit 82a7aca5a6.

Release Notes:

- N/A
2025-05-12 09:47:04 +00:00
Julia Ryan
907b2f0521 Parse env vars and args from debug launch editor (#30538)
Release Notes:

- debugger: allow setting env vars and arguments on the launch command.

---------

Co-authored-by: Cole Miller <m@cole-miller.net>
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-05-12 09:44:17 +00:00
Umesh Yadav
0ad582eec4 agent: Fix inline assistant focusing behavior for cursor placement (#29998)
Ref: https://github.com/zed-industries/zed/pull/29919

This PR improves how inline assistants are detected and focused based on
cursor position.

### Problem
The current implementation has inconsistent behavior:
- When selecting text within an inline assistant's range, the assistant
properly focuses
- When placing a cursor on a line containing an assistant (without
selection), a new assistant is created instead of focusing the existing
one

### Solution
Enhanced the assistant detection logic to:
- Check if the cursor is anywhere within the line range of an existing
assistant
- Maintain the same behavior for both cursor placement and text
selection
- Convert both cursor position and assistant ranges to points for better
line-based comparison

This creates a more intuitive editing experience when working with
inline assistants, reducing the creation of duplicate assistants when
the user intends to interact with existing ones.


https://github.com/user-attachments/assets/55eb80d1-76a7-4d42-aac4-2702e85f13c4

Release Notes:

- agent: Improved inline assistant behavior to focus existing assistants
when cursor is placed on their line, matching selection behavior

---------

Co-authored-by: Bennet Bo Fenner <bennet@zed.dev>
2025-05-12 09:29:14 +00:00
Marshall Bowers
58ed81b698 extension_host: Include more details about error messages (#30543)
This PR makes it so the error messages surfaced to extensions will
contain more information.

Supersedes https://github.com/zed-industries/zed/pull/28491.

Release Notes:

- N/A
2025-05-12 09:21:37 +00:00
Danilo Leal
83319c8a6d agent: Fix instruction list item with multiple buttons not working (#30541)
This was a particular problem in the Amazon Bedrock section (at least
for now) where there were multiple buttons and none of them actually
worked because they all had the same id.

Release Notes:

- agent: Fixed Amazon Bedrock settings link buttons not working.
2025-05-12 06:19:20 -03:00
Michael Sloan
4deb8cce8d agent: Fix 10 line code blocks being expandable despite fitting (#30540)
Release Notes:

- N/A
2025-05-12 09:17:37 +00:00
Shardul Vaidya
8d79226445 bedrock: Add support for Mistral - Pixtral Large (#28274)
Release Notes:

- AWS Bedrock: Added support for Pixtral Large 25.02 v1

---------

Co-authored-by: Peter Tripp <peter@zed.dev>
Co-authored-by: Marshall Bowers <git@maxdeviant.com>
2025-05-12 09:13:37 +00:00
Michael Sloan
5abca0f867 Fix codeblock expansion initial state + refactor (#30539)
Release Notes:

- N/A
2025-05-12 09:05:00 +00:00
Ben Brandt
68945ac53e workspace: Add keyboard shortcuts to close active dock (#30508)
Adds the normal close keybinding for the new Close Active Dock action.

Release Notes:

- N/A
2025-05-12 11:02:15 +02:00
Richard Feldman
49887d6934 Add no_tools_enabled eval (#30537)
This is our first eval of the Minimal tool profile. Right now they're
all passing; the value of having it is to catch regressions in the
system prompt (which has special logic in it for the case where no tools
are enabled).

Release Notes:

- N/A
2025-05-12 08:52:03 +00:00
Shardul Vaidya
d867897746 bedrock: Support cross-region inference for US Claude 3.5 Haiku (#28523)
Release Notes:

- Added Cross-Region inference support for US Claude 3.5 Haiku

Co-authored-by: Peter Tripp <peter@zed.dev>
Co-authored-by: Marshall Bowers <git@maxdeviant.com>
2025-05-12 08:41:45 +00:00
Shardul Vaidya
1f58ce80f2 bedrock: Support Amazon Nova Premier (#29720)
Release Notes:

- Bedrock: Added support for Amazon Nova Premier.


https://aws.amazon.com/blogs/aws/amazon-nova-premier-our-most-capable-model-for-complex-tasks-and-teacher-for-model-distillation/

Co-authored-by: Marshall Bowers <git@maxdeviant.com>
2025-05-12 08:15:18 +00:00
Chris Kelly
ed772e6baf agent: Allow to collapse provider sections in the settings view (#30437)
This is my first time contributing, so happy to make changes as needed.

## Problem

I found the LLM Provider settings to be pretty difficult to scan as I
was looking to enter my API credentials for a provider. Because all of
the provider configuration is exposed by default, providers that come at
the end of the list are pushed fairly far down and require scrolling. As
this list increases the problem only get worse.

## Solution

This is strictly a UI change.

* I put each provider configuration in a Disclosure that is closed by
default. This made scanning for my provider easy, and exposing the
configuration takes a single click. No scrolling is required to see all
providers on my 956px high laptop screen.
* I also added the success checkmark to authenticated providers to make
it even easier to find them to update a key or sign out.
* The `Start New Thread` had a class applied that was overriding the
default hover behavior of other buttons, so I removed it.

## Before
![CleanShot 2025-05-09 at 14 06
04@2x](https://github.com/user-attachments/assets/48d1e7ea-0dc8-4adc-845c-5227ec965130)

## After
![CleanShot 2025-05-09 at 14 33
23](https://github.com/user-attachments/assets/67e842a7-3251-46e5-ab18-7c4e600b84d8)

Release Notes:

- Improved Agent Panel settings view scannability by making each
provider block collapsible by default.

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-05-12 05:07:30 -03:00
Shardul Vaidya
559725d8f5 bedrock: Support Writer Palmyra models (#29719)
Release Notes:

- Added support for Writer Palmyra X4, and X5


https://writer.com/engineering/long-context-palmyra-x5/

Co-authored-by: Marshall Bowers <git@maxdeviant.com>
2025-05-12 07:54:09 +00:00
167 changed files with 6278 additions and 2377 deletions

View File

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

1
.gitignore vendored
View File

@@ -2,6 +2,7 @@
**/cargo-target
**/target
**/venv
**/.direnv
*.wasm
*.xcodeproj
.DS_Store

29
Cargo.lock generated
View File

@@ -3309,6 +3309,7 @@ dependencies = [
"http_client",
"indoc",
"inline_completion",
"itertools 0.14.0",
"language",
"log",
"lsp",
@@ -3318,11 +3319,9 @@ dependencies = [
"paths",
"project",
"rpc",
"schemars",
"serde",
"serde_json",
"settings",
"strum 0.27.1",
"task",
"theme",
"ui",
@@ -4135,6 +4134,18 @@ dependencies = [
"winapi",
]
[[package]]
name = "debug_adapter_extension"
version = "0.1.0"
dependencies = [
"anyhow",
"async-trait",
"dap",
"extension",
"gpui",
"workspace-hack",
]
[[package]]
name = "debugger_tools"
version = "0.1.0"
@@ -4168,6 +4179,7 @@ dependencies = [
"editor",
"env_logger 0.11.8",
"feature_flags",
"file_icons",
"futures 0.3.31",
"fuzzy",
"gpui",
@@ -4183,6 +4195,7 @@ dependencies = [
"serde",
"serde_json",
"settings",
"shlex",
"sysinfo",
"task",
"tasks_ui",
@@ -5038,6 +5051,7 @@ dependencies = [
"async-tar",
"async-trait",
"collections",
"dap",
"fs",
"futures 0.3.31",
"gpui",
@@ -5050,6 +5064,7 @@ dependencies = [
"semantic_version",
"serde",
"serde_json",
"task",
"toml 0.8.20",
"util",
"wasm-encoder 0.221.3",
@@ -5093,6 +5108,7 @@ dependencies = [
"client",
"collections",
"ctor",
"dap",
"env_logger 0.11.8",
"extension",
"fs",
@@ -7225,7 +7241,6 @@ dependencies = [
"lsp",
"paths",
"project",
"proto",
"regex",
"serde_json",
"settings",
@@ -7233,7 +7248,6 @@ dependencies = [
"telemetry",
"theme",
"ui",
"util",
"workspace",
"workspace-hack",
"zed_actions",
@@ -10039,7 +10053,7 @@ name = "perplexity"
version = "0.1.0"
dependencies = [
"serde",
"zed_extension_api 0.5.0",
"zed_extension_api 0.6.0",
]
[[package]]
@@ -18019,7 +18033,6 @@ dependencies = [
"aho-corasick",
"anstream",
"arrayvec",
"async-compression",
"async-std",
"async-tungstenite",
"aws-config",
@@ -18697,7 +18710,7 @@ dependencies = [
[[package]]
name = "zed_extension_api"
version = "0.5.0"
version = "0.6.0"
dependencies = [
"serde",
"serde_json",
@@ -18757,7 +18770,7 @@ dependencies = [
name = "zed_test_extension"
version = "0.1.0"
dependencies = [
"zed_extension_api 0.5.0",
"zed_extension_api 0.6.0",
]
[[package]]

View File

@@ -37,6 +37,7 @@ members = [
"crates/dap",
"crates/dap_adapters",
"crates/db",
"crates/debug_adapter_extension",
"crates/debugger_tools",
"crates/debugger_ui",
"crates/deepseek",
@@ -243,6 +244,7 @@ credentials_provider = { path = "crates/credentials_provider" }
dap = { path = "crates/dap" }
dap_adapters = { path = "crates/dap_adapters" }
db = { path = "crates/db" }
debug_adapter_extension = { path = "crates/debug_adapter_extension" }
debugger_tools = { path = "crates/debugger_tools" }
debugger_ui = { path = "crates/debugger_ui" }
deepseek = { path = "crates/deepseek" }

View File

@@ -244,7 +244,7 @@
"ctrl-shift-a": "agent::ToggleContextPicker",
"ctrl-shift-o": "agent::ToggleNavigationMenu",
"ctrl-shift-i": "agent::ToggleOptionsMenu",
"shift-escape": "agent::ExpandMessageEditor",
"shift-alt-escape": "agent::ExpandMessageEditor",
"ctrl-alt-e": "agent::RemoveAllContext",
"ctrl-shift-e": "project_panel::ToggleFocus"
}
@@ -538,6 +538,7 @@
"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",
@@ -978,5 +979,12 @@
"bindings": {
"ctrl-r": "diagnostics::ToggleDiagnosticsRefresh"
}
},
{
"context": "DebugConsole > Editor",
"use_key_equivalents": true,
"bindings": {
"enter": "menu::Confirm"
}
}
]

View File

@@ -290,7 +290,7 @@
"cmd-shift-a": "agent::ToggleContextPicker",
"cmd-shift-o": "agent::ToggleNavigationMenu",
"cmd-shift-i": "agent::ToggleOptionsMenu",
"shift-escape": "agent::ExpandMessageEditor",
"shift-alt-escape": "agent::ExpandMessageEditor",
"cmd-alt-e": "agent::RemoveAllContext",
"cmd-shift-e": "project_panel::ToggleFocus"
}
@@ -608,6 +608,7 @@
"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 }],
@@ -1084,5 +1085,12 @@
"bindings": {
"ctrl-r": "diagnostics::ToggleDiagnosticsRefresh"
}
},
{
"context": "DebugConsole > Editor",
"use_key_equivalents": true,
"bindings": {
"enter": "menu::Confirm"
}
}
]

View File

@@ -113,8 +113,8 @@
// Whether to show the informational hover box when moving the mouse
// over symbols in the editor.
"hover_popover_enabled": true,
// Time to wait before showing the informational hover box
"hover_popover_delay": 350,
// Time to wait in milliseconds before showing the informational hover box.
"hover_popover_delay": 300,
// Whether to confirm before quitting Zed.
"confirm_quit": false,
// Whether to restore last closed project when fresh Zed instance is opened.
@@ -328,6 +328,10 @@
"title_bar": {
// Whether to show the branch icon beside branch switcher in the titlebar.
"show_branch_icon": false,
// Whether to show the branch name button in the titlebar.
"show_branch_name": true,
// Whether to show the project host and name in the titlebar.
"show_project_items": true,
// Whether to show onboarding banners in the titlebar.
"show_onboarding_banner": true,
// Whether to show user picture in the titlebar.
@@ -470,6 +474,8 @@
"search_wrap": true,
// Search options to enable by default when opening new project and buffer searches.
"search": {
// Whether to show the project search button in the status bar.
"button": true,
"whole_word": false,
"case_sensitive": false,
"include_ignored": false,
@@ -1002,6 +1008,8 @@
"auto_update": true,
// Diagnostics configuration.
"diagnostics": {
// Whether to show the project diagnostics button in the status bar.
"button": true,
// Whether to show warnings or not by default.
"include_warnings": true,
// Settings for inline diagnostics

View File

@@ -33,7 +33,9 @@ use language_model::{
LanguageModelRequestMessage, LanguageModelToolUseId, MessageContent, Role, StopReason,
};
use markdown::parser::{CodeBlockKind, CodeBlockMetadata};
use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle, ParsedMarkdown};
use markdown::{
HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle, ParsedMarkdown, PathWithRange,
};
use project::{ProjectEntryId, ProjectItem as _};
use rope::Point;
use settings::{Settings as _, SettingsStore, update_settings_file};
@@ -430,49 +432,8 @@ fn render_markdown_code_block(
let path_range = path_range.clone();
move |_, window, cx| {
workspace
.update(cx, {
|workspace, cx| {
let Some(project_path) = workspace
.project()
.read(cx)
.find_project_path(&path_range.path, cx)
else {
return;
};
let Some(target) = path_range.range.as_ref().map(|range| {
Point::new(
// Line number is 1-based
range.start.line.saturating_sub(1),
range.start.col.unwrap_or(0),
)
}) else {
return;
};
let open_task = workspace.open_path(
project_path,
None,
true,
window,
cx,
);
window
.spawn(cx, async move |cx| {
let item = open_task.await?;
if let Some(active_editor) =
item.downcast::<Editor>()
{
active_editor
.update_in(cx, |editor, window, cx| {
editor.go_to_singleton_buffer_point(
target, window, cx,
);
})
.ok();
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
.update(cx, |workspace, cx| {
open_path(&path_range, window, workspace, cx)
})
.ok();
}
@@ -487,15 +448,10 @@ fn render_markdown_code_block(
.copied_code_block_ids
.contains(&(message_id, ix));
let can_expand = metadata.line_count > MAX_UNCOLLAPSED_LINES_IN_CODE_BLOCK;
let can_expand = metadata.line_count >= MAX_UNCOLLAPSED_LINES_IN_CODE_BLOCK;
let is_expanded = if can_expand {
active_thread
.read(cx)
.expanded_code_blocks
.get(&(message_id, ix))
.copied()
.unwrap_or(false)
active_thread.read(cx).is_codeblock_expanded(message_id, ix)
} else {
false
};
@@ -582,11 +538,7 @@ fn render_markdown_code_block(
let active_thread = active_thread.clone();
move |_event, _window, cx| {
active_thread.update(cx, |this, cx| {
let is_expanded = this
.expanded_code_blocks
.entry((message_id, ix))
.or_insert(true);
*is_expanded = !*is_expanded;
this.toggle_codeblock_expanded(message_id, ix);
cx.notify();
});
}
@@ -607,6 +559,45 @@ fn render_markdown_code_block(
.when(can_expand && !is_expanded, |this| this.max_h_80())
}
fn open_path(
path_range: &PathWithRange,
window: &mut Window,
workspace: &mut Workspace,
cx: &mut Context<'_, Workspace>,
) {
let Some(project_path) = workspace
.project()
.read(cx)
.find_project_path(&path_range.path, cx)
else {
return; // TODO instead of just bailing out, open that path in a buffer.
};
let Some(target) = path_range.range.as_ref().map(|range| {
Point::new(
// Line number is 1-based
range.start.line.saturating_sub(1),
range.start.col.unwrap_or(0),
)
}) else {
return;
};
let open_task = workspace.open_path(project_path, None, true, window, cx);
window
.spawn(cx, async move |cx| {
let item = open_task.await?;
if let Some(active_editor) = item.downcast::<Editor>() {
active_editor
.update_in(cx, |editor, window, cx| {
editor.go_to_singleton_buffer_point(target, window, cx);
})
.ok();
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
fn render_code_language(
language: Option<&Arc<Language>>,
name_fallback: SharedString,
@@ -2356,17 +2347,14 @@ impl ActiveThread {
move |el, range, metadata, _, cx| {
let can_expand = metadata.line_count
> MAX_UNCOLLAPSED_LINES_IN_CODE_BLOCK;
>= MAX_UNCOLLAPSED_LINES_IN_CODE_BLOCK;
if !can_expand {
return el;
}
let is_expanded = active_thread
.read(cx)
.expanded_code_blocks
.get(&(message_id, range.start))
.copied()
.unwrap_or(false);
.is_codeblock_expanded(message_id, range.start);
if is_expanded {
return el;
}
@@ -3384,6 +3372,21 @@ impl ActiveThread {
.log_err();
}))
}
pub fn is_codeblock_expanded(&self, message_id: MessageId, ix: usize) -> bool {
self.expanded_code_blocks
.get(&(message_id, ix))
.copied()
.unwrap_or(false)
}
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);
*is_expanded = !*is_expanded;
}
}
pub enum ActiveThreadEvent {

View File

@@ -36,6 +36,7 @@ pub struct AgentConfiguration {
configuration_views_by_provider: HashMap<LanguageModelProviderId, AnyView>,
context_server_store: Entity<ContextServerStore>,
expanded_context_server_tools: HashMap<ContextServerId, bool>,
expanded_provider_configurations: HashMap<LanguageModelProviderId, bool>,
tools: Entity<ToolWorkingSet>,
_registry_subscription: Subscription,
scroll_handle: ScrollHandle,
@@ -78,6 +79,7 @@ impl AgentConfiguration {
configuration_views_by_provider: HashMap::default(),
context_server_store,
expanded_context_server_tools: HashMap::default(),
expanded_provider_configurations: HashMap::default(),
tools,
_registry_subscription: registry_subscription,
scroll_handle,
@@ -96,6 +98,7 @@ impl AgentConfiguration {
fn remove_provider_configuration_view(&mut self, provider_id: &LanguageModelProviderId) {
self.configuration_views_by_provider.remove(provider_id);
self.expanded_provider_configurations.remove(provider_id);
}
fn add_provider_configuration_view(
@@ -135,9 +138,14 @@ impl AgentConfiguration {
.get(&provider.id())
.cloned();
let is_expanded = self
.expanded_provider_configurations
.get(&provider.id())
.copied()
.unwrap_or(true);
v_flex()
.pt_3()
.pb_1()
.gap_1p5()
.border_t_1()
.border_color(cx.theme().colors().border.opacity(0.6))
@@ -152,32 +160,59 @@ impl AgentConfiguration {
.size(IconSize::Small)
.color(Color::Muted),
)
.child(Label::new(provider_name.clone()).size(LabelSize::Large)),
.child(Label::new(provider_name.clone()).size(LabelSize::Large))
.when(provider.is_authenticated(cx) && !is_expanded, |parent| {
parent.child(Icon::new(IconName::Check).color(Color::Success))
}),
)
.when(provider.is_authenticated(cx), |parent| {
parent.child(
Button::new(
SharedString::from(format!("new-thread-{provider_id}")),
"Start New Thread",
)
.icon_position(IconPosition::Start)
.icon(IconName::Plus)
.icon_size(IconSize::Small)
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ModalSurface)
.label_size(LabelSize::Small)
.on_click(cx.listener({
let provider = provider.clone();
move |_this, _event, _window, cx| {
cx.emit(AssistantConfigurationEvent::NewThread(
provider.clone(),
))
}
})),
)
}),
.child(
h_flex()
.gap_1()
.when(provider.is_authenticated(cx), |parent| {
parent.child(
Button::new(
SharedString::from(format!("new-thread-{provider_id}")),
"Start New Thread",
)
.icon_position(IconPosition::Start)
.icon(IconName::Plus)
.icon_size(IconSize::Small)
.layer(ElevationIndex::ModalSurface)
.label_size(LabelSize::Small)
.on_click(cx.listener({
let provider = provider.clone();
move |_this, _event, _window, cx| {
cx.emit(AssistantConfigurationEvent::NewThread(
provider.clone(),
))
}
})),
)
})
.child(
Disclosure::new(
SharedString::from(format!(
"provider-disclosure-{provider_id}"
)),
is_expanded,
)
.opened_icon(IconName::ChevronUp)
.closed_icon(IconName::ChevronDown)
.on_click(cx.listener({
let provider_id = provider.id().clone();
move |this, _event, _window, _cx| {
let is_open = this
.expanded_provider_configurations
.entry(provider_id.clone())
.or_insert(true);
*is_open = !*is_open;
}
})),
),
),
)
.map(|parent| match configuration_view {
.when(is_expanded, |parent| match configuration_view {
Some(configuration_view) => parent.child(configuration_view),
None => parent.child(div().child(Label::new(format!(
"No configuration view for {provider_name}",
@@ -387,6 +422,7 @@ impl AgentConfiguration {
.unwrap_or(ContextServerStatus::Stopped);
let is_running = matches!(server_status, ContextServerStatus::Running);
let item_id = SharedString::from(context_server_id.0.clone());
let error = if let ContextServerStatus::Error(error) = server_status.clone() {
Some(error)
@@ -408,9 +444,38 @@ impl AgentConfiguration {
let tool_count = tools.len();
let border_color = cx.theme().colors().border.opacity(0.6);
let success_color = Color::Success.color(cx);
let (status_indicator, tooltip_text) = match server_status {
ContextServerStatus::Starting => (
Indicator::dot()
.color(Color::Success)
.with_animation(
SharedString::from(format!("{}-starting", context_server_id.0.clone(),)),
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(pulsating_between(0.4, 1.)),
move |this, delta| this.color(success_color.alpha(delta).into()),
)
.into_any_element(),
"Server is starting.",
),
ContextServerStatus::Running => (
Indicator::dot().color(Color::Success).into_any_element(),
"Server is running.",
),
ContextServerStatus::Error(_) => (
Indicator::dot().color(Color::Error).into_any_element(),
"Server has an error.",
),
ContextServerStatus::Stopped => (
Indicator::dot().color(Color::Muted).into_any_element(),
"Server is stopped.",
),
};
v_flex()
.id(SharedString::from(context_server_id.0.clone()))
.id(item_id.clone())
.border_1()
.rounded_md()
.border_color(border_color)
@@ -445,35 +510,12 @@ impl AgentConfiguration {
}
})),
)
.child(match server_status {
ContextServerStatus::Starting => {
let color = Color::Success.color(cx);
Indicator::dot()
.color(Color::Success)
.with_animation(
SharedString::from(format!(
"{}-starting",
context_server_id.0.clone(),
)),
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(pulsating_between(0.4, 1.)),
move |this, delta| {
this.color(color.alpha(delta).into())
},
)
.into_any_element()
}
ContextServerStatus::Running => {
Indicator::dot().color(Color::Success).into_any_element()
}
ContextServerStatus::Error(_) => {
Indicator::dot().color(Color::Error).into_any_element()
}
ContextServerStatus::Stopped => {
Indicator::dot().color(Color::Muted).into_any_element()
}
})
.child(
div()
.id(item_id.clone())
.tooltip(Tooltip::text(tooltip_text))
.child(status_indicator),
)
.child(Label::new(context_server_id.0.clone()).ml_0p5())
.when(is_running, |this| {
this.child(

View File

@@ -1648,6 +1648,12 @@ impl AgentPanel {
}),
);
let zoom_in_label = if self.is_zoomed(window, cx) {
"Zoom Out"
} else {
"Zoom In"
};
let agent_extra_menu = PopoverMenu::new("agent-options-menu")
.trigger_with_tooltip(
IconButton::new("agent-options-menu", IconName::Ellipsis)
@@ -1734,7 +1740,8 @@ impl AgentPanel {
menu = menu
.action("Rules…", Box::new(OpenRulesLibrary::default()))
.action("Settings", Box::new(OpenConfiguration));
.action("Settings", Box::new(OpenConfiguration))
.action(zoom_in_label, Box::new(ToggleZoom));
menu
}))
});
@@ -2432,9 +2439,6 @@ impl AgentPanel {
.occlude()
.child(match last_error {
ThreadError::PaymentRequired => self.render_payment_required_error(cx),
ThreadError::MaxMonthlySpendReached => {
self.render_max_monthly_spend_reached_error(cx)
}
ThreadError::ModelRequestLimitReached { plan } => {
self.render_model_request_limit_reached_error(plan, cx)
}
@@ -2494,56 +2498,6 @@ impl AgentPanel {
.into_any()
}
fn render_max_monthly_spend_reached_error(&self, cx: &mut Context<Self>) -> AnyElement {
const ERROR_MESSAGE: &str = "You have reached your maximum monthly spend. Increase your spend limit to continue using Zed LLMs.";
v_flex()
.gap_0p5()
.child(
h_flex()
.gap_1p5()
.items_center()
.child(Icon::new(IconName::XCircle).color(Color::Error))
.child(Label::new("Max Monthly Spend Reached").weight(FontWeight::MEDIUM)),
)
.child(
div()
.id("error-message")
.max_h_24()
.overflow_y_scroll()
.child(Label::new(ERROR_MESSAGE)),
)
.child(
h_flex()
.justify_end()
.mt_1()
.gap_1()
.child(self.create_copy_button(ERROR_MESSAGE))
.child(
Button::new("subscribe", "Update Monthly Spend Limit").on_click(
cx.listener(|this, _, _, cx| {
this.thread.update(cx, |this, _cx| {
this.clear_last_error();
});
cx.open_url(&zed_urls::account_url(cx));
cx.notify();
}),
),
)
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|this, _, _, cx| {
this.thread.update(cx, |this, _cx| {
this.clear_last_error();
});
cx.notify();
},
))),
)
.into_any()
}
fn render_model_request_limit_reached_error(
&self,
plan: Plan,

View File

@@ -338,13 +338,27 @@ impl InlineAssistant {
window: &mut Window,
cx: &mut App,
) {
let (snapshot, initial_selections) = editor.update(cx, |editor, cx| {
(
editor.snapshot(window, cx),
editor.selections.all::<Point>(cx),
)
let (snapshot, initial_selections, newest_selection) = editor.update(cx, |editor, cx| {
let selections = editor.selections.all::<Point>(cx);
let newest_selection = editor.selections.newest::<Point>(cx);
(editor.snapshot(window, cx), selections, newest_selection)
});
// Check if there is already an inline assistant that contains the
// newest selection, if there is, focus it
if let Some(editor_assists) = self.assists_by_editor.get(&editor.downgrade()) {
for assist_id in &editor_assists.assist_ids {
let assist = &self.assists[assist_id];
let range = assist.range.to_point(&snapshot.buffer_snapshot);
if range.start.row <= newest_selection.start.row
&& newest_selection.end.row <= range.end.row
{
self.focus_assist(*assist_id, window, cx);
return;
}
}
}
let mut selections = Vec::<Selection<Point>>::new();
let mut newest_selection = None;
for mut selection in initial_selections {

View File

@@ -22,7 +22,7 @@ use language_model::{
ConfiguredModel, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
LanguageModelId, LanguageModelKnownError, LanguageModelRegistry, LanguageModelRequest,
LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolResult,
LanguageModelToolUseId, MaxMonthlySpendReachedError, MessageContent,
LanguageModelToolResultContent, LanguageModelToolUseId, MessageContent,
ModelRequestLimitReachedError, PaymentRequiredError, RequestUsage, Role, SelectedModel,
StopReason, TokenUsage,
};
@@ -880,7 +880,13 @@ impl Thread {
}
pub fn output_for_tool(&self, id: &LanguageModelToolUseId) -> Option<&Arc<str>> {
Some(&self.tool_use.tool_result(id)?.content)
match &self.tool_use.tool_result(id)?.content {
LanguageModelToolResultContent::Text(str) => Some(str),
LanguageModelToolResultContent::Image(_) => {
// TODO: We should display image
None
}
}
}
pub fn card_for_tool(&self, id: &LanguageModelToolUseId) -> Option<AnyToolCard> {
@@ -1682,10 +1688,6 @@ impl Thread {
if error.is::<PaymentRequiredError>() {
cx.emit(ThreadEvent::ShowError(ThreadError::PaymentRequired));
} else if error.is::<MaxMonthlySpendReachedError>() {
cx.emit(ThreadEvent::ShowError(
ThreadError::MaxMonthlySpendReached,
));
} else if let Some(error) =
error.downcast_ref::<ModelRequestLimitReachedError>()
{
@@ -2241,7 +2243,7 @@ impl Thread {
.read(cx)
.enabled_tools(cx)
.iter()
.map(|tool| tool.name().to_string())
.map(|tool| tool.name())
.collect();
self.message_feedback.insert(message_id, feedback);
@@ -2502,7 +2504,15 @@ impl Thread {
}
writeln!(markdown, "**\n")?;
writeln!(markdown, "{}", tool_result.content)?;
match &tool_result.content {
LanguageModelToolResultContent::Text(str) => {
writeln!(markdown, "{}", str)?;
}
LanguageModelToolResultContent::Image(image) => {
writeln!(markdown, "![Image](data:base64,{})", image.source)?;
}
}
if let Some(output) = tool_result.output.as_ref() {
writeln!(
markdown,
@@ -2692,8 +2702,6 @@ impl Thread {
pub enum ThreadError {
#[error("Payment required")]
PaymentRequired,
#[error("Max monthly spend reached")]
MaxMonthlySpendReached,
#[error("Model request limit reached")]
ModelRequestLimitReached { plan: Plan },
#[error("Message {header}: {message}")]

View File

@@ -19,7 +19,7 @@ use gpui::{
};
use heed::Database;
use heed::types::SerdeBincode;
use language_model::{LanguageModelToolUseId, Role, TokenUsage};
use language_model::{LanguageModelToolResultContent, LanguageModelToolUseId, Role, TokenUsage};
use project::context_server_store::{ContextServerStatus, ContextServerStore};
use project::{Project, ProjectItem, ProjectPath, Worktree};
use prompt_store::{
@@ -486,8 +486,8 @@ impl ThreadStore {
ToolSource::Native,
&profile
.tools
.iter()
.filter_map(|(tool, enabled)| enabled.then(|| tool.clone()))
.into_iter()
.filter_map(|(tool, enabled)| enabled.then(|| tool))
.collect::<Vec<_>>(),
cx,
);
@@ -511,32 +511,32 @@ impl ThreadStore {
});
}
// Enable all the tools from all context servers, but disable the ones that are explicitly disabled
for (context_server_id, preset) in &profile.context_servers {
for (context_server_id, preset) in profile.context_servers {
self.tools.update(cx, |tools, cx| {
tools.disable(
ToolSource::ContextServer {
id: context_server_id.clone().into(),
id: context_server_id.into(),
},
&preset
.tools
.iter()
.filter_map(|(tool, enabled)| (!enabled).then(|| tool.clone()))
.into_iter()
.filter_map(|(tool, enabled)| (!enabled).then(|| tool))
.collect::<Vec<_>>(),
cx,
)
})
}
} else {
for (context_server_id, preset) in &profile.context_servers {
for (context_server_id, preset) in profile.context_servers {
self.tools.update(cx, |tools, cx| {
tools.enable(
ToolSource::ContextServer {
id: context_server_id.clone().into(),
id: context_server_id.into(),
},
&preset
.tools
.iter()
.filter_map(|(tool, enabled)| enabled.then(|| tool.clone()))
.into_iter()
.filter_map(|(tool, enabled)| enabled.then(|| tool))
.collect::<Vec<_>>(),
cx,
)
@@ -775,7 +775,7 @@ pub struct SerializedToolUse {
pub struct SerializedToolResult {
pub tool_use_id: LanguageModelToolUseId,
pub is_error: bool,
pub content: Arc<str>,
pub content: LanguageModelToolResultContent,
pub output: Option<serde_json::Value>,
}

View File

@@ -1,14 +1,16 @@
use std::sync::Arc;
use anyhow::Result;
use assistant_tool::{AnyToolCard, Tool, ToolResultOutput, ToolUseStatus, ToolWorkingSet};
use assistant_tool::{
AnyToolCard, Tool, ToolResultContent, ToolResultOutput, ToolUseStatus, ToolWorkingSet,
};
use collections::HashMap;
use futures::FutureExt as _;
use futures::future::Shared;
use gpui::{App, Entity, SharedString, Task};
use language_model::{
ConfiguredModel, LanguageModel, LanguageModelRequest, LanguageModelToolResult,
LanguageModelToolUse, LanguageModelToolUseId, Role,
LanguageModelToolResultContent, LanguageModelToolUse, LanguageModelToolUseId, Role,
};
use project::Project;
use ui::{IconName, Window};
@@ -165,10 +167,16 @@ impl ToolUseState {
let status = (|| {
if let Some(tool_result) = tool_result {
let content = tool_result
.content
.to_str()
.map(|str| str.to_owned().into())
.unwrap_or_default();
return if tool_result.is_error {
ToolUseStatus::Error(tool_result.content.clone().into())
ToolUseStatus::Error(content)
} else {
ToolUseStatus::Finished(tool_result.content.clone().into())
ToolUseStatus::Finished(content)
};
}
@@ -399,21 +407,44 @@ impl ToolUseState {
let tool_result = output.content;
const BYTES_PER_TOKEN_ESTIMATE: usize = 3;
// Protect from clearly large output
let old_use = self.pending_tool_uses_by_id.remove(&tool_use_id);
// Protect from overly large output
let tool_output_limit = configured_model
.map(|model| model.model.max_token_count() * BYTES_PER_TOKEN_ESTIMATE)
.unwrap_or(usize::MAX);
let tool_result = if tool_result.len() <= tool_output_limit {
tool_result
} else {
let truncated = truncate_lines_to_byte_limit(&tool_result, tool_output_limit);
let content = match tool_result {
ToolResultContent::Text(text) => {
let truncated = truncate_lines_to_byte_limit(&text, tool_output_limit);
format!(
"Tool result too long. The first {} bytes:\n\n{}",
truncated.len(),
truncated
)
LanguageModelToolResultContent::Text(
format!(
"Tool result too long. The first {} bytes:\n\n{}",
truncated.len(),
truncated
)
.into(),
)
}
ToolResultContent::Image(language_model_image) => {
if language_model_image.estimate_tokens() < tool_output_limit {
LanguageModelToolResultContent::Image(language_model_image)
} else {
self.tool_results.insert(
tool_use_id.clone(),
LanguageModelToolResult {
tool_use_id: tool_use_id.clone(),
tool_name,
content: "Tool responded with an image that would exceeded the remaining tokens".into(),
is_error: true,
output: None,
},
);
return old_use;
}
}
};
self.tool_results.insert(
@@ -421,12 +452,13 @@ impl ToolUseState {
LanguageModelToolResult {
tool_use_id: tool_use_id.clone(),
tool_name,
content: tool_result.into(),
content,
is_error: false,
output: output.output,
},
);
self.pending_tool_uses_by_id.remove(&tool_use_id)
old_use
}
Err(err) => {
self.tool_results.insert(
@@ -434,7 +466,7 @@ impl ToolUseState {
LanguageModelToolResult {
tool_use_id: tool_use_id.clone(),
tool_name,
content: err.to_string().into(),
content: LanguageModelToolResultContent::Text(err.to_string().into()),
is_error: true,
output: None,
},

View File

@@ -534,12 +534,26 @@ pub enum RequestContent {
ToolResult {
tool_use_id: String,
is_error: bool,
content: String,
content: ToolResultContent,
#[serde(skip_serializing_if = "Option::is_none")]
cache_control: Option<CacheControl>,
},
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ToolResultContent {
Plain(String),
Multipart(Vec<ToolResultPart>),
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum ToolResultPart {
Text { text: String },
Image { source: ImageSource },
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum ResponseContent {

View File

@@ -21,8 +21,8 @@ use language::{AnchorRangeExt, Bias, Buffer, LanguageRegistry, OffsetRangeExt, P
use language_model::{
LanguageModel, LanguageModelCacheConfiguration, LanguageModelCompletionEvent,
LanguageModelImage, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
LanguageModelToolUseId, MaxMonthlySpendReachedError, MessageContent, PaymentRequiredError,
Role, StopReason, report_assistant_event,
LanguageModelToolUseId, MessageContent, PaymentRequiredError, Role, StopReason,
report_assistant_event,
};
use open_ai::Model as OpenAiModel;
use paths::contexts_dir;
@@ -447,7 +447,6 @@ impl ContextOperation {
pub enum ContextEvent {
ShowAssistError(SharedString),
ShowPaymentRequiredError,
ShowMaxMonthlySpendReachedError,
MessagesEdited,
SummaryChanged,
SummaryGenerated,
@@ -2155,12 +2154,6 @@ impl AssistantContext {
metadata.status = MessageStatus::Canceled;
});
Some(error.to_string())
} else if error.is::<MaxMonthlySpendReachedError>() {
cx.emit(ContextEvent::ShowMaxMonthlySpendReachedError);
this.update_metadata(assistant_message_id, cx, |metadata| {
metadata.status = MessageStatus::Canceled;
});
Some(error.to_string())
} else {
let error_message = error
.chain()

View File

@@ -114,7 +114,6 @@ type MessageHeader = MessageMetadata;
#[derive(Clone)]
enum AssistError {
PaymentRequired,
MaxMonthlySpendReached,
Message(SharedString),
}
@@ -732,9 +731,6 @@ impl ContextEditor {
ContextEvent::ShowPaymentRequiredError => {
self.last_error = Some(AssistError::PaymentRequired);
}
ContextEvent::ShowMaxMonthlySpendReachedError => {
self.last_error = Some(AssistError::MaxMonthlySpendReached);
}
}
}
@@ -1594,7 +1590,7 @@ impl ContextEditor {
&mut self,
cx: &mut Context<Self>,
) -> (String, CopyMetadata, Vec<text::Selection<usize>>) {
let (selection, creases) = self.editor.update(cx, |editor, cx| {
let (mut selection, creases) = self.editor.update(cx, |editor, cx| {
let mut selection = editor.selections.newest_adjusted(cx);
let snapshot = editor.buffer().read(cx).snapshot(cx);
@@ -1652,7 +1648,18 @@ impl ContextEditor {
} else if message.offset_range.end >= selection.range().start {
let range = cmp::max(message.offset_range.start, selection.range().start)
..cmp::min(message.offset_range.end, selection.range().end);
if !range.is_empty() {
if range.is_empty() {
let snapshot = context.buffer().read(cx).snapshot();
let point = snapshot.offset_to_point(range.start);
selection.start = snapshot.point_to_offset(Point::new(point.row, 0));
selection.end = snapshot.point_to_offset(cmp::min(
Point::new(point.row + 1, 0),
snapshot.max_point(),
));
for chunk in context.buffer().read(cx).text_for_range(selection.range()) {
text.push_str(chunk);
}
} else {
for chunk in context.buffer().read(cx).text_for_range(range) {
text.push_str(chunk);
}
@@ -2107,9 +2114,6 @@ impl ContextEditor {
.occlude()
.child(match last_error {
AssistError::PaymentRequired => self.render_payment_required_error(cx),
AssistError::MaxMonthlySpendReached => {
self.render_max_monthly_spend_reached_error(cx)
}
AssistError::Message(error_message) => {
self.render_assist_error(error_message, cx)
}
@@ -2158,48 +2162,6 @@ impl ContextEditor {
.into_any()
}
fn render_max_monthly_spend_reached_error(&self, cx: &mut Context<Self>) -> AnyElement {
const ERROR_MESSAGE: &str = "You have reached your maximum monthly spend. Increase your spend limit to continue using Zed LLMs.";
v_flex()
.gap_0p5()
.child(
h_flex()
.gap_1p5()
.items_center()
.child(Icon::new(IconName::XCircle).color(Color::Error))
.child(Label::new("Max Monthly Spend Reached").weight(FontWeight::MEDIUM)),
)
.child(
div()
.id("error-message")
.max_h_24()
.overflow_y_scroll()
.child(Label::new(ERROR_MESSAGE)),
)
.child(
h_flex()
.justify_end()
.mt_1()
.child(
Button::new("subscribe", "Update Monthly Spend Limit").on_click(
cx.listener(|this, _, _window, cx| {
this.last_error = None;
cx.open_url(&zed_urls::account_url(cx));
cx.notify();
}),
),
)
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|this, _, _window, cx| {
this.last_error = None;
cx.notify();
},
))),
)
.into_any()
}
fn render_assist_error(
&self,
error_message: &SharedString,
@@ -3251,9 +3213,77 @@ pub fn make_lsp_adapter_delegate(
#[cfg(test)]
mod tests {
use super::*;
use gpui::App;
use language::Buffer;
use fs::FakeFs;
use gpui::{App, TestAppContext, VisualTestContext};
use language::{Buffer, LanguageRegistry};
use prompt_store::PromptBuilder;
use unindent::Unindent;
use util::path;
#[gpui::test]
async fn test_copy_paste_no_selection(cx: &mut TestAppContext) {
cx.update(init_test);
let fs = FakeFs::new(cx.executor());
let registry = Arc::new(LanguageRegistry::test(cx.executor()));
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let context = cx.new(|cx| {
AssistantContext::local(
registry,
None,
None,
prompt_builder.clone(),
Arc::new(SlashCommandWorkingSet::default()),
cx,
)
});
let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
let workspace = window.root(cx).unwrap();
let cx = &mut VisualTestContext::from_window(*window, cx);
let context_editor = window
.update(cx, |_, window, cx| {
cx.new(|cx| {
ContextEditor::for_context(
context,
fs,
workspace.downgrade(),
project,
None,
window,
cx,
)
})
})
.unwrap();
context_editor.update_in(cx, |context_editor, window, cx| {
context_editor.editor.update(cx, |editor, cx| {
editor.set_text("abc\ndef\nghi", window, cx);
editor.move_to_beginning(&Default::default(), window, cx);
})
});
context_editor.update_in(cx, |context_editor, window, cx| {
context_editor.editor.update(cx, |editor, cx| {
editor.copy(&Default::default(), window, cx);
editor.paste(&Default::default(), window, cx);
assert_eq!(editor.text(cx), "abc\nabc\ndef\nghi");
})
});
context_editor.update_in(cx, |context_editor, window, cx| {
context_editor.editor.update(cx, |editor, cx| {
editor.cut(&Default::default(), window, cx);
assert_eq!(editor.text(cx), "abc\ndef\nghi");
editor.paste(&Default::default(), window, cx);
assert_eq!(editor.text(cx), "abc\nabc\ndef\nghi");
})
});
}
#[gpui::test]
fn test_find_code_blocks(cx: &mut App) {
@@ -3328,4 +3358,17 @@ mod tests {
assert_eq!(range, expected, "unexpected result on row {:?}", row);
}
}
fn init_test(cx: &mut App) {
let settings_store = SettingsStore::test(cx);
prompt_store::init(cx);
LanguageModelRegistry::test(cx);
cx.set_global(settings_store);
language::init(cx);
assistant_settings::init(cx);
Project::init_settings(cx);
theme::init(theme::LoadThemes::JustBase, cx);
workspace::init_settings(cx);
editor::init_settings(cx);
}
}

View File

@@ -278,8 +278,8 @@ impl CompletionProvider for SlashCommandCompletionProvider {
buffer.anchor_after(Point::new(position.row, first_arg_start.start as u32));
let arguments = call
.arguments
.iter()
.filter_map(|argument| Some(line.get(argument.clone())?.to_string()))
.into_iter()
.filter_map(|argument| Some(line.get(argument)?.to_string()))
.collect::<Vec<_>>();
let argument_range = first_arg_start..buffer_position;
(

View File

@@ -41,6 +41,7 @@ pub enum NotifyWhenAgentWaiting {
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(tag = "name", rename_all = "snake_case")]
#[schemars(deny_unknown_fields)]
pub enum AssistantProviderContentV1 {
#[serde(rename = "zed.dev")]
ZedDotDev { default_model: Option<CloudModel> },
@@ -543,6 +544,7 @@ impl AssistantSettingsContent {
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
#[serde(tag = "version")]
#[schemars(deny_unknown_fields)]
pub enum VersionedAssistantSettingsContent {
#[serde(rename = "1")]
V1(AssistantSettingsContentV1),
@@ -576,6 +578,7 @@ impl Default for VersionedAssistantSettingsContent {
}
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, Default)]
#[schemars(deny_unknown_fields)]
pub struct AssistantSettingsContentV2 {
/// Whether the Assistant is enabled.
///
@@ -734,6 +737,7 @@ pub struct ContextServerPresetContent {
}
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
#[schemars(deny_unknown_fields)]
pub struct AssistantSettingsContentV1 {
/// Whether the Assistant is enabled.
///
@@ -763,6 +767,7 @@ pub struct AssistantSettingsContentV1 {
}
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
#[schemars(deny_unknown_fields)]
pub struct LegacyAssistantSettingsContent {
/// Whether to show the assistant panel button in the status bar.
///

View File

@@ -19,6 +19,7 @@ use gpui::Window;
use gpui::{App, Entity, SharedString, Task, WeakEntity};
use icons::IconName;
use language_model::LanguageModel;
use language_model::LanguageModelImage;
use language_model::LanguageModelRequest;
use language_model::LanguageModelToolSchemaFormat;
use project::Project;
@@ -65,21 +66,50 @@ impl ToolUseStatus {
#[derive(Debug)]
pub struct ToolResultOutput {
pub content: String,
pub content: ToolResultContent,
pub output: Option<serde_json::Value>,
}
#[derive(Debug, PartialEq, Eq)]
pub enum ToolResultContent {
Text(String),
Image(LanguageModelImage),
}
impl ToolResultContent {
pub fn len(&self) -> usize {
match self {
ToolResultContent::Text(str) => str.len(),
ToolResultContent::Image(image) => image.len(),
}
}
pub fn is_empty(&self) -> bool {
match self {
ToolResultContent::Text(str) => str.is_empty(),
ToolResultContent::Image(image) => image.is_empty(),
}
}
pub fn as_str(&self) -> Option<&str> {
match self {
ToolResultContent::Text(str) => Some(str),
ToolResultContent::Image(_) => None,
}
}
}
impl From<String> for ToolResultOutput {
fn from(value: String) -> Self {
ToolResultOutput {
content: value,
content: ToolResultContent::Text(value),
output: None,
}
}
}
impl Deref for ToolResultOutput {
type Target = String;
type Target = ToolResultContent;
fn deref(&self) -> &Self::Target {
&self.content

View File

@@ -10,8 +10,8 @@ use futures::{FutureExt, future::LocalBoxFuture};
use gpui::{AppContext, TestAppContext};
use indoc::{formatdoc, indoc};
use language_model::{
LanguageModelRegistry, LanguageModelRequestTool, LanguageModelToolResult, LanguageModelToolUse,
LanguageModelToolUseId,
LanguageModelRegistry, LanguageModelRequestTool, LanguageModelToolResult,
LanguageModelToolResultContent, LanguageModelToolUse, LanguageModelToolUseId,
};
use project::Project;
use rand::prelude::*;
@@ -951,7 +951,7 @@ fn tool_result(
tool_use_id: LanguageModelToolUseId::from(id.into()),
tool_name: name.into(),
is_error: false,
content: result.into(),
content: LanguageModelToolResultContent::Text(result.into()),
output: None,
})
}

View File

@@ -5,7 +5,8 @@ use crate::{
};
use anyhow::{Result, anyhow};
use assistant_tool::{
ActionLog, AnyToolCard, Tool, ToolCard, ToolResult, ToolResultOutput, ToolUseStatus,
ActionLog, AnyToolCard, Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput,
ToolUseStatus,
};
use buffer_diff::{BufferDiff, BufferDiffSnapshot};
use editor::{Editor, EditorMode, MultiBuffer, PathKey};
@@ -292,7 +293,10 @@ impl Tool for EditFileTool {
}
} else {
Ok(ToolResultOutput {
content: format!("Edited {}:\n\n```diff\n{}\n```", input_path, diff),
content: ToolResultContent::Text(format!(
"Edited {}:\n\n```diff\n{}\n```",
input_path, diff
)),
output: serde_json::to_value(output).ok(),
})
}

View File

@@ -1,6 +1,8 @@
use crate::{schema::json_schema_for, ui::ToolCallCardHeader};
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolCard, ToolResult, ToolUseStatus};
use assistant_tool::{
ActionLog, Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput, ToolUseStatus,
};
use editor::Editor;
use futures::channel::oneshot::{self, Receiver};
use gpui::{
@@ -38,6 +40,12 @@ pub struct FindPathToolInput {
pub offset: usize,
}
#[derive(Debug, Serialize, Deserialize)]
struct FindPathToolOutput {
glob: String,
paths: Vec<PathBuf>,
}
const RESULTS_PER_PAGE: usize = 50;
pub struct FindPathTool;
@@ -111,10 +119,18 @@ impl Tool for FindPathTool {
)
.unwrap();
}
let output = FindPathToolOutput {
glob,
paths: matches.clone(),
};
for mat in matches.into_iter().skip(offset).take(RESULTS_PER_PAGE) {
write!(&mut message, "\n{}", mat.display()).unwrap();
}
Ok(message.into())
Ok(ToolResultOutput {
content: ToolResultContent::Text(message),
output: Some(serde_json::to_value(output)?),
})
}
});
@@ -123,6 +139,18 @@ impl Tool for FindPathTool {
card: Some(card.into()),
}
}
fn deserialize_card(
self: Arc<Self>,
output: serde_json::Value,
_project: Entity<Project>,
_window: &mut Window,
cx: &mut App,
) -> Option<assistant_tool::AnyToolCard> {
let output = serde_json::from_value::<FindPathToolOutput>(output).ok()?;
let card = cx.new(|_| FindPathToolCard::from_output(output));
Some(card.into())
}
}
fn search_paths(glob: &str, project: Entity<Project>, cx: &mut App) -> Task<Result<Vec<PathBuf>>> {
@@ -180,6 +208,15 @@ impl FindPathToolCard {
_receiver_task: Some(_receiver_task),
}
}
fn from_output(output: FindPathToolOutput) -> Self {
Self {
glob: output.glob,
paths: output.paths,
expanded: false,
_receiver_task: None,
}
}
}
impl ToolCard for FindPathToolCard {

View File

@@ -752,9 +752,9 @@ mod tests {
match task.output.await {
Ok(result) => {
if cfg!(windows) {
result.content.replace("root\\", "root/")
result.content.as_str().unwrap().replace("root\\", "root/")
} else {
result.content
result.content.as_str().unwrap().to_string()
}
}
Err(e) => panic!("Failed to run grep tool: {}", e),

View File

@@ -1,13 +1,17 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::outline;
use assistant_tool::{ActionLog, Tool, ToolResult};
use assistant_tool::{ToolResultContent, outline};
use gpui::{AnyWindowHandle, App, Entity, Task};
use project::{ImageItem, image_store};
use assistant_tool::ToolResultOutput;
use indoc::formatdoc;
use itertools::Itertools;
use language::{Anchor, Point};
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
use language_model::{
LanguageModel, LanguageModelImage, LanguageModelRequest, LanguageModelToolSchemaFormat,
};
use project::{AgentLocation, Project};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -86,7 +90,7 @@ impl Tool for ReadFileTool {
_request: Arc<LanguageModelRequest>,
project: Entity<Project>,
action_log: Entity<ActionLog>,
_model: Arc<dyn LanguageModel>,
model: Arc<dyn LanguageModel>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
@@ -100,6 +104,42 @@ impl Tool for ReadFileTool {
};
let file_path = input.path.clone();
if image_store::is_image_file(&project, &project_path, cx) {
if !model.supports_images() {
return Task::ready(Err(anyhow!(
"Attempted to read an image, but Zed doesn't currently sending images to {}.",
model.name().0
)))
.into();
}
let task = cx.spawn(async move |cx| -> Result<ToolResultOutput> {
let image_entity: Entity<ImageItem> = cx
.update(|cx| {
project.update(cx, |project, cx| {
project.open_image(project_path.clone(), cx)
})
})?
.await?;
let image =
image_entity.read_with(cx, |image_item, _| Arc::clone(&image_item.image))?;
let language_model_image = cx
.update(|cx| LanguageModelImage::from_image(image, cx))?
.await
.ok_or_else(|| anyhow!("Failed to process image"))?;
Ok(ToolResultOutput {
content: ToolResultContent::Image(language_model_image),
output: None,
})
});
return task.into();
}
cx.spawn(async move |cx| {
let buffer = cx
.update(|cx| {
@@ -282,7 +322,10 @@ mod test {
.output
})
.await;
assert_eq!(result.unwrap().content, "This is a small file content");
assert_eq!(
result.unwrap().content.as_str(),
Some("This is a small file content")
);
}
#[gpui::test]
@@ -322,6 +365,7 @@ mod test {
})
.await;
let content = result.unwrap();
let content = content.as_str().unwrap();
assert_eq!(
content.lines().skip(4).take(6).collect::<Vec<_>>(),
vec![
@@ -365,6 +409,8 @@ mod test {
.collect::<Vec<_>>();
pretty_assertions::assert_eq!(
content
.as_str()
.unwrap()
.lines()
.skip(4)
.take(expected_content.len())
@@ -408,7 +454,10 @@ mod test {
.output
})
.await;
assert_eq!(result.unwrap().content, "Line 2\nLine 3\nLine 4");
assert_eq!(
result.unwrap().content.as_str(),
Some("Line 2\nLine 3\nLine 4")
);
}
#[gpui::test]
@@ -448,7 +497,7 @@ mod test {
.output
})
.await;
assert_eq!(result.unwrap().content, "Line 1\nLine 2");
assert_eq!(result.unwrap().content.as_str(), Some("Line 1\nLine 2"));
// end_line of 0 should result in at least 1 line
let result = cx
@@ -471,7 +520,7 @@ mod test {
.output
})
.await;
assert_eq!(result.unwrap().content, "Line 1");
assert_eq!(result.unwrap().content.as_str(), Some("Line 1"));
// when start_line > end_line, should still return at least 1 line
let result = cx
@@ -494,7 +543,7 @@ mod test {
.output
})
.await;
assert_eq!(result.unwrap().content, "Line 3");
assert_eq!(result.unwrap().content.as_str(), Some("Line 3"));
}
fn init_test(cx: &mut TestAppContext) {

View File

@@ -1,5 +1,5 @@
use crate::schema::json_schema_for;
use anyhow::{Context as _, Result, anyhow, bail};
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolCard, ToolResult, ToolUseStatus};
use futures::{FutureExt as _, future::Shared};
use gpui::{
@@ -125,14 +125,24 @@ impl Tool for TerminalTool {
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
};
let input_path = Path::new(&input.cd);
let working_dir = match working_dir(&input, &project, input_path, cx) {
let working_dir = match working_dir(&input, &project, cx) {
Ok(dir) => dir,
Err(err) => return Task::ready(Err(err)).into(),
};
let program = self.determine_shell.clone();
let command = format!("({}) </dev/null", input.command);
let args = vec!["-c".into(), command.clone()];
let command = if cfg!(windows) {
format!("$null | & {{{}}}", input.command.replace("\"", "'"))
} else if let Some(cwd) = working_dir
.as_ref()
.and_then(|cwd| cwd.as_os_str().to_str())
{
// Make sure once we're *inside* the shell, we cd into `cwd`
format!("(cd {cwd}; {}) </dev/null", input.command)
} else {
format!("({}) </dev/null", input.command)
};
let args = vec!["-c".into(), command];
let cwd = working_dir.clone();
let env = match &working_dir {
Some(dir) => project.update(cx, |project, cx| {
@@ -315,19 +325,13 @@ fn process_content(
} else {
content
};
let is_empty = content.trim().is_empty();
let content = format!(
"```\n{}{}```",
content,
if content.ends_with('\n') { "" } else { "\n" }
);
let content = content.trim();
let is_empty = content.is_empty();
let content = format!("```\n{content}\n```");
let content = if should_truncate {
format!(
"Command output too long. The first {} bytes:\n\n{}",
"Command output too long. The first {} bytes:\n\n{content}",
content.len(),
content,
)
} else {
content
@@ -367,42 +371,47 @@ fn process_content(
fn working_dir(
input: &TerminalToolInput,
project: &Entity<Project>,
input_path: &Path,
cx: &mut App,
) -> Result<Option<PathBuf>> {
let project = project.read(cx);
let cd = &input.cd;
if input.cd == "." {
// Accept "." as meaning "the one worktree" if we only have one worktree.
if cd == "." || cd == "" {
// Accept "." or "" as meaning "the one worktree" if we only have one worktree.
let mut worktrees = project.worktrees(cx);
match worktrees.next() {
Some(worktree) => {
if worktrees.next().is_some() {
bail!(
if worktrees.next().is_none() {
Ok(Some(worktree.read(cx).abs_path().to_path_buf()))
} else {
Err(anyhow!(
"'.' is ambiguous in multi-root workspaces. Please specify a root directory explicitly.",
);
))
}
Ok(Some(worktree.read(cx).abs_path().to_path_buf()))
}
None => Ok(None),
}
} else if input_path.is_absolute() {
// Absolute paths are allowed, but only if they're in one of the project's worktrees.
if !project
.worktrees(cx)
.any(|worktree| input_path.starts_with(&worktree.read(cx).abs_path()))
{
bail!("The absolute path must be within one of the project's worktrees");
} else {
let input_path = Path::new(cd);
if input_path.is_absolute() {
// Absolute paths are allowed, but only if they're in one of the project's worktrees.
if project
.worktrees(cx)
.any(|worktree| input_path.starts_with(&worktree.read(cx).abs_path()))
{
return Ok(Some(input_path.into()));
}
} else {
if let Some(worktree) = project.worktree_for_root_name(cd, cx) {
return Ok(Some(worktree.read(cx).abs_path().to_path_buf()));
}
}
Ok(Some(input_path.into()))
} else {
let Some(worktree) = project.worktree_for_root_name(&input.cd, cx) else {
bail!("`cd` directory {:?} not found in the project", input.cd);
};
Ok(Some(worktree.read(cx).abs_path().to_path_buf()))
Err(anyhow!(
"`cd` directory {cd:?} was not in any of the project's worktrees."
))
}
}
@@ -723,8 +732,8 @@ mod tests {
)
});
let output = result.output.await.log_err().map(|output| output.content);
assert_eq!(output, Some("Command executed successfully.".into()));
let output = result.output.await.log_err().unwrap().content;
assert_eq!(output.as_str().unwrap(), "Command executed successfully.");
}
#[gpui::test]
@@ -757,12 +766,13 @@ mod tests {
cx,
);
cx.spawn(async move |_| {
let output = headless_result
.output
.await
.log_err()
.map(|output| output.content);
assert_eq!(output, expected);
let output = headless_result.output.await.map(|output| output.content);
assert_eq!(
output
.ok()
.and_then(|content| content.as_str().map(ToString::to_string)),
expected
);
})
};
@@ -770,7 +780,7 @@ mod tests {
check(
TerminalToolInput {
command: "pwd".into(),
cd: "project".into(),
cd: ".".into(),
},
Some(format!(
"```\n{}\n```",
@@ -785,12 +795,9 @@ mod tests {
check(
TerminalToolInput {
command: "pwd".into(),
cd: ".".into(),
cd: "other-project".into(),
},
Some(format!(
"```\n{}\n```",
tree.path().join("project").display()
)),
None, // other-project is a dir, but *not* a worktree (yet)
cx,
)
})

View File

@@ -3,7 +3,9 @@ use std::{sync::Arc, time::Duration};
use crate::schema::json_schema_for;
use crate::ui::ToolCallCardHeader;
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolCard, ToolResult, ToolUseStatus};
use assistant_tool::{
ActionLog, Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput, ToolUseStatus,
};
use futures::{Future, FutureExt, TryFutureExt};
use gpui::{
AnyWindowHandle, App, AppContext, Context, Entity, IntoElement, Task, WeakEntity, Window,
@@ -73,9 +75,13 @@ impl Tool for WebSearchTool {
let search_task = search_task.clone();
async move {
let response = search_task.await.map_err(|err| anyhow!(err))?;
serde_json::to_string(&response)
.context("Failed to serialize search results")
.map(Into::into)
Ok(ToolResultOutput {
content: ToolResultContent::Text(
serde_json::to_string(&response)
.context("Failed to serialize search results")?,
),
output: Some(serde_json::to_value(response)?),
})
}
});
@@ -84,6 +90,18 @@ impl Tool for WebSearchTool {
card: Some(cx.new(|cx| WebSearchToolCard::new(search_task, cx)).into()),
}
}
fn deserialize_card(
self: Arc<Self>,
output: serde_json::Value,
_project: Entity<Project>,
_window: &mut Window,
cx: &mut App,
) -> Option<assistant_tool::AnyToolCard> {
let output = serde_json::from_value::<WebSearchResponse>(output).ok()?;
let card = cx.new(|cx| WebSearchToolCard::new(Task::ready(Ok(output)), cx));
Some(card.into())
}
}
#[derive(RegisterComponent)]

View File

@@ -38,6 +38,7 @@ pub enum Model {
AmazonNovaLite,
AmazonNovaMicro,
AmazonNovaPro,
AmazonNovaPremier,
// AI21 models
AI21J2GrandeInstruct,
AI21J2JumboInstruct,
@@ -72,6 +73,10 @@ pub enum Model {
MistralMixtral8x7BInstructV0,
MistralMistralLarge2402V1,
MistralMistralSmall2402V1,
MistralPixtralLarge2502V1,
// Writer models
PalmyraWriterX5,
PalmyraWriterX4,
#[serde(rename = "custom")]
Custom {
name: String,
@@ -120,6 +125,7 @@ impl Model {
Model::AmazonNovaLite => "amazon.nova-lite-v1:0",
Model::AmazonNovaMicro => "amazon.nova-micro-v1:0",
Model::AmazonNovaPro => "amazon.nova-pro-v1:0",
Model::AmazonNovaPremier => "amazon.nova-premier-v1:0",
Model::DeepSeekR1 => "us.deepseek.r1-v1:0",
Model::AI21J2GrandeInstruct => "ai21.j2-grande-instruct",
Model::AI21J2JumboInstruct => "ai21.j2-jumbo-instruct",
@@ -149,6 +155,9 @@ impl Model {
Model::MistralMixtral8x7BInstructV0 => "mistral.mixtral-8x7b-instruct-v0:1",
Model::MistralMistralLarge2402V1 => "mistral.mistral-large-2402-v1:0",
Model::MistralMistralSmall2402V1 => "mistral.mistral-small-2402-v1:0",
Model::MistralPixtralLarge2502V1 => "mistral.pixtral-large-2502-v1:0",
Model::PalmyraWriterX4 => "writer.palmyra-x4-v1:0",
Model::PalmyraWriterX5 => "writer.palmyra-x5-v1:0",
Self::Custom { name, .. } => name,
}
}
@@ -166,6 +175,7 @@ impl Model {
Self::AmazonNovaLite => "Amazon Nova Lite",
Self::AmazonNovaMicro => "Amazon Nova Micro",
Self::AmazonNovaPro => "Amazon Nova Pro",
Self::AmazonNovaPremier => "Amazon Nova Premier",
Self::DeepSeekR1 => "DeepSeek R1",
Self::AI21J2GrandeInstruct => "AI21 Jurassic2 Grande Instruct",
Self::AI21J2JumboInstruct => "AI21 Jurassic2 Jumbo Instruct",
@@ -195,6 +205,9 @@ impl Model {
Self::MistralMixtral8x7BInstructV0 => "Mistral Mixtral 8x7B Instruct V0",
Self::MistralMistralLarge2402V1 => "Mistral Large 2402 V1",
Self::MistralMistralSmall2402V1 => "Mistral Small 2402 V1",
Self::MistralPixtralLarge2502V1 => "Pixtral Large 25.02 V1",
Self::PalmyraWriterX5 => "Writer Palmyra X5",
Self::PalmyraWriterX4 => "Writer Palmyra X4",
Self::Custom {
display_name, name, ..
} => display_name.as_deref().unwrap_or(name),
@@ -208,8 +221,11 @@ impl Model {
| Self::Claude3Sonnet
| Self::Claude3_5Haiku
| Self::Claude3_7Sonnet => 200_000,
Self::AmazonNovaPremier => 1_000_000,
Self::PalmyraWriterX5 => 1_000_000,
Self::PalmyraWriterX4 => 128_000,
Self::Custom { max_tokens, .. } => *max_tokens,
_ => 200_000,
_ => 128_000,
}
}
@@ -217,7 +233,7 @@ impl Model {
match self {
Self::Claude3Opus | Self::Claude3Sonnet | Self::Claude3_5Haiku => 4_096,
Self::Claude3_7Sonnet | Self::Claude3_7SonnetThinking => 128_000,
Self::Claude3_5SonnetV2 => 8_192,
Self::Claude3_5SonnetV2 | Self::PalmyraWriterX4 | Self::PalmyraWriterX5 => 8_192,
Self::Custom {
max_output_tokens, ..
} => max_output_tokens.unwrap_or(4_096),
@@ -252,7 +268,10 @@ impl Model {
| Self::Claude3_5Haiku => true,
// Amazon Nova models (all support tool use)
Self::AmazonNovaPro | Self::AmazonNovaLite | Self::AmazonNovaMicro => true,
Self::AmazonNovaPremier
| Self::AmazonNovaPro
| Self::AmazonNovaLite
| Self::AmazonNovaMicro => true,
// AI21 Jamba 1.5 models support tool use
Self::AI21Jamba15LargeV1 | Self::AI21Jamba15MiniV1 => true,
@@ -305,8 +324,11 @@ impl Model {
// Models available only in US
(Model::Claude3Opus, "us")
| (Model::Claude3_5Haiku, "us")
| (Model::Claude3_7Sonnet, "us")
| (Model::Claude3_7SonnetThinking, "us") => {
| (Model::Claude3_7SonnetThinking, "us")
| (Model::AmazonNovaPremier, "us")
| (Model::MistralPixtralLarge2502V1, "us") => {
Ok(format!("{}.{}", region_group, model_id))
}
@@ -340,6 +362,12 @@ impl Model {
Ok(format!("{}.{}", region_group, model_id))
}
// Writer models only available in the US
(Model::PalmyraWriterX4, "us") | (Model::PalmyraWriterX5, "us") => {
// They have some goofiness
Ok(format!("{}.{}", region_group, model_id))
}
// Any other combination is not supported
_ => Ok(self.id().into()),
}

View File

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

View File

@@ -378,16 +378,27 @@ impl CollabPanel {
workspace: WeakEntity<Workspace>,
mut cx: AsyncWindowContext,
) -> anyhow::Result<Entity<Self>> {
let serialized_panel = cx
.background_spawn(async move { KEY_VALUE_STORE.read_kvp(COLLABORATION_PANEL_KEY) })
.await
.map_err(|_| anyhow::anyhow!("Failed to read collaboration panel from key value store"))
.log_err()
let serialized_panel = match workspace
.read_with(&cx, |workspace, _| {
CollabPanel::serialization_key(workspace)
})
.ok()
.flatten()
.map(|panel| serde_json::from_str::<SerializedCollabPanel>(&panel))
.transpose()
.log_err()
.flatten();
{
Some(serialization_key) => cx
.background_spawn(async move { KEY_VALUE_STORE.read_kvp(&serialization_key) })
.await
.map_err(|_| {
anyhow::anyhow!("Failed to read collaboration panel from key value store")
})
.log_err()
.flatten()
.map(|panel| serde_json::from_str::<SerializedCollabPanel>(&panel))
.transpose()
.log_err()
.flatten(),
None => None,
};
workspace.update_in(&mut cx, |workspace, window, cx| {
let panel = CollabPanel::new(workspace, window, cx);
@@ -407,14 +418,30 @@ impl CollabPanel {
})
}
fn serialization_key(workspace: &Workspace) -> Option<String> {
workspace
.database_id()
.map(|id| i64::from(id).to_string())
.or(workspace.session_id())
.map(|id| format!("{}-{:?}", COLLABORATION_PANEL_KEY, id))
}
fn serialize(&mut self, cx: &mut Context<Self>) {
let Some(serialization_key) = self
.workspace
.update(cx, |workspace, _| CollabPanel::serialization_key(workspace))
.ok()
.flatten()
else {
return;
};
let width = self.width;
let collapsed_channels = self.collapsed_channels.clone();
self.pending_serialization = cx.background_spawn(
async move {
KEY_VALUE_STORE
.write_kvp(
COLLABORATION_PANEL_KEY.into(),
serialization_key,
serde_json::to_string(&SerializedCollabPanel {
width,
collapsed_channels: Some(
@@ -2999,10 +3026,12 @@ impl Panel for CollabPanel {
.unwrap_or_else(|| CollaborationPanelSettings::get_global(cx).default_width)
}
fn set_size(&mut self, size: Option<Pixels>, _: &mut Window, cx: &mut Context<Self>) {
fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
self.width = size;
self.serialize(cx);
cx.notify();
cx.defer_in(window, |this, _, cx| {
this.serialize(cx);
});
}
fn icon(&self, _window: &Window, cx: &App) -> Option<ui::IconName> {

View File

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

View File

@@ -14,7 +14,6 @@ doctest = false
[features]
default = []
schemars = ["dep:schemars"]
test-support = [
"collections/test-support",
"gpui/test-support",
@@ -43,16 +42,15 @@ node_runtime.workspace = true
parking_lot.workspace = true
paths.workspace = true
project.workspace = true
schemars = { workspace = true, optional = true }
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
strum.workspace = true
task.workspace = true
ui.workspace = true
util.workspace = true
workspace.workspace = true
workspace-hack.workspace = true
itertools.workspace = true
[target.'cfg(windows)'.dependencies]
async-std = { version = "1.12.0", features = ["unstable"] }

View File

@@ -9,13 +9,20 @@ use fs::Fs;
use futures::{AsyncBufReadExt, AsyncReadExt, StreamExt, io::BufReader, stream::BoxStream};
use gpui::{App, AsyncApp, Global, prelude::*};
use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest};
use itertools::Itertools;
use paths::home_dir;
use serde::{Deserialize, Serialize};
use settings::watch_config_dir;
use strum::EnumIter;
pub const COPILOT_CHAT_COMPLETION_URL: &str = "https://api.githubcopilot.com/chat/completions";
pub const COPILOT_CHAT_AUTH_URL: &str = "https://api.github.com/copilot_internal/v2/token";
pub const COPILOT_CHAT_MODELS_URL: &str = "https://api.githubcopilot.com/models";
// Copilot's base model; defined by Microsoft in premium requests table
// This will be moved to the front of the Copilot model list, and will be used for
// 'fast' requests (e.g. title generation)
// https://docs.github.com/en/copilot/managing-copilot/monitoring-usage-and-entitlements/about-premium-requests
const DEFAULT_MODEL_ID: &str = "gpt-4.1";
#[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)]
#[serde(rename_all = "lowercase")]
@@ -25,132 +32,130 @@ pub enum Role {
System,
}
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, EnumIter)]
pub enum Model {
#[default]
#[serde(alias = "gpt-4o", rename = "gpt-4o-2024-05-13")]
Gpt4o,
#[serde(alias = "gpt-4", rename = "gpt-4")]
Gpt4,
#[serde(alias = "gpt-4.1", rename = "gpt-4.1")]
Gpt4_1,
#[serde(alias = "gpt-3.5-turbo", rename = "gpt-3.5-turbo")]
Gpt3_5Turbo,
#[serde(alias = "o1", rename = "o1")]
O1,
#[serde(alias = "o1-mini", rename = "o3-mini")]
O3Mini,
#[serde(alias = "o3", rename = "o3")]
O3,
#[serde(alias = "o4-mini", rename = "o4-mini")]
O4Mini,
#[serde(alias = "claude-3-5-sonnet", rename = "claude-3.5-sonnet")]
Claude3_5Sonnet,
#[serde(alias = "claude-3-7-sonnet", rename = "claude-3.7-sonnet")]
Claude3_7Sonnet,
#[serde(
alias = "claude-3.7-sonnet-thought",
rename = "claude-3.7-sonnet-thought"
)]
Claude3_7SonnetThinking,
#[serde(alias = "gemini-2.0-flash", rename = "gemini-2.0-flash-001")]
Gemini20Flash,
#[serde(alias = "gemini-2.5-pro", rename = "gemini-2.5-pro")]
Gemini25Pro,
#[derive(Deserialize)]
struct ModelSchema {
#[serde(deserialize_with = "deserialize_models_skip_errors")]
data: Vec<Model>,
}
fn deserialize_models_skip_errors<'de, D>(deserializer: D) -> Result<Vec<Model>, D::Error>
where
D: serde::Deserializer<'de>,
{
let raw_values = Vec::<serde_json::Value>::deserialize(deserializer)?;
let models = raw_values
.into_iter()
.filter_map(|value| match serde_json::from_value::<Model>(value) {
Ok(model) => Some(model),
Err(err) => {
log::warn!("GitHub Copilot Chat model failed to deserialize: {:?}", err);
None
}
})
.collect();
Ok(models)
}
#[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)]
pub struct Model {
capabilities: ModelCapabilities,
id: String,
name: String,
policy: Option<ModelPolicy>,
vendor: ModelVendor,
model_picker_enabled: bool,
}
#[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)]
struct ModelCapabilities {
family: String,
#[serde(default)]
limits: ModelLimits,
supports: ModelSupportedFeatures,
}
#[derive(Default, Clone, Serialize, Deserialize, Debug, Eq, PartialEq)]
struct ModelLimits {
#[serde(default)]
max_context_window_tokens: usize,
#[serde(default)]
max_output_tokens: usize,
#[serde(default)]
max_prompt_tokens: usize,
}
#[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)]
struct ModelPolicy {
state: String,
}
#[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)]
struct ModelSupportedFeatures {
#[serde(default)]
streaming: bool,
#[serde(default)]
tool_calls: bool,
#[serde(default)]
parallel_tool_calls: bool,
#[serde(default)]
vision: bool,
}
#[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)]
pub enum ModelVendor {
// Azure OpenAI should have no functional difference from OpenAI in Copilot Chat
#[serde(alias = "Azure OpenAI")]
OpenAI,
Google,
Anthropic,
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)]
#[serde(tag = "type")]
pub enum ChatMessagePart {
#[serde(rename = "text")]
Text { text: String },
#[serde(rename = "image_url")]
Image { image_url: ImageUrl },
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)]
pub struct ImageUrl {
pub url: String,
}
impl Model {
pub fn default_fast() -> Self {
Self::Claude3_7Sonnet
}
pub fn uses_streaming(&self) -> bool {
match self {
Self::Gpt4o
| Self::Gpt4
| Self::Gpt4_1
| Self::Gpt3_5Turbo
| Self::O3
| Self::O4Mini
| Self::Claude3_5Sonnet
| Self::Claude3_7Sonnet
| Self::Claude3_7SonnetThinking => true,
Self::O3Mini | Self::O1 | Self::Gemini20Flash | Self::Gemini25Pro => false,
}
self.capabilities.supports.streaming
}
pub fn from_id(id: &str) -> Result<Self> {
match id {
"gpt-4o" => Ok(Self::Gpt4o),
"gpt-4" => Ok(Self::Gpt4),
"gpt-4.1" => Ok(Self::Gpt4_1),
"gpt-3.5-turbo" => Ok(Self::Gpt3_5Turbo),
"o1" => Ok(Self::O1),
"o3-mini" => Ok(Self::O3Mini),
"o3" => Ok(Self::O3),
"o4-mini" => Ok(Self::O4Mini),
"claude-3-5-sonnet" => Ok(Self::Claude3_5Sonnet),
"claude-3-7-sonnet" => Ok(Self::Claude3_7Sonnet),
"claude-3.7-sonnet-thought" => Ok(Self::Claude3_7SonnetThinking),
"gemini-2.0-flash-001" => Ok(Self::Gemini20Flash),
"gemini-2.5-pro" => Ok(Self::Gemini25Pro),
_ => Err(anyhow!("Invalid model id: {}", id)),
}
pub fn id(&self) -> &str {
self.id.as_str()
}
pub fn id(&self) -> &'static str {
match self {
Self::Gpt3_5Turbo => "gpt-3.5-turbo",
Self::Gpt4 => "gpt-4",
Self::Gpt4_1 => "gpt-4.1",
Self::Gpt4o => "gpt-4o",
Self::O3Mini => "o3-mini",
Self::O1 => "o1",
Self::O3 => "o3",
Self::O4Mini => "o4-mini",
Self::Claude3_5Sonnet => "claude-3-5-sonnet",
Self::Claude3_7Sonnet => "claude-3-7-sonnet",
Self::Claude3_7SonnetThinking => "claude-3.7-sonnet-thought",
Self::Gemini20Flash => "gemini-2.0-flash-001",
Self::Gemini25Pro => "gemini-2.5-pro",
}
}
pub fn display_name(&self) -> &'static str {
match self {
Self::Gpt3_5Turbo => "GPT-3.5",
Self::Gpt4 => "GPT-4",
Self::Gpt4_1 => "GPT-4.1",
Self::Gpt4o => "GPT-4o",
Self::O3Mini => "o3-mini",
Self::O1 => "o1",
Self::O3 => "o3",
Self::O4Mini => "o4-mini",
Self::Claude3_5Sonnet => "Claude 3.5 Sonnet",
Self::Claude3_7Sonnet => "Claude 3.7 Sonnet",
Self::Claude3_7SonnetThinking => "Claude 3.7 Sonnet Thinking",
Self::Gemini20Flash => "Gemini 2.0 Flash",
Self::Gemini25Pro => "Gemini 2.5 Pro",
}
pub fn display_name(&self) -> &str {
self.name.as_str()
}
pub fn max_token_count(&self) -> usize {
match self {
Self::Gpt4o => 64_000,
Self::Gpt4 => 32_768,
Self::Gpt4_1 => 128_000,
Self::Gpt3_5Turbo => 12_288,
Self::O3Mini => 64_000,
Self::O1 => 20_000,
Self::O3 => 128_000,
Self::O4Mini => 128_000,
Self::Claude3_5Sonnet => 200_000,
Self::Claude3_7Sonnet => 90_000,
Self::Claude3_7SonnetThinking => 90_000,
Self::Gemini20Flash => 128_000,
Self::Gemini25Pro => 128_000,
}
self.capabilities.limits.max_prompt_tokens
}
pub fn supports_tools(&self) -> bool {
self.capabilities.supports.tool_calls
}
pub fn vendor(&self) -> ModelVendor {
self.vendor
}
pub fn supports_vision(&self) -> bool {
self.capabilities.supports.vision
}
pub fn supports_parallel_tool_calls(&self) -> bool {
self.capabilities.supports.parallel_tool_calls
}
}
@@ -160,7 +165,7 @@ pub struct Request {
pub n: usize,
pub stream: bool,
pub temperature: f32,
pub model: Model,
pub model: String,
pub messages: Vec<ChatMessage>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tools: Vec<Tool>,
@@ -189,26 +194,55 @@ pub enum ToolChoice {
None,
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
#[derive(Serialize, Deserialize, Debug)]
#[serde(tag = "role", rename_all = "lowercase")]
pub enum ChatMessage {
Assistant {
content: Option<String>,
content: ChatMessageContent,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
tool_calls: Vec<ToolCall>,
},
User {
content: String,
content: ChatMessageContent,
},
System {
content: String,
},
Tool {
content: String,
content: ChatMessageContent,
tool_call_id: String,
},
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ChatMessageContent {
Plain(String),
Multipart(Vec<ChatMessagePart>),
}
impl ChatMessageContent {
pub fn empty() -> Self {
ChatMessageContent::Multipart(vec![])
}
}
impl From<Vec<ChatMessagePart>> for ChatMessageContent {
fn from(mut parts: Vec<ChatMessagePart>) -> Self {
if let [ChatMessagePart::Text { text }] = parts.as_mut_slice() {
ChatMessageContent::Plain(std::mem::take(text))
} else {
ChatMessageContent::Multipart(parts)
}
}
}
impl From<String> for ChatMessageContent {
fn from(text: String) -> Self {
ChatMessageContent::Plain(text)
}
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub struct ToolCall {
pub id: String,
@@ -232,7 +266,6 @@ pub struct FunctionContent {
#[serde(tag = "type", rename_all = "snake_case")]
pub struct ResponseEvent {
pub choices: Vec<ResponseChoice>,
pub created: u64,
pub id: String,
}
@@ -306,6 +339,7 @@ impl Global for GlobalCopilotChat {}
pub struct CopilotChat {
oauth_token: Option<String>,
api_token: Option<ApiToken>,
models: Option<Vec<Model>>,
client: Arc<dyn HttpClient>,
}
@@ -342,31 +376,56 @@ impl CopilotChat {
let config_paths: HashSet<PathBuf> = copilot_chat_config_paths().into_iter().collect();
let dir_path = copilot_chat_config_dir();
cx.spawn(async move |cx| {
let mut parent_watch_rx = watch_config_dir(
cx.background_executor(),
fs.clone(),
dir_path.clone(),
config_paths,
);
while let Some(contents) = parent_watch_rx.next().await {
let oauth_token = extract_oauth_token(contents);
cx.update(|cx| {
if let Some(this) = Self::global(cx).as_ref() {
this.update(cx, |this, cx| {
this.oauth_token = oauth_token;
cx.notify();
});
cx.spawn({
let client = client.clone();
async move |cx| {
let mut parent_watch_rx = watch_config_dir(
cx.background_executor(),
fs.clone(),
dir_path.clone(),
config_paths,
);
while let Some(contents) = parent_watch_rx.next().await {
let oauth_token = extract_oauth_token(contents);
cx.update(|cx| {
if let Some(this) = Self::global(cx).as_ref() {
this.update(cx, |this, cx| {
this.oauth_token = oauth_token.clone();
cx.notify();
});
}
})?;
if let Some(ref oauth_token) = oauth_token {
let api_token = request_api_token(oauth_token, client.clone()).await?;
cx.update(|cx| {
if let Some(this) = Self::global(cx).as_ref() {
this.update(cx, |this, cx| {
this.api_token = Some(api_token.clone());
cx.notify();
});
}
})?;
let models = get_models(api_token.api_key, client.clone()).await?;
cx.update(|cx| {
if let Some(this) = Self::global(cx).as_ref() {
this.update(cx, |this, cx| {
this.models = Some(models);
cx.notify();
});
}
})?;
}
})?;
}
anyhow::Ok(())
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
Self {
oauth_token: None,
api_token: None,
models: None,
client,
}
}
@@ -375,6 +434,10 @@ impl CopilotChat {
self.oauth_token.is_some()
}
pub fn models(&self) -> Option<&[Model]> {
self.models.as_deref()
}
pub async fn stream_completion(
request: Request,
mut cx: AsyncApp,
@@ -409,6 +472,61 @@ impl CopilotChat {
}
}
async fn get_models(api_token: String, client: Arc<dyn HttpClient>) -> Result<Vec<Model>> {
let all_models = request_models(api_token, client).await?;
let mut models: Vec<Model> = all_models
.into_iter()
.filter(|model| {
// Ensure user has access to the model; Policy is present only for models that must be
// enabled in the GitHub dashboard
model.model_picker_enabled
&& model
.policy
.as_ref()
.is_none_or(|policy| policy.state == "enabled")
})
// The first model from the API response, in any given family, appear to be the non-tagged
// models, which are likely the best choice (e.g. gpt-4o rather than gpt-4o-2024-11-20)
.dedup_by(|a, b| a.capabilities.family == b.capabilities.family)
.collect();
if let Some(default_model_position) =
models.iter().position(|model| model.id == DEFAULT_MODEL_ID)
{
let default_model = models.remove(default_model_position);
models.insert(0, default_model);
}
Ok(models)
}
async fn request_models(api_token: String, client: Arc<dyn HttpClient>) -> Result<Vec<Model>> {
let request_builder = HttpRequest::builder()
.method(Method::GET)
.uri(COPILOT_CHAT_MODELS_URL)
.header("Authorization", format!("Bearer {}", api_token))
.header("Content-Type", "application/json")
.header("Copilot-Integration-Id", "vscode-chat");
let request = request_builder.body(AsyncBody::empty())?;
let mut response = client.send(request).await?;
if response.status().is_success() {
let mut body = Vec::new();
response.body_mut().read_to_end(&mut body).await?;
let body_str = std::str::from_utf8(&body)?;
let models = serde_json::from_str::<ModelSchema>(body_str)?.data;
Ok(models)
} else {
Err(anyhow!("Failed to request models: {}", response.status()))
}
}
async fn request_api_token(oauth_token: &str, client: Arc<dyn HttpClient>) -> Result<ApiToken> {
let request_builder = HttpRequest::builder()
.method(Method::GET)
@@ -472,7 +590,8 @@ async fn stream_completion(
)
.header("Authorization", format!("Bearer {}", api_key))
.header("Content-Type", "application/json")
.header("Copilot-Integration-Id", "vscode-chat");
.header("Copilot-Integration-Id", "vscode-chat")
.header("Copilot-Vision-Request", "true");
let is_streaming = request.stream;
@@ -527,3 +646,82 @@ async fn stream_completion(
Ok(futures::stream::once(async move { Ok(response) }).boxed())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_resilient_model_schema_deserialize() {
let json = r#"{
"data": [
{
"capabilities": {
"family": "gpt-4",
"limits": {
"max_context_window_tokens": 32768,
"max_output_tokens": 4096,
"max_prompt_tokens": 32768
},
"object": "model_capabilities",
"supports": { "streaming": true, "tool_calls": true },
"tokenizer": "cl100k_base",
"type": "chat"
},
"id": "gpt-4",
"model_picker_enabled": false,
"name": "GPT 4",
"object": "model",
"preview": false,
"vendor": "Azure OpenAI",
"version": "gpt-4-0613"
},
{
"some-unknown-field": 123
},
{
"capabilities": {
"family": "claude-3.7-sonnet",
"limits": {
"max_context_window_tokens": 200000,
"max_output_tokens": 16384,
"max_prompt_tokens": 90000,
"vision": {
"max_prompt_image_size": 3145728,
"max_prompt_images": 1,
"supported_media_types": ["image/jpeg", "image/png", "image/webp"]
}
},
"object": "model_capabilities",
"supports": {
"parallel_tool_calls": true,
"streaming": true,
"tool_calls": true,
"vision": true
},
"tokenizer": "o200k_base",
"type": "chat"
},
"id": "claude-3.7-sonnet",
"model_picker_enabled": true,
"name": "Claude 3.7 Sonnet",
"object": "model",
"policy": {
"state": "enabled",
"terms": "Enable access to the latest Claude 3.7 Sonnet model from Anthropic. [Learn more about how GitHub Copilot serves Claude 3.7 Sonnet](https://docs.github.com/copilot/using-github-copilot/using-claude-sonnet-in-github-copilot)."
},
"preview": false,
"vendor": "Anthropic",
"version": "claude-3.7-sonnet"
}
],
"object": "list"
}"#;
let schema: ModelSchema = serde_json::from_str(&json).unwrap();
assert_eq!(schema.data.len(), 2);
assert_eq!(schema.data[0].id, "gpt-4");
assert_eq!(schema.data[1].id, "claude-3.7-sonnet");
}
}

View File

@@ -4,11 +4,11 @@ use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive;
use async_trait::async_trait;
use collections::HashMap;
use dap_types::{StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest};
pub use dap_types::{StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest};
use futures::io::BufReader;
use gpui::{AsyncApp, SharedString};
pub use http_client::{HttpClient, github::latest_github_release};
use language::LanguageToolchainStore;
use language::{LanguageName, LanguageToolchainStore};
use node_runtime::NodeRuntime;
use serde::{Deserialize, Serialize};
use settings::WorktreeId;
@@ -418,6 +418,11 @@ pub trait DebugAdapter: 'static + Send + Sync {
user_installed_path: Option<PathBuf>,
cx: &mut AsyncApp,
) -> Result<DebugAdapterBinary>;
/// Returns the language name of an adapter if it only supports one language
fn adapter_language_name(&self) -> Option<LanguageName> {
None
}
}
#[cfg(any(test, feature = "test-support"))]

View File

@@ -7,21 +7,14 @@ use dap_types::{
messages::{Message, Response},
requests::Request,
};
use futures::{FutureExt as _, channel::oneshot, select};
use gpui::{AppContext, AsyncApp, BackgroundExecutor};
use futures::channel::oneshot;
use gpui::{AppContext, AsyncApp};
use smol::channel::{Receiver, Sender};
use std::{
hash::Hash,
sync::atomic::{AtomicU64, Ordering},
time::Duration,
};
#[cfg(any(test, feature = "test-support"))]
const DAP_REQUEST_TIMEOUT: Duration = Duration::from_secs(2);
#[cfg(not(any(test, feature = "test-support")))]
const DAP_REQUEST_TIMEOUT: Duration = Duration::from_secs(12);
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(transparent)]
pub struct SessionId(pub u32);
@@ -41,7 +34,6 @@ pub struct DebugAdapterClient {
id: SessionId,
sequence_count: AtomicU64,
binary: DebugAdapterBinary,
executor: BackgroundExecutor,
transport_delegate: TransportDelegate,
}
@@ -61,7 +53,6 @@ impl DebugAdapterClient {
binary,
transport_delegate,
sequence_count: AtomicU64::new(1),
executor: cx.background_executor().clone(),
};
log::info!("Successfully connected to debug adapter");
@@ -173,40 +164,33 @@ impl DebugAdapterClient {
self.send_message(Message::Request(request)).await?;
let mut timeout = self.executor.timer(DAP_REQUEST_TIMEOUT).fuse();
let command = R::COMMAND.to_string();
select! {
response = callback_rx.fuse() => {
log::debug!(
"Client {} received response for: `{}` sequence_id: {}",
self.id.0,
command,
sequence_id
);
let response = response??;
match response.success {
true => {
if let Some(json) = response.body {
Ok(serde_json::from_value(json)?)
// Note: dap types configure themselves to return `None` when an empty object is received,
// which then fails here...
} else if let Ok(result) = serde_json::from_value(serde_json::Value::Object(Default::default())) {
Ok(result)
} else {
Ok(serde_json::from_value(Default::default())?)
}
}
false => Err(anyhow!("Request failed: {}", response.message.unwrap_or_default())),
let response = callback_rx.await??;
log::debug!(
"Client {} received response for: `{}` sequence_id: {}",
self.id.0,
command,
sequence_id
);
match response.success {
true => {
if let Some(json) = response.body {
Ok(serde_json::from_value(json)?)
// Note: dap types configure themselves to return `None` when an empty object is received,
// which then fails here...
} else if let Ok(result) =
serde_json::from_value(serde_json::Value::Object(Default::default()))
{
Ok(result)
} else {
Ok(serde_json::from_value(Default::default())?)
}
}
_ = timeout => {
self.transport_delegate.cancel_pending_request(&sequence_id).await;
log::error!("Cancelled DAP request for {command:?} id {sequence_id} which took over {DAP_REQUEST_TIMEOUT:?}");
anyhow::bail!("DAP request timeout");
}
false => Err(anyhow!(
"Request failed: {}",
response.message.unwrap_or_default()
)),
}
}

View File

@@ -29,7 +29,7 @@ pub struct InlineValueLocation {
/// during debugging sessions. Implementors must also handle variable scoping
/// themselves by traversing the syntax tree upwards to determine whether a
/// variable is local or global.
pub trait InlineValueProvider {
pub trait InlineValueProvider: 'static + Send + Sync {
/// Provides a list of inline value locations based on the given node and source code.
///
/// # Parameters

View File

@@ -2,6 +2,7 @@ use anyhow::Result;
use async_trait::async_trait;
use collections::FxHashMap;
use gpui::{App, Global, SharedString};
use language::LanguageName;
use parking_lot::RwLock;
use task::{DebugRequest, DebugScenario, SpawnInTerminal, TaskTemplate};
@@ -59,6 +60,11 @@ impl DapRegistry {
);
}
pub fn adapter_language(&self, adapter_name: &str) -> Option<LanguageName> {
self.adapter(adapter_name)
.and_then(|adapter| adapter.adapter_language_name())
}
pub fn add_locator(&self, locator: Arc<dyn DapLocator>) {
let _previous_value = self.0.write().locators.insert(locator.name(), locator);
debug_assert!(

View File

@@ -224,11 +224,6 @@ impl TransportDelegate {
pending_requests.insert(sequence_id, request);
}
pub(crate) async fn cancel_pending_request(&self, sequence_id: &u64) {
let mut pending_requests = self.pending_requests.lock().await;
pending_requests.remove(sequence_id);
}
pub(crate) async fn send_message(&self, message: Message) -> Result<()> {
if let Some(server_tx) = self.server_tx.lock().await.as_ref() {
server_tx

View File

@@ -42,7 +42,9 @@ impl CodeLldbDebugAdapter {
if !launch.args.is_empty() {
map.insert("args".into(), launch.args.clone().into());
}
if !launch.env.is_empty() {
map.insert("env".into(), launch.env_json());
}
if let Some(stop_on_entry) = config.stop_on_entry {
map.insert("stopOnEntry".into(), stop_on_entry.into());
}

View File

@@ -35,6 +35,10 @@ impl GdbDebugAdapter {
map.insert("args".into(), launch.args.clone().into());
}
if !launch.env.is_empty() {
map.insert("env".into(), launch.env_json());
}
if let Some(stop_on_entry) = config.stop_on_entry {
map.insert(
"stopAtBeginningOfMainSubprogram".into(),

View File

@@ -1,5 +1,6 @@
use dap::{StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
use gpui::AsyncApp;
use gpui::{AsyncApp, SharedString};
use language::LanguageName;
use std::{collections::HashMap, ffi::OsStr, path::PathBuf};
use crate::*;
@@ -19,7 +20,8 @@ impl GoDebugAdapter {
dap::DebugRequest::Launch(launch_config) => json!({
"program": launch_config.program,
"cwd": launch_config.cwd,
"args": launch_config.args
"args": launch_config.args,
"env": launch_config.env_json()
}),
};
@@ -42,6 +44,10 @@ impl DebugAdapter for GoDebugAdapter {
DebugAdapterName(Self::ADAPTER_NAME.into())
}
fn adapter_language_name(&self) -> Option<LanguageName> {
Some(SharedString::new_static("Go").into())
}
async fn get_binary(
&self,
delegate: &dyn DapDelegate,

View File

@@ -36,6 +36,9 @@ impl JsDebugAdapter {
if !launch.args.is_empty() {
map.insert("args".into(), launch.args.clone().into());
}
if !launch.env.is_empty() {
map.insert("env".into(), launch.env_json());
}
if let Some(stop_on_entry) = config.stop_on_entry {
map.insert("stopOnEntry".into(), stop_on_entry.into());

View File

@@ -1,6 +1,7 @@
use adapters::latest_github_release;
use dap::adapters::{DebugTaskDefinition, TcpArguments};
use gpui::AsyncApp;
use gpui::{AsyncApp, SharedString};
use language::LanguageName;
use std::{collections::HashMap, path::PathBuf, sync::OnceLock};
use util::ResultExt;
@@ -29,6 +30,7 @@ impl PhpDebugAdapter {
"program": launch_config.program,
"cwd": launch_config.cwd,
"args": launch_config.args,
"env": launch_config.env_json(),
"stopOnEntry": config.stop_on_entry.unwrap_or_default(),
}),
request: config.request.to_dap(),
@@ -118,6 +120,10 @@ impl DebugAdapter for PhpDebugAdapter {
DebugAdapterName(Self::ADAPTER_NAME.into())
}
fn adapter_language_name(&self) -> Option<LanguageName> {
Some(SharedString::new_static("PHP").into())
}
async fn get_binary(
&self,
delegate: &dyn DapDelegate,

View File

@@ -1,6 +1,7 @@
use crate::*;
use dap::{DebugRequest, StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
use gpui::AsyncApp;
use gpui::{AsyncApp, SharedString};
use language::LanguageName;
use std::{collections::HashMap, ffi::OsStr, path::PathBuf, sync::OnceLock};
use util::ResultExt;
@@ -32,6 +33,9 @@ impl PythonDebugAdapter {
DebugRequest::Launch(launch) => {
map.insert("program".into(), launch.program.clone().into());
map.insert("args".into(), launch.args.clone().into());
if !launch.env.is_empty() {
map.insert("env".into(), launch.env_json());
}
if let Some(stop_on_entry) = config.stop_on_entry {
map.insert("stopOnEntry".into(), stop_on_entry.into());
@@ -162,6 +166,10 @@ impl DebugAdapter for PythonDebugAdapter {
DebugAdapterName(Self::ADAPTER_NAME.into())
}
fn adapter_language_name(&self) -> Option<LanguageName> {
Some(SharedString::new_static("Python").into())
}
async fn get_binary(
&self,
delegate: &dyn DapDelegate,

View File

@@ -6,7 +6,8 @@ use dap::{
self, DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName, DebugTaskDefinition,
},
};
use gpui::AsyncApp;
use gpui::{AsyncApp, SharedString};
use language::LanguageName;
use std::path::PathBuf;
use util::command::new_smol_command;
@@ -25,6 +26,10 @@ impl DebugAdapter for RubyDebugAdapter {
DebugAdapterName(Self::ADAPTER_NAME.into())
}
fn adapter_language_name(&self) -> Option<LanguageName> {
Some(SharedString::new_static("Ruby").into())
}
async fn get_binary(
&self,
delegate: &dyn DapDelegate,
@@ -62,7 +67,7 @@ impl DebugAdapter for RubyDebugAdapter {
let tcp_connection = definition.tcp_connection.clone().unwrap_or_default();
let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
let DebugRequest::Launch(mut launch) = definition.request.clone() else {
let DebugRequest::Launch(launch) = definition.request.clone() else {
anyhow::bail!("rdbg does not yet support attaching");
};
@@ -71,12 +76,6 @@ impl DebugAdapter for RubyDebugAdapter {
format!("--port={}", port),
format!("--host={}", host),
];
if launch.args.is_empty() {
let program = launch.program.clone();
let mut split = program.split(" ");
launch.program = split.next().unwrap().to_string();
launch.args = split.map(|s| s.to_string()).collect();
}
if delegate.which(launch.program.as_ref()).is_some() {
arguments.push("--command".to_string())
}

View File

@@ -0,0 +1,20 @@
[package]
name = "debug_adapter_extension"
version = "0.1.0"
license = "GPL-3.0-or-later"
publish.workspace = true
edition.workspace = true
[dependencies]
anyhow.workspace = true
async-trait.workspace = true
dap.workspace = true
extension.workspace = true
gpui.workspace = true
workspace-hack = { version = "0.1", path = "../../tooling/workspace-hack" }
[lints]
workspace = true
[lib]
path = "src/debug_adapter_extension.rs"

View File

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

View File

@@ -0,0 +1,40 @@
mod extension_dap_adapter;
use std::sync::Arc;
use dap::DapRegistry;
use extension::{ExtensionDebugAdapterProviderProxy, ExtensionHostProxy};
use extension_dap_adapter::ExtensionDapAdapter;
use gpui::App;
pub fn init(extension_host_proxy: Arc<ExtensionHostProxy>, cx: &mut App) {
let language_server_registry_proxy = DebugAdapterRegistryProxy::new(cx);
extension_host_proxy.register_debug_adapter_proxy(language_server_registry_proxy);
}
#[derive(Clone)]
struct DebugAdapterRegistryProxy {
debug_adapter_registry: DapRegistry,
}
impl DebugAdapterRegistryProxy {
fn new(cx: &mut App) -> Self {
Self {
debug_adapter_registry: DapRegistry::global(cx).clone(),
}
}
}
impl ExtensionDebugAdapterProviderProxy for DebugAdapterRegistryProxy {
fn register_debug_adapter(
&self,
extension: Arc<dyn extension::Extension>,
debug_adapter_name: Arc<str>,
) {
self.debug_adapter_registry
.add_adapter(Arc::new(ExtensionDapAdapter::new(
extension,
debug_adapter_name,
)));
}
}

View File

@@ -0,0 +1,49 @@
use std::{path::PathBuf, sync::Arc};
use anyhow::Result;
use async_trait::async_trait;
use dap::adapters::{
DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName, DebugTaskDefinition,
};
use extension::Extension;
use gpui::AsyncApp;
pub(crate) struct ExtensionDapAdapter {
extension: Arc<dyn Extension>,
debug_adapter_name: Arc<str>,
}
impl ExtensionDapAdapter {
pub(crate) fn new(
extension: Arc<dyn extension::Extension>,
debug_adapter_name: Arc<str>,
) -> Self {
Self {
extension,
debug_adapter_name,
}
}
}
#[async_trait(?Send)]
impl DebugAdapter for ExtensionDapAdapter {
fn name(&self) -> DebugAdapterName {
self.debug_adapter_name.as_ref().into()
}
async fn get_binary(
&self,
_: &dyn DapDelegate,
config: &DebugTaskDefinition,
user_installed_path: Option<PathBuf>,
_cx: &mut AsyncApp,
) -> Result<DebugAdapterBinary> {
self.extension
.get_dap_binary(
self.debug_adapter_name.clone(),
config.clone(),
user_installed_path,
)
.await
}
}

View File

@@ -36,6 +36,7 @@ dap_adapters = { workspace = true, optional = true }
db.workspace = true
editor.workspace = true
feature_flags.workspace = true
file_icons.workspace = true
futures.workspace = true
fuzzy.workspace = true
gpui.workspace = true
@@ -51,6 +52,7 @@ rpc.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
shlex.workspace = true
sysinfo.workspace = true
task.workspace = true
tasks_ui.workspace = true

View File

@@ -2,8 +2,9 @@ use crate::persistence::DebuggerPaneItem;
use crate::session::DebugSession;
use crate::{
ClearAllBreakpoints, Continue, Detach, FocusBreakpointList, FocusConsole, FocusFrames,
FocusLoadedSources, FocusModules, FocusTerminal, FocusVariables, Pause, Restart, StepBack,
StepInto, StepOut, StepOver, Stop, ToggleIgnoreBreakpoints, persistence,
FocusLoadedSources, FocusModules, FocusTerminal, FocusVariables, Pause, Restart,
ShowStackTrace, StepBack, StepInto, StepOut, StepOver, Stop, ToggleIgnoreBreakpoints,
persistence,
};
use anyhow::{Result, anyhow};
use command_palette_hooks::CommandPaletteFilter;
@@ -22,7 +23,7 @@ use gpui::{
use language::Buffer;
use project::debugger::session::{Session, SessionStateEvent};
use project::{Fs, WorktreeId};
use project::{Fs, ProjectPath, WorktreeId};
use project::{Project, debugger::session::ThreadStatus};
use rpc::proto::{self};
use settings::Settings;
@@ -67,11 +68,7 @@ pub struct DebugPanel {
}
impl DebugPanel {
pub fn new(
workspace: &Workspace,
_window: &mut Window,
cx: &mut Context<Workspace>,
) -> Entity<Self> {
pub fn new(workspace: &Workspace, cx: &mut Context<Workspace>) -> Entity<Self> {
cx.new(|cx| {
let project = workspace.project().clone();
@@ -119,6 +116,7 @@ impl DebugPanel {
TypeId::of::<StepOver>(),
TypeId::of::<StepInto>(),
TypeId::of::<StepOut>(),
TypeId::of::<ShowStackTrace>(),
TypeId::of::<editor::actions::DebuggerRunToCursor>(),
TypeId::of::<editor::actions::DebuggerEvaluateSelectedText>(),
];
@@ -170,8 +168,8 @@ impl DebugPanel {
cx: &mut AsyncWindowContext,
) -> Task<Result<Entity<Self>>> {
cx.spawn(async move |cx| {
workspace.update_in(cx, |workspace, window, cx| {
let debug_panel = DebugPanel::new(workspace, window, cx);
workspace.update(cx, |workspace, cx| {
let debug_panel = DebugPanel::new(workspace, cx);
workspace.register_action(|workspace, _: &ClearAllBreakpoints, _, cx| {
workspace.project().read(cx).breakpoint_store().update(
@@ -218,6 +216,18 @@ impl DebugPanel {
cx,
)
});
if let Some(inventory) = self
.project
.read(cx)
.task_store()
.read(cx)
.task_inventory()
.cloned()
{
inventory.update(cx, |inventory, _| {
inventory.scenario_scheduled(scenario.clone());
})
}
let task = cx.spawn_in(window, {
let session = session.clone();
async move |this, cx| {
@@ -409,6 +419,7 @@ impl DebugPanel {
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>) {
let Some(session) = self
.sessions
@@ -987,7 +998,7 @@ impl DebugPanel {
this.go_to_selected_stack_frame(window, cx);
});
});
self.active_session = Some(session_item);
self.active_session = Some(session_item.clone());
cx.notify();
}
@@ -997,7 +1008,7 @@ impl DebugPanel {
worktree_id: WorktreeId,
window: &mut Window,
cx: &mut App,
) -> Task<Result<()>> {
) -> Task<Result<ProjectPath>> {
self.workspace
.update(cx, |workspace, cx| {
let Some(mut path) = workspace.absolute_path_of_worktree(worktree_id, cx) else {
@@ -1006,14 +1017,20 @@ impl DebugPanel {
let serialized_scenario = serde_json::to_value(scenario);
path.push(paths::local_debug_file_relative_path());
cx.spawn_in(window, async move |workspace, cx| {
let serialized_scenario = serialized_scenario?;
let path = path.as_path();
let fs =
workspace.update(cx, |workspace, _| workspace.app_state().fs.clone())?;
path.push(paths::local_settings_folder_relative_path());
if !fs.is_dir(path.as_path()).await {
fs.create_dir(path.as_path()).await?;
}
path.pop();
path.push(paths::local_debug_file_relative_path());
let path = path.as_path();
if !fs.is_file(path).await {
let content =
serde_json::to_string_pretty(&serde_json::Value::Array(vec![
@@ -1034,21 +1051,19 @@ impl DebugPanel {
.await?;
}
workspace.update_in(cx, |workspace, window, cx| {
workspace.update(cx, |workspace, cx| {
if let Some(project_path) = workspace
.project()
.read(cx)
.project_path_for_absolute_path(&path, cx)
{
workspace.open_path(project_path, None, true, window, cx)
Ok(project_path)
} else {
Task::ready(Err(anyhow!(
Err(anyhow!(
"Couldn't get project path for .zed/debug.json in active worktree"
)))
))
}
})?.await?;
anyhow::Ok(())
})?
})
})
.unwrap_or_else(|err| Task::ready(Err(err)))
@@ -1115,7 +1130,7 @@ impl Panel for DebugPanel {
}
fn set_size(&mut self, size: Option<Pixels>, _window: &mut Window, _cx: &mut Context<Self>) {
self.size = size.unwrap();
self.size = size.unwrap_or(px(300.));
}
fn remote_id() -> Option<proto::PanelId> {

View File

@@ -7,14 +7,16 @@ use new_session_modal::NewSessionModal;
use project::debugger::{self, breakpoint_store::SourceBreakpoint};
use session::DebugSession;
use settings::Settings;
use stack_trace_view::StackTraceView;
use util::maybe;
use workspace::{ShutdownDebugAdapters, Workspace};
use workspace::{ItemHandle, ShutdownDebugAdapters, Workspace};
pub mod attach_modal;
pub mod debugger_panel;
mod new_session_modal;
mod persistence;
pub(crate) mod session;
mod stack_trace_view;
#[cfg(any(test, feature = "test-support"))]
pub mod tests;
@@ -41,6 +43,7 @@ actions!(
FocusModules,
FocusLoadedSources,
FocusTerminal,
ShowStackTrace,
]
);
@@ -146,6 +149,38 @@ pub fn init(cx: &mut App) {
})
},
)
.register_action(
|workspace: &mut Workspace, _: &ShowStackTrace, window, cx| {
let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) else {
return;
};
if let Some(existing) = workspace.item_of_type::<StackTraceView>(cx) {
let is_active = workspace
.active_item(cx)
.is_some_and(|item| item.item_id() == existing.item_id());
workspace.activate_item(&existing, true, !is_active, window, cx);
} else {
let Some(active_session) = debug_panel.read(cx).active_session() else {
return;
};
let project = workspace.project();
let stack_trace_view = active_session.update(cx, |session, cx| {
session.stack_trace_view(project, window, cx).clone()
});
workspace.add_item_to_active_pane(
Box::new(stack_trace_view),
None,
true,
window,
cx,
);
}
},
)
.register_action(|workspace: &mut Workspace, _: &Start, window, cx| {
NewSessionModal::show(workspace, window, cx);
});

View File

@@ -1,11 +1,15 @@
use collections::FxHashMap;
use language::LanguageRegistry;
use std::{
borrow::Cow,
ops::Not,
path::{Path, PathBuf},
sync::Arc,
time::Duration,
usize,
};
use anyhow::Result;
use dap::{
DapRegistry, DebugRequest,
adapters::{DebugAdapterName, DebugTaskDefinition},
@@ -13,26 +17,32 @@ use dap::{
use editor::{Editor, EditorElement, EditorStyle};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Render,
Subscription, TextStyle, WeakEntity,
Animation, AnimationExt as _, App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle,
Focusable, Render, Subscription, TextStyle, Transformation, WeakEntity, percentage,
};
use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch};
use project::{TaskContexts, TaskSourceKind, task_store::TaskStore};
use project::{ProjectPath, TaskContexts, TaskSourceKind, task_store::TaskStore};
use settings::Settings;
use task::{DebugScenario, LaunchRequest};
use theme::ThemeSettings;
use ui::{
ActiveTheme, Button, ButtonCommon, ButtonSize, CheckboxWithLabel, Clickable, Color, Context,
ContextMenu, Disableable, DropdownMenu, FluentBuilder, Icon, IconName, InteractiveElement,
IntoElement, Label, LabelCommon as _, ListItem, ListItemSpacing, ParentElement, RenderOnce,
SharedString, Styled, StyledExt, ToggleButton, ToggleState, Toggleable, Window, div, h_flex,
relative, rems, v_flex,
ContextMenu, Disableable, DropdownMenu, FluentBuilder, Icon, IconButton, IconName, IconSize,
InteractiveElement, IntoElement, Label, LabelCommon as _, ListItem, ListItemSpacing,
ParentElement, RenderOnce, SharedString, Styled, StyledExt, ToggleButton, ToggleState,
Toggleable, Window, div, h_flex, relative, rems, v_flex,
};
use util::ResultExt;
use workspace::{ModalView, Workspace, pane};
use crate::{attach_modal::AttachModal, debugger_panel::DebugPanel};
enum SaveScenarioState {
Saving,
Saved(ProjectPath),
Failed(SharedString),
}
pub(super) struct NewSessionModal {
workspace: WeakEntity<Workspace>,
debug_panel: WeakEntity<DebugPanel>,
@@ -41,7 +51,7 @@ pub(super) struct NewSessionModal {
attach_mode: Entity<AttachMode>,
custom_mode: Entity<CustomMode>,
debugger: Option<DebugAdapterName>,
task_contexts: Arc<TaskContexts>,
save_scenario_state: Option<SaveScenarioState>,
_subscriptions: [Subscription; 2],
}
@@ -73,16 +83,9 @@ impl NewSessionModal {
return;
};
let task_store = workspace.project().read(cx).task_store().clone();
let languages = workspace.app_state().languages.clone();
cx.spawn_in(window, async move |workspace, cx| {
let task_contexts = Arc::from(
workspace
.update_in(cx, |workspace, window, cx| {
tasks_ui::task_contexts(workspace, window, cx)
})?
.await,
);
workspace.update_in(cx, |workspace, window, cx| {
let workspace_handle = workspace.weak_handle();
workspace.toggle_modal(window, cx, |window, cx| {
@@ -90,12 +93,7 @@ impl NewSessionModal {
let launch_picker = cx.new(|cx| {
Picker::uniform_list(
DebugScenarioDelegate::new(
debug_panel.downgrade(),
workspace_handle.clone(),
task_store,
task_contexts.clone(),
),
DebugScenarioDelegate::new(debug_panel.downgrade(), task_store),
window,
cx,
)
@@ -116,6 +114,40 @@ impl NewSessionModal {
let custom_mode = CustomMode::new(None, window, cx);
cx.spawn_in(window, {
let workspace_handle = workspace_handle.clone();
async move |this, cx| {
let task_contexts = workspace_handle
.update_in(cx, |workspace, window, cx| {
tasks_ui::task_contexts(workspace, window, cx)
})?
.await;
this.update_in(cx, |this, window, cx| {
if let Some(active_cwd) = task_contexts
.active_context()
.and_then(|context| context.cwd.clone())
{
this.custom_mode.update(cx, |custom, cx| {
custom.load(active_cwd, window, cx);
});
}
this.launch_picker.update(cx, |picker, cx| {
picker.delegate.task_contexts_loaded(
task_contexts,
languages,
window,
cx,
);
picker.refresh(window, cx);
cx.notify();
});
})
}
})
.detach();
Self {
launch_picker,
attach_mode,
@@ -124,7 +156,7 @@ impl NewSessionModal {
mode: NewSessionMode::Launch,
debug_panel: debug_panel.downgrade(),
workspace: workspace_handle,
task_contexts,
save_scenario_state: None,
_subscriptions,
}
});
@@ -135,7 +167,7 @@ impl NewSessionModal {
.detach();
}
fn render_mode(&self, window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
fn render_mode(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
let dap_menu = self.adapter_drop_down_menu(window, cx);
match self.mode {
NewSessionMode::Attach => self.attach_mode.update(cx, |this, cx| {
@@ -190,8 +222,6 @@ impl NewSessionModal {
fn start_new_session(&self, window: &mut Window, cx: &mut Context<Self>) {
let Some(debugger) = self.debugger.as_ref() else {
// todo(debugger): show in UI.
log::error!("No debugger selected");
return;
};
@@ -208,10 +238,12 @@ impl NewSessionModal {
};
let debug_panel = self.debug_panel.clone();
let task_contexts = self.task_contexts.clone();
let Some(task_contexts) = self.task_contexts(cx) else {
return;
};
let task_context = task_contexts.active_context().cloned().unwrap_or_default();
let worktree_id = task_contexts.worktree();
cx.spawn_in(window, async move |this, cx| {
let task_context = task_contexts.active_context().cloned().unwrap_or_default();
let worktree_id = task_contexts.worktree();
debug_panel.update_in(cx, |debug_panel, window, cx| {
debug_panel.start_session(config, task_context, None, worktree_id, window, cx)
})?;
@@ -219,7 +251,7 @@ impl NewSessionModal {
cx.emit(DismissEvent);
})
.ok();
anyhow::Result::<_, anyhow::Error>::Ok(())
Result::<_, anyhow::Error>::Ok(())
})
.detach_and_log_err(cx);
}
@@ -245,33 +277,55 @@ impl NewSessionModal {
cx.notify();
})
}
fn task_contexts<'a>(&self, cx: &'a mut Context<Self>) -> Option<&'a TaskContexts> {
self.launch_picker.read(cx).delegate.task_contexts.as_ref()
}
fn adapter_drop_down_menu(
&self,
&mut self,
window: &mut Window,
cx: &mut Context<Self>,
) -> ui::DropdownMenu {
let workspace = self.workspace.clone();
let weak = cx.weak_entity();
let active_buffer = self.task_contexts(cx).and_then(|tc| {
tc.active_item_context
.as_ref()
.and_then(|aic| aic.1.as_ref().map(|l| l.buffer.clone()))
});
let active_buffer_language = active_buffer
.and_then(|buffer| buffer.read(cx).language())
.cloned();
let mut available_adapters = workspace
.update(cx, |_, cx| DapRegistry::global(cx).enumerate_adapters())
.unwrap_or_default();
if let Some(language) = active_buffer_language {
available_adapters.sort_by_key(|adapter| {
language
.config()
.debuggers
.get_index_of(adapter.0.as_ref())
.unwrap_or(usize::MAX)
});
}
if self.debugger.is_none() {
self.debugger = available_adapters.first().cloned();
}
let label = self
.debugger
.as_ref()
.map(|d| d.0.clone())
.unwrap_or_else(|| SELECT_DEBUGGER_LABEL.clone());
let active_buffer_language = self
.task_contexts
.active_item_context
.as_ref()
.and_then(|item| {
item.1
.as_ref()
.and_then(|location| location.buffer.read(cx).language())
})
.cloned();
DropdownMenu::new(
"dap-adapter-picker",
label,
ContextMenu::build(window, cx, move |mut menu, _, cx| {
ContextMenu::build(window, cx, move |mut menu, _, _| {
let setter_for_name = |name: DebugAdapterName| {
let weak = weak.clone();
move |window: &mut Window, cx: &mut App| {
@@ -286,22 +340,10 @@ impl NewSessionModal {
}
};
let mut available_adapters = workspace
.update(cx, |_, cx| DapRegistry::global(cx).enumerate_adapters())
.unwrap_or_default();
if let Some(language) = active_buffer_language {
available_adapters.sort_by_key(|adapter| {
language
.config()
.debuggers
.get_index_of(adapter.0.as_ref())
.unwrap_or(usize::MAX)
});
}
for adapter in available_adapters.into_iter() {
menu = menu.entry(adapter.0.clone(), None, setter_for_name(adapter.clone()));
}
menu
}),
)
@@ -379,6 +421,8 @@ impl Render for NewSessionModal {
window: &mut ui::Window,
cx: &mut ui::Context<Self>,
) -> impl ui::IntoElement {
let this = cx.weak_entity().clone();
v_flex()
.size_full()
.w(rems(34.))
@@ -484,42 +528,151 @@ impl Render for NewSessionModal {
}
}),
),
NewSessionMode::Custom => div().child(
Button::new("new-session-modal-back", "Save to .zed/debug.json...")
.on_click(cx.listener(|this, _, window, cx| {
let Some(save_scenario_task) = this
.debugger
.as_ref()
.and_then(|debugger| this.debug_scenario(&debugger, cx))
.zip(this.task_contexts.worktree())
.and_then(|(scenario, worktree_id)| {
this.debug_panel
.update(cx, |panel, cx| {
panel.save_scenario(
&scenario,
worktree_id,
window,
cx,
)
})
.ok()
})
else {
return;
};
NewSessionMode::Custom => h_flex()
.child(
Button::new("new-session-modal-back", "Save to .zed/debug.json...")
.on_click(cx.listener(|this, _, window, cx| {
let Some(save_scenario) = this
.debugger
.as_ref()
.and_then(|debugger| this.debug_scenario(&debugger, cx))
.zip(
this.task_contexts(cx)
.and_then(|tcx| tcx.worktree()),
)
.and_then(|(scenario, worktree_id)| {
this.debug_panel
.update(cx, |panel, cx| {
panel.save_scenario(
&scenario,
worktree_id,
window,
cx,
)
})
.ok()
})
else {
return;
};
cx.spawn(async move |this, cx| {
if save_scenario_task.await.is_ok() {
this.update(cx, |_, cx| cx.emit(DismissEvent)).ok();
}
})
.detach();
}))
.disabled(
self.debugger.is_none()
|| self.custom_mode.read(cx).program.read(cx).is_empty(cx),
),
),
this.save_scenario_state = Some(SaveScenarioState::Saving);
cx.spawn(async move |this, cx| {
let res = save_scenario.await;
this.update(cx, |this, _| match res {
Ok(saved_file) => {
this.save_scenario_state =
Some(SaveScenarioState::Saved(saved_file))
}
Err(error) => {
this.save_scenario_state =
Some(SaveScenarioState::Failed(
error.to_string().into(),
))
}
})
.ok();
cx.background_executor()
.timer(Duration::from_secs(2))
.await;
this.update(cx, |this, _| {
this.save_scenario_state.take()
})
.ok();
})
.detach();
}))
.disabled(
self.debugger.is_none()
|| self
.custom_mode
.read(cx)
.program
.read(cx)
.is_empty(cx)
|| self.save_scenario_state.is_some(),
),
)
.when_some(self.save_scenario_state.as_ref(), {
let this_entity = this.clone();
move |this, save_state| match save_state {
SaveScenarioState::Saved(saved_path) => this.child(
IconButton::new(
"new-session-modal-go-to-file",
IconName::ArrowUpRight,
)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.on_click({
let this_entity = this_entity.clone();
let saved_path = saved_path.clone();
move |_, window, cx| {
window
.spawn(cx, {
let this_entity = this_entity.clone();
let saved_path = saved_path.clone();
async move |cx| {
this_entity
.update_in(
cx,
|this, window, cx| {
this.workspace.update(
cx,
|workspace, cx| {
workspace.open_path(
saved_path
.clone(),
None,
true,
window,
cx,
)
},
)
},
)??
.await?;
this_entity
.update(cx, |_, cx| {
cx.emit(DismissEvent)
})
.ok();
anyhow::Ok(())
}
})
.detach();
}
}),
),
SaveScenarioState::Saving => this.child(
Icon::new(IconName::Spinner)
.size(IconSize::Small)
.color(Color::Muted)
.with_animation(
"Spinner",
Animation::new(Duration::from_secs(3)).repeat(),
|icon, delta| {
icon.transform(Transformation::rotate(
percentage(delta),
))
},
),
),
SaveScenarioState::Failed(error_msg) => this.child(
IconButton::new("Failed Scenario Saved", IconName::X)
.icon_size(IconSize::Small)
.icon_color(Color::Error)
.tooltip(ui::Tooltip::text(error_msg.clone())),
),
}
}),
})
.child(
Button::new("debugger-spawn", "Start")
@@ -595,7 +748,7 @@ impl CustomMode {
let program = cx.new(|cx| Editor::single_line(window, cx));
program.update(cx, |this, cx| {
this.set_placeholder_text("Program path", cx);
this.set_placeholder_text("Run", cx);
if let Some(past_program) = past_program {
this.set_text(past_program, window, cx);
@@ -615,13 +768,75 @@ impl CustomMode {
})
}
fn load(&mut self, cwd: PathBuf, window: &mut Window, cx: &mut App) {
self.cwd.update(cx, |editor, cx| {
if editor.is_empty(cx) {
editor.set_text(cwd.to_string_lossy(), window, cx);
}
});
}
pub(super) fn debug_request(&self, cx: &App) -> task::LaunchRequest {
let path = self.cwd.read(cx).text(cx);
if cfg!(windows) {
return task::LaunchRequest {
program: self.program.read(cx).text(cx),
cwd: path.is_empty().not().then(|| PathBuf::from(path)),
args: Default::default(),
env: Default::default(),
};
}
let command = self.program.read(cx).text(cx);
let mut args = shlex::split(&command).into_iter().flatten().peekable();
let mut env = FxHashMap::default();
while args.peek().is_some_and(|arg| arg.contains('=')) {
let arg = args.next().unwrap();
let (lhs, rhs) = arg.split_once('=').unwrap();
env.insert(lhs.to_string(), rhs.to_string());
}
let program = if let Some(program) = args.next() {
program
} else {
env = FxHashMap::default();
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<_>>();
task::LaunchRequest {
program: self.program.read(cx).text(cx),
program,
cwd: path.is_empty().not().then(|| PathBuf::from(path)),
args: Default::default(),
env: Default::default(),
args,
env,
}
}
@@ -636,14 +851,6 @@ impl CustomMode {
.w_full()
.gap_3()
.track_focus(&self.program.focus_handle(cx))
.child(
div().child(
Label::new("Program")
.size(ui::LabelSize::Small)
.color(Color::Muted),
),
)
.child(render_editor(&self.program, window, cx))
.child(
h_flex()
.child(
@@ -654,10 +861,14 @@ impl CustomMode {
.gap(ui::DynamicSpacing::Base08.rems(cx))
.child(adapter_menu),
)
.child(render_editor(&self.program, window, cx))
.child(render_editor(&self.cwd, window, cx))
.child(
CheckboxWithLabel::new(
"debugger-stop-on-entry",
Label::new("Stop on Entry").size(ui::LabelSize::Small),
Label::new("Stop on Entry")
.size(ui::LabelSize::Small)
.color(Color::Muted),
self.stop_on_entry,
{
let this = cx.weak_entity();
@@ -714,33 +925,106 @@ impl AttachMode {
pub(super) struct DebugScenarioDelegate {
task_store: Entity<TaskStore>,
candidates: Option<Vec<(TaskSourceKind, DebugScenario)>>,
candidates: Vec<(Option<TaskSourceKind>, DebugScenario)>,
selected_index: usize,
matches: Vec<StringMatch>,
prompt: String,
debug_panel: WeakEntity<DebugPanel>,
workspace: WeakEntity<Workspace>,
task_contexts: Arc<TaskContexts>,
task_contexts: Option<TaskContexts>,
divider_index: Option<usize>,
last_used_candidate_index: Option<usize>,
}
impl DebugScenarioDelegate {
pub(super) fn new(
debug_panel: WeakEntity<DebugPanel>,
workspace: WeakEntity<Workspace>,
task_store: Entity<TaskStore>,
task_contexts: Arc<TaskContexts>,
) -> Self {
pub(super) fn new(debug_panel: WeakEntity<DebugPanel>, task_store: Entity<TaskStore>) -> Self {
Self {
task_store,
candidates: None,
candidates: Vec::default(),
selected_index: 0,
matches: Vec::new(),
prompt: String::new(),
debug_panel,
workspace,
task_contexts,
task_contexts: None,
divider_index: None,
last_used_candidate_index: None,
}
}
fn get_scenario_kind(
languages: &Arc<LanguageRegistry>,
dap_registry: &DapRegistry,
scenario: DebugScenario,
) -> (Option<TaskSourceKind>, DebugScenario) {
let language_names = languages.language_names();
let language = dap_registry
.adapter_language(&scenario.adapter)
.map(|language| TaskSourceKind::Language {
name: language.into(),
});
let language = language.or_else(|| {
scenario
.request
.as_ref()
.and_then(|request| match request {
DebugRequest::Launch(launch) => launch
.program
.rsplit_once(".")
.and_then(|split| languages.language_name_for_extension(split.1))
.map(|name| TaskSourceKind::Language { name: name.into() }),
_ => None,
})
.or_else(|| {
scenario.label.split_whitespace().find_map(|word| {
language_names
.iter()
.find(|name| name.eq_ignore_ascii_case(word))
.map(|name| TaskSourceKind::Language {
name: name.to_owned().into(),
})
})
})
});
(language, scenario)
}
pub fn task_contexts_loaded(
&mut self,
task_contexts: TaskContexts,
languages: Arc<LanguageRegistry>,
_window: &mut Window,
cx: &mut Context<Picker<Self>>,
) {
self.task_contexts = Some(task_contexts);
let (recent, scenarios) = self
.task_store
.update(cx, |task_store, cx| {
task_store.task_inventory().map(|inventory| {
inventory.update(cx, |inventory, cx| {
inventory.list_debug_scenarios(self.task_contexts.as_ref().unwrap(), cx)
})
})
})
.unwrap_or_default();
if !recent.is_empty() {
self.last_used_candidate_index = Some(recent.len() - 1);
}
let dap_registry = cx.global::<DapRegistry>();
self.candidates = recent
.into_iter()
.map(|scenario| Self::get_scenario_kind(&languages, &dap_registry, scenario))
.chain(scenarios.into_iter().map(|(kind, scenario)| {
let (language, scenario) =
Self::get_scenario_kind(&languages, &dap_registry, scenario);
(language.or(Some(kind)), scenario)
}))
.collect();
}
}
impl PickerDelegate for DebugScenarioDelegate {
@@ -774,53 +1058,15 @@ impl PickerDelegate for DebugScenarioDelegate {
cx: &mut Context<picker::Picker<Self>>,
) -> gpui::Task<()> {
let candidates = self.candidates.clone();
let workspace = self.workspace.clone();
let task_store = self.task_store.clone();
cx.spawn_in(window, async move |picker, cx| {
let candidates: Vec<_> = match &candidates {
Some(candidates) => candidates
.into_iter()
.enumerate()
.map(|(index, (_, candidate))| {
StringMatchCandidate::new(index, candidate.label.as_ref())
})
.collect(),
None => {
let worktree_ids: Vec<_> = workspace
.update(cx, |this, cx| {
this.visible_worktrees(cx)
.map(|tree| tree.read(cx).id())
.collect()
})
.ok()
.unwrap_or_default();
let scenarios: Vec<_> = task_store
.update(cx, |task_store, cx| {
task_store.task_inventory().map(|item| {
item.read(cx).list_debug_scenarios(worktree_ids.into_iter())
})
})
.ok()
.flatten()
.unwrap_or_default();
picker
.update(cx, |picker, _| {
picker.delegate.candidates = Some(scenarios.clone());
})
.ok();
scenarios
.into_iter()
.enumerate()
.map(|(index, (_, candidate))| {
StringMatchCandidate::new(index, candidate.label.as_ref())
})
.collect()
}
};
let candidates: Vec<_> = candidates
.into_iter()
.enumerate()
.map(|(index, (_, candidate))| {
StringMatchCandidate::new(index, candidate.label.as_ref())
})
.collect();
let matches = fuzzy::match_strings(
&candidates,
@@ -839,6 +1085,13 @@ impl PickerDelegate for DebugScenarioDelegate {
delegate.matches = matches;
delegate.prompt = query;
delegate.divider_index = delegate.last_used_candidate_index.and_then(|index| {
let index = delegate
.matches
.partition_point(|matching_task| matching_task.candidate_id <= index);
Some(index).and_then(|index| (index != 0).then(|| index - 1))
});
if delegate.matches.is_empty() {
delegate.selected_index = 0;
} else {
@@ -850,34 +1103,34 @@ impl PickerDelegate for DebugScenarioDelegate {
})
}
fn separators_after_indices(&self) -> Vec<usize> {
if let Some(i) = self.divider_index {
vec![i]
} else {
Vec::new()
}
}
fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<picker::Picker<Self>>) {
let debug_scenario = self
.matches
.get(self.selected_index())
.and_then(|match_candidate| {
self.candidates
.as_ref()
.map(|candidates| candidates[match_candidate.candidate_id].clone())
});
.and_then(|match_candidate| self.candidates.get(match_candidate.candidate_id).cloned());
let Some((task_source_kind, debug_scenario)) = debug_scenario else {
let Some((_, debug_scenario)) = debug_scenario else {
return;
};
let (task_context, worktree_id) = if let TaskSourceKind::Worktree {
id: worktree_id,
directory_in_worktree: _,
id_base: _,
} = task_source_kind
{
self.task_contexts
.task_context_for_worktree_id(worktree_id)
.cloned()
.map(|context| (context, Some(worktree_id)))
} else {
None
}
.unwrap_or_default();
let (task_context, worktree_id) = self
.task_contexts
.as_ref()
.and_then(|task_contexts| {
Some((
task_contexts.active_context().cloned()?,
task_contexts.worktree(),
))
})
.unwrap_or_default();
self.debug_panel
.update(cx, |panel, cx| {
@@ -907,10 +1160,19 @@ impl PickerDelegate for DebugScenarioDelegate {
char_count: hit.string.chars().count(),
color: Color::Default,
};
let task_kind = &self.candidates[hit.candidate_id].0;
let icon = Icon::new(IconName::FileTree)
.color(Color::Muted)
.size(ui::IconSize::Small);
let icon = match task_kind {
Some(TaskSourceKind::Lsp(..)) => Some(Icon::new(IconName::Bolt)),
Some(TaskSourceKind::UserInput) => Some(Icon::new(IconName::Terminal)),
Some(TaskSourceKind::AbsPath { .. }) => Some(Icon::new(IconName::Settings)),
Some(TaskSourceKind::Worktree { .. }) => Some(Icon::new(IconName::FileTree)),
Some(TaskSourceKind::Language { name }) => file_icons::FileIcons::get(cx)
.get_icon_for_type(&name.to_lowercase(), cx)
.map(Icon::from_path),
None => Some(Icon::new(IconName::HistoryRerun)),
}
.map(|icon| icon.color(Color::Muted).size(ui::IconSize::Small));
Some(
ListItem::new(SharedString::from(format!("debug-scenario-selection-{ix}")))

View File

@@ -278,7 +278,7 @@ pub(crate) fn deserialize_pane_layout(
cx,
)),
DebuggerPaneItem::Console => Box::new(SubView::new(
pane.focus_handle(cx),
console.focus_handle(cx),
console.clone().into(),
DebuggerPaneItem::Console,
Some(Box::new({
@@ -292,7 +292,7 @@ pub(crate) fn deserialize_pane_layout(
cx,
)),
DebuggerPaneItem::Terminal => Box::new(SubView::new(
pane.focus_handle(cx),
terminal.focus_handle(cx),
terminal.clone().into(),
DebuggerPaneItem::Terminal,
None,

View File

@@ -1,6 +1,6 @@
pub mod running;
use std::sync::OnceLock;
use std::{cell::OnceCell, sync::OnceLock};
use dap::client::SessionId;
use gpui::{
@@ -17,15 +17,16 @@ use workspace::{
item::{self, Item},
};
use crate::{debugger_panel::DebugPanel, persistence::SerializedLayout};
use crate::{StackTraceView, debugger_panel::DebugPanel, persistence::SerializedLayout};
pub struct DebugSession {
remote_id: Option<workspace::ViewId>,
running_state: Entity<RunningState>,
label: OnceLock<SharedString>,
stack_trace_view: OnceCell<Entity<StackTraceView>>,
_debug_panel: WeakEntity<DebugPanel>,
_worktree_store: WeakEntity<WorktreeStore>,
_workspace: WeakEntity<Workspace>,
workspace: WeakEntity<Workspace>,
_subscriptions: [Subscription; 1],
}
@@ -66,8 +67,9 @@ impl DebugSession {
running_state,
label: OnceLock::new(),
_debug_panel,
stack_trace_view: OnceCell::new(),
_worktree_store: project.read(cx).worktree_store().downgrade(),
_workspace: workspace,
workspace,
})
}
@@ -75,6 +77,32 @@ impl DebugSession {
self.running_state.read(cx).session_id()
}
pub(crate) fn stack_trace_view(
&mut self,
project: &Entity<Project>,
window: &mut Window,
cx: &mut Context<Self>,
) -> &Entity<StackTraceView> {
let workspace = self.workspace.clone();
let running_state = self.running_state.clone();
self.stack_trace_view.get_or_init(|| {
let stackframe_list = running_state.read(cx).stack_frame_list().clone();
let stack_frame_view = cx.new(|cx| {
StackTraceView::new(
workspace.clone(),
project.clone(),
stackframe_list,
window,
cx,
)
});
stack_frame_view
})
}
pub fn session(&self, cx: &App) -> Entity<Session> {
self.running_state.read(cx).session().clone()
}

View File

@@ -119,7 +119,7 @@ impl Render for RunningState {
pub(crate) struct SubView {
inner: AnyView,
pane_focus_handle: FocusHandle,
item_focus_handle: FocusHandle,
kind: DebuggerPaneItem,
show_indicator: Box<dyn Fn(&App) -> bool>,
hovered: bool,
@@ -127,7 +127,7 @@ pub(crate) struct SubView {
impl SubView {
pub(crate) fn new(
pane_focus_handle: FocusHandle,
item_focus_handle: FocusHandle,
view: AnyView,
kind: DebuggerPaneItem,
show_indicator: Option<Box<dyn Fn(&App) -> bool>>,
@@ -136,7 +136,7 @@ impl SubView {
cx.new(|_| Self {
kind,
inner: view,
pane_focus_handle,
item_focus_handle,
show_indicator: show_indicator.unwrap_or(Box::new(|_| false)),
hovered: false,
})
@@ -148,7 +148,7 @@ impl SubView {
}
impl Focusable for SubView {
fn focus_handle(&self, _: &App) -> FocusHandle {
self.pane_focus_handle.clone()
self.item_focus_handle.clone()
}
}
impl EventEmitter<()> for SubView {}
@@ -199,7 +199,7 @@ impl Render for SubView {
.size_full()
// Add border unconditionally to prevent layout shifts on focus changes.
.border_1()
.when(self.pane_focus_handle.contains_focused(window, cx), |el| {
.when(self.item_focus_handle.contains_focused(window, cx), |el| {
el.border_color(cx.theme().colors().pane_focused_border)
})
.child(self.inner.clone())
@@ -1202,7 +1202,9 @@ impl RunningState {
.as_ref()
.and_then(|pane| self.panes.find_pane_in_direction(pane, direction, cx))
{
window.focus(&pane.focus_handle(cx));
pane.update(cx, |pane, cx| {
pane.focus_active_item(window, cx);
})
} else {
self.workspace
.update(cx, |workspace, cx| {
@@ -1235,8 +1237,7 @@ impl RunningState {
self.stack_frame_list.read(cx).selected_stack_frame_id()
}
#[cfg(test)]
pub fn stack_frame_list(&self) -> &Entity<StackFrameList> {
pub(crate) fn stack_frame_list(&self) -> &Entity<StackFrameList> {
&self.stack_frame_list
}

View File

@@ -5,7 +5,7 @@ use super::{
use anyhow::Result;
use collections::HashMap;
use dap::OutputEvent;
use editor::{CompletionProvider, Editor, EditorElement, EditorStyle, ExcerptId};
use editor::{Bias, CompletionProvider, Editor, EditorElement, EditorStyle, ExcerptId};
use fuzzy::StringMatchCandidate;
use gpui::{
Context, Entity, FocusHandle, Focusable, Render, Subscription, Task, TextStyle, WeakEntity,
@@ -45,6 +45,7 @@ impl Console {
let mut editor = Editor::multi_line(window, cx);
editor.move_to_end(&editor::actions::MoveToEnd, window, cx);
editor.set_read_only(true);
editor.disable_scrollbars_and_minimap(window, cx);
editor.set_show_gutter(false, cx);
editor.set_show_runnables(false, cx);
editor.set_show_breakpoints(false, cx);
@@ -76,8 +77,14 @@ impl Console {
editor
});
let _subscriptions =
vec![cx.subscribe(&stack_frame_list, Self::handle_stack_frame_list_events)];
let _subscriptions = vec![
cx.subscribe(&stack_frame_list, Self::handle_stack_frame_list_events),
cx.on_focus_in(&focus_handle, window, |console, window, cx| {
if console.is_running(cx) {
console.query_bar.focus_handle(cx).focus(window);
}
}),
];
Self {
session,
@@ -97,7 +104,7 @@ impl Console {
&self.console
}
fn is_local(&self, cx: &Context<Self>) -> bool {
fn is_running(&self, cx: &Context<Self>) -> bool {
self.session.read(cx).is_local()
}
@@ -109,6 +116,7 @@ impl Console {
) {
match event {
StackFrameListEvent::SelectedStackFrameChanged(_) => cx.notify(),
StackFrameListEvent::BuiltEntries => {}
}
}
@@ -218,7 +226,7 @@ impl Render for Console {
.on_action(cx.listener(Self::evaluate))
.size_full()
.child(self.render_console(cx))
.when(self.is_local(cx), |this| {
.when(self.is_running(cx), |this| {
this.child(Divider::horizontal())
.child(self.render_query_bar(cx))
})
@@ -401,28 +409,21 @@ impl ConsoleQueryBarCompletionProvider {
.as_ref()
.unwrap_or(&completion.label)
.to_owned();
let mut word_bytes_length = 0;
for chunk in snapshot
.reversed_chunks_in_range(language::Anchor::MIN..buffer_position)
{
let mut processed_bytes = 0;
if let Some(_) = chunk.chars().rfind(|c| {
let is_whitespace = c.is_whitespace();
if !is_whitespace {
processed_bytes += c.len_utf8();
}
let buffer_text = snapshot.text();
let buffer_bytes = buffer_text.as_bytes();
let new_bytes = new_text.as_bytes();
is_whitespace
}) {
word_bytes_length += processed_bytes;
let mut prefix_len = 0;
for i in (0..new_bytes.len()).rev() {
if buffer_bytes.ends_with(&new_bytes[0..i]) {
prefix_len = i;
break;
} else {
word_bytes_length += chunk.len();
}
}
let buffer_offset = buffer_position.to_offset(&snapshot);
let start = buffer_offset - word_bytes_length;
let start = buffer_offset - prefix_len;
let start = snapshot.clip_offset(start, Bias::Left);
let start = snapshot.anchor_before(start);
let replace_range = start..buffer_position;

View File

@@ -15,13 +15,16 @@ use project::debugger::session::{Session, SessionEvent, StackFrame};
use project::{ProjectItem, ProjectPath};
use ui::{Scrollbar, ScrollbarState, Tooltip, prelude::*};
use util::ResultExt;
use workspace::Workspace;
use workspace::{ItemHandle, Workspace};
use crate::StackTraceView;
use super::RunningState;
#[derive(Debug)]
pub enum StackFrameListEvent {
SelectedStackFrameChanged(StackFrameId),
BuiltEntries,
}
pub struct StackFrameList {
@@ -101,13 +104,18 @@ impl StackFrameList {
&self.entries
}
#[cfg(test)]
pub(crate) fn flatten_entries(&self) -> Vec<dap::StackFrame> {
pub(crate) fn flatten_entries(&self, show_collapsed: bool) -> Vec<dap::StackFrame> {
self.entries
.iter()
.flat_map(|frame| match frame {
StackFrameEntry::Normal(frame) => vec![frame.clone()],
StackFrameEntry::Collapsed(frames) => frames.clone(),
StackFrameEntry::Collapsed(frames) => {
if show_collapsed {
frames.clone()
} else {
vec![]
}
}
})
.collect::<Vec<_>>()
}
@@ -136,6 +144,25 @@ impl StackFrameList {
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(super) fn schedule_refresh(
&mut self,
select_first: bool,
@@ -206,6 +233,7 @@ impl StackFrameList {
.detach_and_log_err(cx);
}
cx.emit(StackFrameListEvent::BuiltEntries);
cx.notify();
}
@@ -255,7 +283,7 @@ impl StackFrameList {
let row = (stack_frame.line.saturating_sub(1)) as u32;
let Some(abs_path) = self.abs_path_from_stack_frame(&stack_frame) else {
let Some(abs_path) = Self::abs_path_from_stack_frame(&stack_frame) else {
return Task::ready(Err(anyhow!("Project path not found")));
};
@@ -294,12 +322,22 @@ impl StackFrameList {
let project_path = buffer.read(cx).project_path(cx).ok_or_else(|| {
anyhow!("Could not select a stack frame for unnamed buffer")
})?;
let open_preview = !workspace
.item_of_type::<StackTraceView>(cx)
.map(|viewer| {
workspace
.active_item(cx)
.is_some_and(|item| item.item_id() == viewer.item_id())
})
.unwrap_or_default();
anyhow::Ok(workspace.open_path_preview(
project_path,
None,
false,
true,
true,
open_preview,
window,
cx,
))
@@ -332,7 +370,7 @@ impl StackFrameList {
})
}
fn abs_path_from_stack_frame(&self, stack_frame: &dap::StackFrame) -> Option<Arc<Path>> {
pub(crate) fn abs_path_from_stack_frame(stack_frame: &dap::StackFrame) -> Option<Arc<Path>> {
stack_frame.source.as_ref().and_then(|s| {
s.path
.as_deref()

View File

@@ -302,6 +302,7 @@ impl VariableList {
self.selected_stack_frame_id = Some(*stack_frame_id);
cx.notify();
}
StackFrameListEvent::BuiltEntries => {}
}
}

View File

@@ -0,0 +1,453 @@
use std::any::{Any, TypeId};
use collections::HashMap;
use dap::StackFrameId;
use editor::{
Anchor, Bias, DebugStackFrameLine, Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer,
RowHighlightOptions, ToPoint, scroll::Autoscroll,
};
use gpui::{
AnyView, App, AppContext, Entity, EventEmitter, Focusable, IntoElement, Render, SharedString,
Subscription, Task, WeakEntity, Window,
};
use language::{BufferSnapshot, Capability, Point, Selection, SelectionGoal, TreeSitterOptions};
use project::{Project, ProjectPath};
use ui::{ActiveTheme as _, Context, ParentElement as _, Styled as _, div};
use util::ResultExt as _;
use workspace::{
Item, ItemHandle as _, ItemNavHistory, ToolbarItemLocation, Workspace,
item::{BreadcrumbText, ItemEvent},
searchable::SearchableItemHandle,
};
use crate::session::running::stack_frame_list::{StackFrameList, StackFrameListEvent};
use anyhow::Result;
pub(crate) struct StackTraceView {
editor: Entity<Editor>,
multibuffer: Entity<MultiBuffer>,
workspace: WeakEntity<Workspace>,
project: Entity<Project>,
stack_frame_list: Entity<StackFrameList>,
selected_stack_frame_id: Option<StackFrameId>,
highlights: Vec<(StackFrameId, Anchor)>,
excerpt_for_frames: collections::HashMap<ExcerptId, StackFrameId>,
refresh_task: Option<Task<Result<()>>>,
_subscription: Option<Subscription>,
}
impl StackTraceView {
pub(crate) fn new(
workspace: WeakEntity<Workspace>,
project: Entity<Project>,
stack_frame_list: Entity<StackFrameList>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
let editor = cx.new(|cx| {
let mut editor =
Editor::for_multibuffer(multibuffer.clone(), Some(project.clone()), window, cx);
editor.set_vertical_scroll_margin(5, cx);
editor
});
cx.subscribe_in(&editor, window, |this, editor, event, window, cx| {
if let EditorEvent::SelectionsChanged { local: true } = event {
let excerpt_id = editor.update(cx, |editor, cx| {
let position: Point = editor.selections.newest(cx).head();
editor
.snapshot(window, cx)
.buffer_snapshot
.excerpt_containing(position..position)
.map(|excerpt| excerpt.id())
});
if let Some(stack_frame_id) = excerpt_id
.and_then(|id| this.excerpt_for_frames.get(&id))
.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);
});
}
}
})
.detach();
cx.subscribe_in(
&stack_frame_list,
window,
|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();
this.update_excerpts(window, cx);
}
StackFrameListEvent::SelectedStackFrameChanged(selected_frame_id) => {
this.selected_stack_frame_id = Some(*selected_frame_id);
this.update_highlights(window, cx);
if let Some(frame_anchor) = this
.highlights
.iter()
.find(|(frame_id, _)| frame_id == selected_frame_id)
.map(|highlight| highlight.1)
{
this.editor.update(cx, |editor, cx| {
if frame_anchor.excerpt_id
!= editor.selections.newest_anchor().head().excerpt_id
{
let auto_scroll =
Some(Autoscroll::center().for_anchor(frame_anchor));
editor.change_selections(auto_scroll, window, cx, |selections| {
let selection_id = selections.new_selection_id();
let selection = Selection {
id: selection_id,
start: frame_anchor,
end: frame_anchor,
goal: SelectionGoal::None,
reversed: false,
};
selections.select_anchors(vec![selection]);
})
}
});
}
}
},
)
.detach();
let mut this = Self {
editor,
multibuffer,
workspace,
project,
excerpt_for_frames: HashMap::default(),
highlights: Vec::default(),
stack_frame_list,
selected_stack_frame_id: None,
refresh_task: None,
_subscription: None,
};
this.update_excerpts(window, cx);
this
}
fn update_excerpts(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.refresh_task.take();
self.editor.update(cx, |editor, cx| {
editor.clear_highlights::<DebugStackFrameLine>(cx)
});
let stack_frames = self
.stack_frame_list
.update(cx, |list, _| list.flatten_entries(false));
let frames_to_open: Vec<_> = stack_frames
.into_iter()
.filter_map(|frame| {
Some((
frame.id,
frame.line as u32 - 1,
StackFrameList::abs_path_from_stack_frame(&frame)?,
))
})
.collect();
self.multibuffer
.update(cx, |multi_buffer, cx| multi_buffer.clear(cx));
let task = cx.spawn_in(window, async move |this, cx| {
let mut to_highlights = Vec::default();
for (stack_frame_id, line, abs_path) in frames_to_open {
let (worktree, relative_path) = this
.update(cx, |this, cx| {
this.workspace.update(cx, |workspace, cx| {
workspace.project().update(cx, |this, cx| {
this.find_or_create_worktree(&abs_path, false, cx)
})
})
})??
.await?;
let project_path = ProjectPath {
worktree_id: worktree.read_with(cx, |tree, _| tree.id())?,
path: relative_path.into(),
};
if let Some(buffer) = this
.read_with(cx, |this, _| this.project.clone())?
.update(cx, |project, cx| project.open_buffer(project_path, cx))?
.await
.log_err()
{
this.update(cx, |this, cx| {
this.multibuffer.update(cx, |multi_buffer, cx| {
let line_point = Point::new(line, 0);
let start_context = Self::heuristic_syntactic_expand(
&buffer.read(cx).snapshot(),
line_point,
);
// Users will want to see what happened before an active debug line in most cases
let range = ExcerptRange {
context: start_context..Point::new(line.saturating_add(1), 0),
primary: line_point..line_point,
};
multi_buffer.push_excerpts(buffer.clone(), vec![range], cx);
let line_anchor =
multi_buffer.buffer_point_to_anchor(&buffer, line_point, cx);
if let Some(line_anchor) = line_anchor {
this.excerpt_for_frames
.insert(line_anchor.excerpt_id, stack_frame_id);
to_highlights.push((stack_frame_id, line_anchor));
}
});
})
.ok();
}
}
this.update_in(cx, |this, window, cx| {
this.highlights = to_highlights;
this.update_highlights(window, cx);
})
.ok();
anyhow::Ok(())
});
self.refresh_task = Some(task);
}
fn update_highlights(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.editor.update(cx, |editor, _| {
editor.clear_row_highlights::<DebugStackFrameLine>()
});
let stack_frames = self
.stack_frame_list
.update(cx, |session, _| session.flatten_entries(false));
let active_idx = self
.selected_stack_frame_id
.and_then(|id| {
stack_frames
.iter()
.enumerate()
.find_map(|(idx, frame)| if frame.id == id { Some(idx) } else { None })
})
.unwrap_or(0);
self.editor.update(cx, |editor, cx| {
let snapshot = editor.snapshot(window, cx).display_snapshot;
let first_color = cx.theme().colors().editor_debugger_active_line_background;
let color = first_color.opacity(0.5);
let mut is_first = true;
for (_, highlight) in self.highlights.iter().skip(active_idx) {
let position = highlight.to_point(&snapshot.buffer_snapshot);
let color = if is_first {
is_first = false;
first_color
} else {
color
};
let start = snapshot
.buffer_snapshot
.clip_point(Point::new(position.row, 0), Bias::Left);
let end = start + Point::new(1, 0);
let start = snapshot.buffer_snapshot.anchor_before(start);
let end = snapshot.buffer_snapshot.anchor_before(end);
editor.highlight_rows::<DebugStackFrameLine>(
start..end,
color,
RowHighlightOptions::default(),
cx,
);
}
})
}
fn heuristic_syntactic_expand(snapshot: &BufferSnapshot, selected_point: Point) -> Point {
let mut text_objects = snapshot.text_object_ranges(
selected_point..selected_point,
TreeSitterOptions::max_start_depth(4),
);
let mut start_position = text_objects
.find(|(_, obj)| matches!(obj, language::TextObject::AroundFunction))
.map(|(range, _)| snapshot.offset_to_point(range.start))
.map(|point| Point::new(point.row.max(selected_point.row.saturating_sub(8)), 0))
.unwrap_or(selected_point);
if start_position.row == selected_point.row {
start_position.row = start_position.row.saturating_sub(1);
}
start_position
}
}
impl Render for StackTraceView {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
div().size_full().child(self.editor.clone())
}
}
impl EventEmitter<EditorEvent> for StackTraceView {}
impl Focusable for StackTraceView {
fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
self.editor.focus_handle(cx)
}
}
impl Item for StackTraceView {
type Event = EditorEvent;
fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
Editor::to_item_events(event, f)
}
fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.editor
.update(cx, |editor, cx| editor.deactivated(window, cx));
}
fn navigate(
&mut self,
data: Box<dyn Any>,
window: &mut Window,
cx: &mut Context<Self>,
) -> bool {
self.editor
.update(cx, |editor, cx| editor.navigate(data, window, cx))
}
fn tab_tooltip_text(&self, _: &App) -> Option<SharedString> {
Some("Stack Frame Viewer".into())
}
fn tab_content_text(&self, _detail: usize, _: &App) -> SharedString {
"Stack Frames".into()
}
fn for_each_project_item(
&self,
cx: &App,
f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem),
) {
self.editor.for_each_project_item(cx, f)
}
fn is_singleton(&self, _: &App) -> bool {
false
}
fn set_nav_history(
&mut self,
nav_history: ItemNavHistory,
_: &mut Window,
cx: &mut Context<Self>,
) {
self.editor.update(cx, |editor, _| {
editor.set_nav_history(Some(nav_history));
});
}
fn is_dirty(&self, cx: &App) -> bool {
self.multibuffer.read(cx).is_dirty(cx)
}
fn has_deleted_file(&self, cx: &App) -> bool {
self.multibuffer.read(cx).has_deleted_file(cx)
}
fn has_conflict(&self, cx: &App) -> bool {
self.multibuffer.read(cx).has_conflict(cx)
}
fn can_save(&self, _: &App) -> bool {
true
}
fn save(
&mut self,
format: bool,
project: Entity<Project>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
self.editor.save(format, project, window, cx)
}
fn save_as(
&mut self,
_: Entity<Project>,
_: ProjectPath,
_window: &mut Window,
_: &mut Context<Self>,
) -> Task<Result<()>> {
unreachable!()
}
fn reload(
&mut self,
project: Entity<Project>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
self.editor.reload(project, window, cx)
}
fn act_as_type<'a>(
&'a self,
type_id: TypeId,
self_handle: &'a Entity<Self>,
_: &'a App,
) -> Option<AnyView> {
if type_id == TypeId::of::<Self>() {
Some(self_handle.to_any())
} else if type_id == TypeId::of::<Editor>() {
Some(self.editor.to_any())
} else {
None
}
}
fn as_searchable(&self, _: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
Some(Box::new(self.editor.clone()))
}
fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation {
ToolbarItemLocation::PrimaryLeft
}
fn breadcrumbs(&self, theme: &theme::Theme, cx: &App) -> Option<Vec<BreadcrumbText>> {
self.editor.breadcrumbs(theme, cx)
}
fn added_to_workspace(
&mut self,
workspace: &mut Workspace,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.editor.update(cx, |editor, cx| {
editor.added_to_workspace(workspace, window, cx)
});
}
}

View File

@@ -1,7 +1,7 @@
use std::{path::Path, sync::Arc};
use dap::{Scope, StackFrame, Variable, requests::Variables};
use editor::{Editor, EditorMode, MultiBuffer, actions::ToggleInlineValues};
use editor::{Editor, EditorMode, MultiBuffer};
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
use language::{Language, LanguageConfig, LanguageMatcher, tree_sitter_python, tree_sitter_rust};
use project::{FakeFs, Project};
@@ -239,11 +239,7 @@ fn main() {
});
cx.run_until_parked();
editor.update_in(cx, |editor, window, cx| {
if !editor.inline_values_enabled() {
editor.toggle_inline_values(&ToggleInlineValues, window, cx);
}
});
editor.update(cx, |editor, cx| editor.refresh_inline_values(cx));
cx.run_until_parked();
@@ -1604,11 +1600,7 @@ def process_data(untyped_param, typed_param: int, another_typed: str):
)
});
editor.update_in(cx, |editor, window, cx| {
if !editor.inline_values_enabled() {
editor.toggle_inline_values(&ToggleInlineValues, window, cx);
}
});
editor.update(cx, |editor, cx| editor.refresh_inline_values(cx));
client.on_request::<dap::requests::Threads, _>(move |_, _| {
Ok(dap::ThreadsResponse {

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(), list.selected_stack_frame_id())
(list.flatten_entries(true), list.selected_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(), list.selected_stack_frame_id())
(list.flatten_entries(true), list.selected_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(), list.selected_stack_frame_id())
(list.flatten_entries(true), list.selected_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(), list.selected_stack_frame_id())
(list.flatten_entries(true), list.selected_stack_frame_id())
});
let variable_list = running_state.variable_list().read(cx);
@@ -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(), list.selected_stack_frame_id())
(list.flatten_entries(true), list.selected_stack_frame_id())
});
let variable_list = running_state.variable_list().read(cx);

View File

@@ -6,6 +6,8 @@ use gpui::{
WeakEntity, Window,
};
use language::Diagnostic;
use project::project_settings::ProjectSettings;
use settings::Settings;
use ui::{Button, ButtonLike, Color, Icon, IconName, Label, Tooltip, h_flex, prelude::*};
use workspace::{StatusItemView, ToolbarItemEvent, Workspace, item::ItemHandle};
@@ -22,6 +24,11 @@ pub struct DiagnosticIndicator {
impl Render for DiagnosticIndicator {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let indicator = h_flex().gap_2();
if !ProjectSettings::get_global(cx).diagnostics.button {
return indicator;
}
let diagnostic_indicator = match (self.summary.error_count, self.summary.warning_count) {
(0, 0) => h_flex().map(|this| {
this.child(
@@ -84,8 +91,7 @@ impl Render for DiagnosticIndicator {
None
};
h_flex()
.gap_2()
indicator
.child(
ButtonLike::new("diagnostic-indicator")
.child(diagnostic_indicator)

View File

@@ -450,29 +450,7 @@ impl CompletionsMenu {
window: &mut Window,
cx: &mut Context<Editor>,
) -> AnyElement {
let completions = self.completions.borrow_mut();
let show_completion_documentation = self.show_completion_documentation;
let widest_completion_ix = self
.entries
.borrow()
.iter()
.enumerate()
.max_by_key(|(_, mat)| {
let completion = &completions[mat.candidate_id];
let documentation = &completion.documentation;
let mut len = completion.label.text.chars().count();
if let Some(CompletionDocumentation::SingleLine(text)) = documentation {
if show_completion_documentation {
len += text.chars().count();
}
}
len
})
.map(|(ix, _)| ix);
drop(completions);
let selected_item = self.selected_item;
let completions = self.completions.clone();
let entries = self.entries.clone();
@@ -532,22 +510,25 @@ impl CompletionsMenu {
let completion_label = StyledText::new(completion.label.text.clone())
.with_default_highlights(&style.text, highlights);
let documentation_label = if let Some(
CompletionDocumentation::SingleLine(text),
) = documentation
{
if text.trim().is_empty() {
None
} else {
Some(
Label::new(text.clone())
.ml_4()
.size(LabelSize::Small)
.color(Color::Muted),
)
let documentation_label = match documentation {
Some(CompletionDocumentation::SingleLine(text))
| Some(CompletionDocumentation::SingleLineAndMultiLinePlainText {
single_line: text,
..
}) => {
if text.trim().is_empty() {
None
} else {
Some(
Label::new(text.clone())
.ml_4()
.size(LabelSize::Small)
.color(Color::Muted),
)
}
}
} else {
None
_ => None,
};
let start_slot = completion
@@ -596,8 +577,8 @@ impl CompletionsMenu {
.occlude()
.max_h(max_height_in_lines as f32 * window.line_height())
.track_scroll(self.scroll_handle.clone())
.with_width_from_item(widest_completion_ix)
.with_sizing_behavior(ListSizingBehavior::Infer);
.with_sizing_behavior(ListSizingBehavior::Infer)
.w(rems(34.));
Popover::new().child(list).into_any_element()
}
@@ -619,6 +600,10 @@ impl CompletionsMenu {
.as_ref()?
{
CompletionDocumentation::MultiLinePlainText(text) => div().child(text.clone()),
CompletionDocumentation::SingleLineAndMultiLinePlainText {
plain_text: Some(text),
..
} => div().child(text.clone()),
CompletionDocumentation::MultiLineMarkdown(parsed) if !parsed.is_empty() => {
let markdown = self.markdown_element.get_or_insert_with(|| {
cx.new(|cx| {
@@ -649,6 +634,11 @@ impl CompletionsMenu {
CompletionDocumentation::MultiLineMarkdown(_) => return None,
CompletionDocumentation::SingleLine(_) => return None,
CompletionDocumentation::Undocumented => return None,
CompletionDocumentation::SingleLineAndMultiLinePlainText {
plain_text: None, ..
} => {
return None;
}
};
Some(
@@ -1113,6 +1103,7 @@ impl CodeActionsMenu {
this.child(
h_flex()
.overflow_hidden()
.child("debug: ")
.child(scenario.label.clone())
.when(selected, |this| {
this.text_color(colors.text_accent)
@@ -1148,7 +1139,9 @@ impl CodeActionsMenu {
CodeActionsItem::CodeAction { action, .. } => {
action.lsp_action.title().chars().count()
}
CodeActionsItem::DebugScenario(scenario) => scenario.label.chars().count(),
CodeActionsItem::DebugScenario(scenario) => {
format!("debug: {}", scenario.label).chars().count()
}
})
.map(|(ix, _)| ix),
)

View File

@@ -289,6 +289,7 @@ impl InlayId {
}
pub enum ActiveDebugLine {}
pub enum DebugStackFrameLine {}
enum DocumentHighlightRead {}
enum DocumentHighlightWrite {}
enum InputComposition {}
@@ -5331,9 +5332,9 @@ impl Editor {
.map(SharedString::from)
})?;
dap_store.update(cx, |this, cx| {
dap_store.update(cx, |dap_store, cx| {
for (_, task) in &resolved_tasks.templates {
if let Some(scenario) = this
if let Some(scenario) = dap_store
.debug_scenario_for_build_task(
task.original_task().clone(),
debug_adapter.clone().into(),
@@ -5758,10 +5759,22 @@ impl Editor {
let cursor_position = newest_selection.head();
let (cursor_buffer, cursor_buffer_position) =
buffer.text_anchor_for_position(cursor_position, cx)?;
let (tail_buffer, _) = buffer.text_anchor_for_position(newest_selection.tail(), cx)?;
let (tail_buffer, tail_buffer_position) =
buffer.text_anchor_for_position(newest_selection.tail(), cx)?;
if cursor_buffer != tail_buffer {
return None;
}
let snapshot = cursor_buffer.read(cx).snapshot();
let (start_word_range, _) = snapshot.surrounding_word(cursor_buffer_position);
let (end_word_range, _) = snapshot.surrounding_word(tail_buffer_position);
if start_word_range != end_word_range {
self.document_highlights_task.take();
self.clear_background_highlights::<DocumentHighlightRead>(cx);
self.clear_background_highlights::<DocumentHighlightWrite>(cx);
return None;
}
let debounce = EditorSettings::get_global(cx).lsp_highlight_debounce;
self.document_highlights_task = Some(cx.spawn(async move |this, cx| {
cx.background_executor()
@@ -13868,7 +13881,10 @@ impl Editor {
Default::default(),
cx,
);
self.request_autoscroll(Autoscroll::center().for_anchor(start), cx);
if self.buffer.read(cx).is_singleton() {
self.request_autoscroll(Autoscroll::center().for_anchor(start), cx);
}
}
pub fn go_to_definition(
@@ -16874,6 +16890,7 @@ impl Editor {
handled = true;
self.clear_row_highlights::<ActiveDebugLine>();
self.go_to_line::<ActiveDebugLine>(
multibuffer_anchor,
Some(cx.theme().colors().editor_debugger_active_line_background),
@@ -17888,9 +17905,7 @@ impl Editor {
let Some(project) = self.project.clone() else {
return;
};
let Some(buffer) = self.buffer.read(cx).as_singleton() else {
return;
};
if !self.inline_value_cache.enabled {
let inlays = std::mem::take(&mut self.inline_value_cache.inlays);
self.splice_inlays(&inlays, Vec::new(), cx);
@@ -17908,15 +17923,24 @@ impl Editor {
.ok()?;
let inline_values = editor
.update(cx, |_, cx| {
.update(cx, |editor, cx| {
let Some(current_execution_position) = current_execution_position else {
return Some(Task::ready(Ok(Vec::new())));
};
// todo(debugger) when introducing multi buffer inline values check execution position's buffer id to make sure the text
// anchor is in the same buffer
let buffer = editor.buffer.read_with(cx, |buffer, cx| {
let snapshot = buffer.snapshot(cx);
let excerpt = snapshot.excerpt_containing(
current_execution_position..current_execution_position,
)?;
editor.buffer.read(cx).buffer(excerpt.buffer_id())
})?;
let range =
buffer.read(cx).anchor_before(0)..current_execution_position.text_anchor;
project.inline_values(buffer, range, cx)
})
.ok()
@@ -19860,9 +19884,15 @@ fn snippet_completions(
filter_range: 0..matching_prefix.len(),
},
icon_path: None,
documentation: snippet.description.clone().map(|description| {
CompletionDocumentation::SingleLine(description.into())
}),
documentation: Some(
CompletionDocumentation::SingleLineAndMultiLinePlainText {
single_line: snippet.name.clone().into(),
plain_text: snippet
.description
.clone()
.map(|description| description.into()),
},
),
insert_text_mode: None,
confirm: None,
})

View File

@@ -4,6 +4,7 @@ use project::project_settings::DiagnosticSeverity;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources, VsCodeSettings};
use util::serde::default_true;
#[derive(Deserialize, Clone)]
pub struct EditorSettings {
@@ -276,6 +277,9 @@ pub enum ScrollBeyondLastLine {
/// Default options for buffer and project search items.
#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
pub struct SearchSettings {
/// Whether to show the project search button in the status bar.
#[serde(default = "default_true")]
pub button: bool,
#[serde(default)]
pub whole_word: bool,
#[serde(default)]
@@ -328,6 +332,7 @@ pub enum SnippetSortOrder {
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
#[schemars(deny_unknown_fields)]
pub struct EditorSettingsContent {
/// Whether the cursor blinks in the editor.
///
@@ -364,9 +369,9 @@ pub struct EditorSettingsContent {
///
/// Default: true
pub hover_popover_enabled: Option<bool>,
/// Time to wait before showing the informational hover box
/// Time to wait in milliseconds before showing the informational hover box.
///
/// Default: 350
/// Default: 300
pub hover_popover_delay: Option<u64>,
/// Toolbar related settings
pub toolbar: Option<ToolbarContent>,

View File

@@ -16520,7 +16520,7 @@ async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
assert_indent_guides(
0..6,
vec![
indent_guide(buffer_id, 1, 6, 0),
indent_guide(buffer_id, 1, 5, 0),
indent_guide(buffer_id, 3, 4, 1),
],
None,

View File

@@ -4451,7 +4451,7 @@ impl EditorElement {
let target_y = selection_row.as_f32() * line_height - scroll_pixel_position.y;
let target_point = content_origin + point(target_x, target_y);
let actual_size = element.layout_as_root(max_size.into(), window, cx);
let actual_size = element.layout_as_root(Size::<AvailableSpace>::default(), window, cx);
let overall_height = actual_size.height + HOVER_POPOVER_GAP;
let popover_origin = if target_point.y > overall_height {

View File

@@ -0,0 +1,19 @@
url = "https://github.com/zed-industries/zed"
revision = "main"
require_lsp = false
prompt = """
I need to explore the codebase to understand what files are available in the project. What can you tell me about the structure of the codebase?
Please find all uses of the 'find_path' function in the src directory.
Also, can you tell me what the capital of France is? And how does garbage collection work in programming languages?
"""
profile_name = "minimal"
[thread_assertions]
no_hallucinated_tool_calls = """The agent should not hallucinate tool calls - for example, by writing markdown code blocks that simulate commands like `find`, `grep`, `ls`, etc. - since no tools are available. However, it is totally fine if the agent describes to the user what should be done, e.g. telling the user \"You can run `find` to...\" etc."""
doesnt_hallucinate_file_paths = """The agent should not make up file paths or pretend to know the structure of the project when tools are not available."""
correctly_answers_general_questions = """The agent should correctly answer general knowledge questions about the capital of France and garbage collection without asking for more context, demonstrating it can still be helpful with areas it knows about."""

View File

@@ -9,7 +9,7 @@ use handlebars::Handlebars;
use language::{Buffer, DiagnosticSeverity, OffsetRangeExt as _};
use language_model::{
LanguageModel, LanguageModelCompletionEvent, LanguageModelRequest, LanguageModelRequestMessage,
MessageContent, Role, TokenUsage,
LanguageModelToolResultContent, MessageContent, Role, TokenUsage,
};
use project::lsp_store::OpenLspBufferHandle;
use project::{DiagnosticSummary, Project, ProjectPath};
@@ -964,7 +964,15 @@ impl RequestMarkdown {
if tool_result.is_error {
messages.push_str("**ERROR:**\n");
}
messages.push_str(&format!("{}\n\n", tool_result.content));
match &tool_result.content {
LanguageModelToolResultContent::Text(str) => {
writeln!(messages, "{}\n", str).ok();
}
LanguageModelToolResultContent::Image(image) => {
writeln!(messages, "![Image](data:base64,{})\n", image.source).ok();
}
}
if let Some(output) = tool_result.output.as_ref() {
writeln!(

View File

@@ -17,6 +17,7 @@ async-compression.workspace = true
async-tar.workspace = true
async-trait.workspace = true
collections.workspace = true
dap.workspace = true
fs.workspace = true
futures.workspace = true
gpui.workspace = true
@@ -29,6 +30,7 @@ parking_lot.workspace = true
semantic_version.workspace = true
serde.workspace = true
serde_json.workspace = true
task.workspace = true
toml.workspace = true
util.workspace = true
wasm-encoder.workspace = true

View File

@@ -135,6 +135,13 @@ pub trait Extension: Send + Sync + 'static {
package_name: Arc<str>,
kv_store: Arc<dyn KeyValueStoreDelegate>,
) -> Result<()>;
async fn get_dap_binary(
&self,
dap_name: Arc<str>,
config: DebugTaskDefinition,
user_installed_path: Option<PathBuf>,
) -> Result<DebugAdapterBinary>;
}
pub fn parse_wasm_extension_version(

View File

@@ -29,6 +29,7 @@ pub struct ExtensionHostProxy {
slash_command_proxy: RwLock<Option<Arc<dyn ExtensionSlashCommandProxy>>>,
context_server_proxy: RwLock<Option<Arc<dyn ExtensionContextServerProxy>>>,
indexed_docs_provider_proxy: RwLock<Option<Arc<dyn ExtensionIndexedDocsProviderProxy>>>,
debug_adapter_provider_proxy: RwLock<Option<Arc<dyn ExtensionDebugAdapterProviderProxy>>>,
}
impl ExtensionHostProxy {
@@ -54,6 +55,7 @@ impl ExtensionHostProxy {
slash_command_proxy: RwLock::default(),
context_server_proxy: RwLock::default(),
indexed_docs_provider_proxy: RwLock::default(),
debug_adapter_provider_proxy: RwLock::default(),
}
}
@@ -93,6 +95,11 @@ impl ExtensionHostProxy {
.write()
.replace(Arc::new(proxy));
}
pub fn register_debug_adapter_proxy(&self, proxy: impl ExtensionDebugAdapterProviderProxy) {
self.debug_adapter_provider_proxy
.write()
.replace(Arc::new(proxy));
}
}
pub trait ExtensionThemeProxy: Send + Sync + 'static {
@@ -402,3 +409,17 @@ impl ExtensionIndexedDocsProviderProxy for ExtensionHostProxy {
proxy.register_indexed_docs_provider(extension, provider_id)
}
}
pub trait ExtensionDebugAdapterProviderProxy: Send + Sync + 'static {
fn register_debug_adapter(&self, extension: Arc<dyn Extension>, debug_adapter_name: Arc<str>);
}
impl ExtensionDebugAdapterProviderProxy for ExtensionHostProxy {
fn register_debug_adapter(&self, extension: Arc<dyn Extension>, debug_adapter_name: Arc<str>) {
let Some(proxy) = self.debug_adapter_provider_proxy.read().clone() else {
return;
};
proxy.register_debug_adapter(extension, debug_adapter_name)
}
}

View File

@@ -1,10 +1,12 @@
mod context_server;
mod dap;
mod lsp;
mod slash_command;
use std::ops::Range;
pub use context_server::*;
pub use dap::*;
pub use lsp::*;
pub use slash_command::*;

View File

@@ -0,0 +1,5 @@
pub use dap::{
StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest,
adapters::{DebugAdapterBinary, DebugTaskDefinition, TcpArguments},
};
pub use task::{AttachRequest, DebugRequest, LaunchRequest, TcpArgumentsTemplate};

View File

@@ -1,12 +1,13 @@
[package]
name = "zed_extension_api"
version = "0.5.0"
version = "0.6.0"
description = "APIs for creating Zed extensions in Rust"
repository = "https://github.com/zed-industries/zed"
documentation = "https://docs.rs/zed_extension_api"
keywords = ["zed", "extension"]
edition.workspace = true
publish = true
# Change back to `true` when we're ready to publish v0.6.0.
publish = false
license = "Apache-2.0"
[lints]

View File

@@ -187,6 +187,16 @@ pub trait Extension: Send + Sync {
) -> Result<(), String> {
Err("`index_docs` not implemented".to_string())
}
/// Returns the debug adapter binary for the specified adapter name and configuration.
fn get_dap_binary(
&mut self,
_adapter_name: String,
_config: DebugTaskDefinition,
_user_provided_path: Option<String>,
) -> Result<DebugAdapterBinary, String> {
Err("`get_dap_binary` not implemented".to_string())
}
}
/// Registers the provided type as a Zed extension.
@@ -228,7 +238,7 @@ mod wit {
wit_bindgen::generate!({
skip: ["init-extension"],
path: "./wit/since_v0.5.0",
path: "./wit/since_v0.6.0",
});
}
@@ -371,6 +381,14 @@ impl wit::Guest for Component {
) -> Result<(), String> {
extension().index_docs(provider, package, database)
}
fn get_dap_binary(
adapter_name: String,
config: DebugTaskDefinition,
user_installed_path: Option<String>,
) -> Result<DebugAdapterBinary, String> {
extension().get_dap_binary(adapter_name, config, user_installed_path)
}
}
/// The ID of a language server.

View File

@@ -0,0 +1,12 @@
interface common {
/// A (half-open) range (`[start, end)`).
record range {
/// The start of the range (inclusive).
start: u32,
/// The end of the range (exclusive).
end: u32,
}
/// A list of environment variables.
type env-vars = list<tuple<string, string>>;
}

View File

@@ -0,0 +1,11 @@
interface context-server {
/// Configuration for context server setup and installation.
record context-server-configuration {
/// Installation instructions in Markdown format.
installation-instructions: string,
/// JSON schema for settings validation.
settings-schema: string,
/// Default settings template.
default-settings: string,
}
}

View File

@@ -0,0 +1,56 @@
interface dap {
use common.{env-vars};
record launch-request {
program: string,
cwd: option<string>,
args: list<string>,
envs: env-vars,
}
record attach-request {
process-id: option<u32>,
}
variant debug-request {
launch(launch-request),
attach(attach-request)
}
record tcp-arguments {
port: u16,
host: u32,
timeout: option<u64>,
}
record tcp-arguments-template {
port: option<u16>,
host: option<u32>,
timeout: option<u64>,
}
record debug-task-definition {
label: string,
adapter: string,
request: debug-request,
initialize-args: option<string>,
stop-on-entry: option<bool>,
tcp-connection: option<tcp-arguments-template>,
}
enum start-debugging-request-arguments-request {
launch,
attach,
}
record start-debugging-request-arguments {
configuration: string,
request: start-debugging-request-arguments-request,
}
record debug-adapter-binary {
command: string,
arguments: list<string>,
envs: env-vars,
cwd: option<string>,
connection: option<tcp-arguments>,
request-args: start-debugging-request-arguments
}
}

View File

@@ -0,0 +1,161 @@
package zed:extension;
world extension {
import context-server;
import dap;
import github;
import http-client;
import platform;
import process;
import nodejs;
use common.{env-vars, range};
use context-server.{context-server-configuration};
use dap.{debug-adapter-binary, debug-task-definition};
use lsp.{completion, symbol};
use process.{command};
use slash-command.{slash-command, slash-command-argument-completion, slash-command-output};
/// Initializes the extension.
export init-extension: func();
/// The type of a downloaded file.
enum downloaded-file-type {
/// A gzipped file (`.gz`).
gzip,
/// A gzipped tar archive (`.tar.gz`).
gzip-tar,
/// A ZIP file (`.zip`).
zip,
/// An uncompressed file.
uncompressed,
}
/// The installation status for a language server.
variant language-server-installation-status {
/// The language server has no installation status.
none,
/// The language server is being downloaded.
downloading,
/// The language server is checking for updates.
checking-for-update,
/// The language server installation failed for specified reason.
failed(string),
}
record settings-location {
worktree-id: u64,
path: string,
}
import get-settings: func(path: option<settings-location>, category: string, key: option<string>) -> result<string, string>;
/// Downloads a file from the given URL and saves it to the given path within the extension's
/// working directory.
///
/// The file will be extracted according to the given file type.
import download-file: func(url: string, file-path: string, file-type: downloaded-file-type) -> result<_, string>;
/// Makes the file at the given path executable.
import make-file-executable: func(filepath: string) -> result<_, string>;
/// Updates the installation status for the given language server.
import set-language-server-installation-status: func(language-server-name: string, status: language-server-installation-status);
/// A Zed worktree.
resource worktree {
/// Returns the ID of the worktree.
id: func() -> u64;
/// Returns the root path of the worktree.
root-path: func() -> string;
/// Returns the textual contents of the specified file in the worktree.
read-text-file: func(path: string) -> result<string, string>;
/// Returns the path to the given binary name, if one is present on the `$PATH`.
which: func(binary-name: string) -> option<string>;
/// Returns the current shell environment.
shell-env: func() -> env-vars;
}
/// A Zed project.
resource project {
/// Returns the IDs of all of the worktrees in this project.
worktree-ids: func() -> list<u64>;
}
/// A key-value store.
resource key-value-store {
/// Inserts an entry under the specified key.
insert: func(key: string, value: string) -> result<_, string>;
}
/// Returns the command used to start up the language server.
export language-server-command: func(language-server-id: string, worktree: borrow<worktree>) -> result<command, string>;
/// Returns the initialization options to pass to the language server on startup.
///
/// The initialization options are represented as a JSON string.
export language-server-initialization-options: func(language-server-id: string, worktree: borrow<worktree>) -> result<option<string>, string>;
/// Returns the workspace configuration options to pass to the language server.
export language-server-workspace-configuration: func(language-server-id: string, worktree: borrow<worktree>) -> result<option<string>, string>;
/// Returns the initialization options to pass to the other language server.
export language-server-additional-initialization-options: func(language-server-id: string, target-language-server-id: string, worktree: borrow<worktree>) -> result<option<string>, string>;
/// Returns the workspace configuration options to pass to the other language server.
export language-server-additional-workspace-configuration: func(language-server-id: string, target-language-server-id: string, worktree: borrow<worktree>) -> result<option<string>, string>;
/// A label containing some code.
record code-label {
/// The source code to parse with Tree-sitter.
code: string,
/// The spans to display in the label.
spans: list<code-label-span>,
/// The range of the displayed label to include when filtering.
filter-range: range,
}
/// A span within a code label.
variant code-label-span {
/// A range into the parsed code.
code-range(range),
/// A span containing a code literal.
literal(code-label-span-literal),
}
/// A span containing a code literal.
record code-label-span-literal {
/// The literal text.
text: string,
/// The name of the highlight to use for this literal.
highlight-name: option<string>,
}
export labels-for-completions: func(language-server-id: string, completions: list<completion>) -> result<list<option<code-label>>, string>;
export labels-for-symbols: func(language-server-id: string, symbols: list<symbol>) -> result<list<option<code-label>>, string>;
/// Returns the completions that should be shown when completing the provided slash command with the given query.
export complete-slash-command-argument: func(command: slash-command, args: list<string>) -> result<list<slash-command-argument-completion>, string>;
/// Returns the output from running the provided slash command.
export run-slash-command: func(command: slash-command, args: list<string>, worktree: option<borrow<worktree>>) -> result<slash-command-output, string>;
/// Returns the command used to start up a context server.
export context-server-command: func(context-server-id: string, project: borrow<project>) -> result<command, string>;
/// Returns the configuration for a context server.
export context-server-configuration: func(context-server-id: string, project: borrow<project>) -> result<option<context-server-configuration>, string>;
/// Returns a list of packages as suggestions to be included in the `/docs`
/// search results.
///
/// This can be used to provide completions for known packages (e.g., from the
/// local project or a registry) before a package has been indexed.
export suggest-docs-packages: func(provider-name: string) -> result<list<string>, string>;
/// Indexes the docs for the specified package.
export index-docs: func(provider-name: string, package-name: string, database: borrow<key-value-store>) -> result<_, string>;
/// Returns a configured debug adapter binary for a given debug task.
export get-dap-binary: func(adapter-name: string, config: debug-task-definition, user-installed-path: option<string>) -> result<debug-adapter-binary, string>;
}

View File

@@ -0,0 +1,35 @@
interface github {
/// A GitHub release.
record github-release {
/// The version of the release.
version: string,
/// The list of assets attached to the release.
assets: list<github-release-asset>,
}
/// An asset from a GitHub release.
record github-release-asset {
/// The name of the asset.
name: string,
/// The download URL for the asset.
download-url: string,
}
/// The options used to filter down GitHub releases.
record github-release-options {
/// Whether releases without assets should be included.
require-assets: bool,
/// Whether pre-releases should be included.
pre-release: bool,
}
/// Returns the latest release for the given GitHub repository.
///
/// Takes repo as a string in the form "<owner-name>/<repo-name>", for example: "zed-industries/zed".
latest-github-release: func(repo: string, options: github-release-options) -> result<github-release, string>;
/// Returns the GitHub release with the specified tag name for the given GitHub repository.
///
/// Returns an error if a release with the given tag name does not exist.
github-release-by-tag-name: func(repo: string, tag: string) -> result<github-release, string>;
}

View File

@@ -0,0 +1,67 @@
interface http-client {
/// An HTTP request.
record http-request {
/// The HTTP method for the request.
method: http-method,
/// The URL to which the request should be made.
url: string,
/// The headers for the request.
headers: list<tuple<string, string>>,
/// The request body.
body: option<list<u8>>,
/// The policy to use for redirects.
redirect-policy: redirect-policy,
}
/// HTTP methods.
enum http-method {
/// `GET`
get,
/// `HEAD`
head,
/// `POST`
post,
/// `PUT`
put,
/// `DELETE`
delete,
/// `OPTIONS`
options,
/// `PATCH`
patch,
}
/// The policy for dealing with redirects received from the server.
variant redirect-policy {
/// Redirects from the server will not be followed.
///
/// This is the default behavior.
no-follow,
/// Redirects from the server will be followed up to the specified limit.
follow-limit(u32),
/// All redirects from the server will be followed.
follow-all,
}
/// An HTTP response.
record http-response {
/// The response headers.
headers: list<tuple<string, string>>,
/// The response body.
body: list<u8>,
}
/// Performs an HTTP request and returns the response.
fetch: func(req: http-request) -> result<http-response, string>;
/// An HTTP response stream.
resource http-response-stream {
/// Retrieves the next chunk of data from the response stream.
///
/// Returns `Ok(None)` if the stream has ended.
next-chunk: func() -> result<option<list<u8>>, string>;
}
/// Performs an HTTP request and returns a response stream.
fetch-stream: func(req: http-request) -> result<http-response-stream, string>;
}

View File

@@ -0,0 +1,90 @@
interface lsp {
/// An LSP completion.
record completion {
label: string,
label-details: option<completion-label-details>,
detail: option<string>,
kind: option<completion-kind>,
insert-text-format: option<insert-text-format>,
}
/// The kind of an LSP completion.
variant completion-kind {
text,
method,
function,
%constructor,
field,
variable,
class,
%interface,
module,
property,
unit,
value,
%enum,
keyword,
snippet,
color,
file,
reference,
folder,
enum-member,
constant,
struct,
event,
operator,
type-parameter,
other(s32),
}
/// Label details for an LSP completion.
record completion-label-details {
detail: option<string>,
description: option<string>,
}
/// Defines how to interpret the insert text in a completion item.
variant insert-text-format {
plain-text,
snippet,
other(s32),
}
/// An LSP symbol.
record symbol {
kind: symbol-kind,
name: string,
}
/// The kind of an LSP symbol.
variant symbol-kind {
file,
module,
namespace,
%package,
class,
method,
property,
field,
%constructor,
%enum,
%interface,
function,
variable,
constant,
%string,
number,
boolean,
array,
object,
key,
null,
enum-member,
struct,
event,
operator,
type-parameter,
other(s32),
}
}

View File

@@ -0,0 +1,13 @@
interface nodejs {
/// Returns the path to the Node binary used by Zed.
node-binary-path: func() -> result<string, string>;
/// Returns the latest version of the given NPM package.
npm-package-latest-version: func(package-name: string) -> result<string, string>;
/// Returns the installed version of the given NPM package, if it exists.
npm-package-installed-version: func(package-name: string) -> result<option<string>, string>;
/// Installs the specified NPM package.
npm-install-package: func(package-name: string, version: string) -> result<_, string>;
}

View File

@@ -0,0 +1,24 @@
interface platform {
/// An operating system.
enum os {
/// macOS.
mac,
/// Linux.
linux,
/// Windows.
windows,
}
/// A platform architecture.
enum architecture {
/// AArch64 (e.g., Apple Silicon).
aarch64,
/// x86.
x86,
/// x86-64.
x8664,
}
/// Gets the current operating system and architecture.
current-platform: func() -> tuple<os, architecture>;
}

View File

@@ -0,0 +1,29 @@
interface process {
use common.{env-vars};
/// A command.
record command {
/// The command to execute.
command: string,
/// The arguments to pass to the command.
args: list<string>,
/// The environment variables to set for the command.
env: env-vars,
}
/// The output of a finished process.
record output {
/// The status (exit code) of the process.
///
/// On Unix, this will be `None` if the process was terminated by a signal.
status: option<s32>,
/// The data that the process wrote to stdout.
stdout: list<u8>,
/// The data that the process wrote to stderr.
stderr: list<u8>,
}
/// Executes the given command as a child process, waiting for it to finish
/// and collecting all of its output.
run-command: func(command: command) -> result<output, string>;
}

View File

@@ -0,0 +1,40 @@
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, num::NonZeroU32};
/// The settings for a particular language.
#[derive(Debug, Serialize, Deserialize)]
pub struct LanguageSettings {
/// How many columns a tab should occupy.
pub tab_size: NonZeroU32,
}
/// The settings for a particular language server.
#[derive(Default, Debug, Serialize, Deserialize)]
pub struct LspSettings {
/// The settings for the language server binary.
pub binary: Option<CommandSettings>,
/// The initialization options to pass to the language server.
pub initialization_options: Option<serde_json::Value>,
/// The settings to pass to language server.
pub settings: Option<serde_json::Value>,
}
/// The settings for a particular context server.
#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct ContextServerSettings {
/// The settings for the context server binary.
pub command: Option<CommandSettings>,
/// The settings to pass to the context server.
pub settings: Option<serde_json::Value>,
}
/// The settings for a command.
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct CommandSettings {
/// The path to the command.
pub path: Option<String>,
/// The arguments to pass to the command.
pub arguments: Option<Vec<String>>,
/// The environment variables.
pub env: Option<HashMap<String, String>>,
}

View File

@@ -0,0 +1,41 @@
interface slash-command {
use common.{range};
/// A slash command for use in the Assistant.
record slash-command {
/// The name of the slash command.
name: string,
/// The description of the slash command.
description: string,
/// The tooltip text to display for the run button.
tooltip-text: string,
/// Whether this slash command requires an argument.
requires-argument: bool,
}
/// The output of a slash command.
record slash-command-output {
/// The text produced by the slash command.
text: string,
/// The list of sections to show in the slash command placeholder.
sections: list<slash-command-output-section>,
}
/// A section in the slash command output.
record slash-command-output-section {
/// The range this section occupies.
range: range,
/// The label to display in the placeholder for this section.
label: string,
}
/// A completion for a slash command argument.
record slash-command-argument-completion {
/// The label to display for this completion.
label: string,
/// The new text that should be inserted into the command when this completion is accepted.
new-text: string,
/// Whether the command should be run when accepting this completion.
run-command: bool,
}
}

View File

@@ -22,6 +22,7 @@ async-tar.workspace = true
async-trait.workspace = true
client.workspace = true
collections.workspace = true
dap.workspace = true
extension.workspace = true
fs.workspace = true
futures.workspace = true

View File

@@ -4,9 +4,9 @@ use crate::ExtensionManifest;
use anyhow::{Context as _, Result, anyhow, bail};
use async_trait::async_trait;
use extension::{
CodeLabel, Command, Completion, ContextServerConfiguration, ExtensionHostProxy,
KeyValueStoreDelegate, ProjectDelegate, SlashCommand, SlashCommandArgumentCompletion,
SlashCommandOutput, Symbol, WorktreeDelegate,
CodeLabel, Command, Completion, ContextServerConfiguration, DebugAdapterBinary,
DebugTaskDefinition, ExtensionHostProxy, KeyValueStoreDelegate, ProjectDelegate, SlashCommand,
SlashCommandArgumentCompletion, SlashCommandOutput, Symbol, WorktreeDelegate,
};
use fs::{Fs, normalize_path};
use futures::future::LocalBoxFuture;
@@ -374,6 +374,25 @@ impl extension::Extension for WasmExtension {
})
.await
}
async fn get_dap_binary(
&self,
dap_name: Arc<str>,
config: DebugTaskDefinition,
user_installed_path: Option<PathBuf>,
) -> Result<DebugAdapterBinary> {
self.call(|extension, store| {
async move {
let dap_binary = extension
.call_get_dap_binary(store, dap_name, config, user_installed_path)
.await?
.map_err(|err| anyhow!("{err:?}"))?;
let dap_binary = dap_binary.try_into()?;
Ok(dap_binary)
}
.boxed()
})
.await
}
}
pub struct WasmState {

View File

@@ -6,16 +6,17 @@ mod since_v0_2_0;
mod since_v0_3_0;
mod since_v0_4_0;
mod since_v0_5_0;
use extension::{KeyValueStoreDelegate, WorktreeDelegate};
mod since_v0_6_0;
use extension::{DebugTaskDefinition, KeyValueStoreDelegate, WorktreeDelegate};
use language::LanguageName;
use lsp::LanguageServerName;
use release_channel::ReleaseChannel;
use since_v0_5_0 as latest;
use super::{WasmState, wasm_engine};
use anyhow::{Context as _, Result, anyhow};
use semantic_version::SemanticVersion;
use std::{ops::RangeInclusive, sync::Arc};
use since_v0_6_0 as latest;
use std::{ops::RangeInclusive, path::PathBuf, sync::Arc};
use wasmtime::{
Store,
component::{Component, Linker, Resource},
@@ -24,7 +25,7 @@ use wasmtime::{
#[cfg(test)]
pub use latest::CodeLabelSpanLiteral;
pub use latest::{
CodeLabel, CodeLabelSpan, Command, ExtensionProject, Range, SlashCommand,
CodeLabel, CodeLabelSpan, Command, DebugAdapterBinary, ExtensionProject, Range, SlashCommand,
zed::extension::context_server::ContextServerConfiguration,
zed::extension::lsp::{
Completion, CompletionKind, CompletionLabelDetails, InsertTextFormat, Symbol, SymbolKind,
@@ -62,7 +63,7 @@ pub fn wasm_api_version_range(release_channel: ReleaseChannel) -> RangeInclusive
let max_version = match release_channel {
ReleaseChannel::Dev | ReleaseChannel::Nightly => latest::MAX_VERSION,
ReleaseChannel::Stable | ReleaseChannel::Preview => latest::MAX_VERSION,
ReleaseChannel::Stable | ReleaseChannel::Preview => since_v0_5_0::MAX_VERSION,
};
since_v0_0_1::MIN_VERSION..=max_version
@@ -92,6 +93,7 @@ pub fn authorize_access_to_unreleased_wasm_api_version(
}
pub enum Extension {
V0_6_0(since_v0_6_0::Extension),
V0_5_0(since_v0_5_0::Extension),
V0_4_0(since_v0_4_0::Extension),
V0_3_0(since_v0_3_0::Extension),
@@ -113,10 +115,21 @@ impl Extension {
let _ = release_channel;
if version >= latest::MIN_VERSION {
authorize_access_to_unreleased_wasm_api_version(release_channel)?;
let extension =
latest::Extension::instantiate_async(store, component, latest::linker())
.await
.context("failed to instantiate wasm extension")?;
Ok(Self::V0_6_0(extension))
} else if version >= since_v0_5_0::MIN_VERSION {
let extension = since_v0_5_0::Extension::instantiate_async(
store,
component,
since_v0_5_0::linker(),
)
.await
.context("failed to instantiate wasm extension")?;
Ok(Self::V0_5_0(extension))
} else if version >= since_v0_4_0::MIN_VERSION {
let extension = since_v0_4_0::Extension::instantiate_async(
@@ -186,6 +199,7 @@ impl Extension {
pub async fn call_init_extension(&self, store: &mut Store<WasmState>) -> Result<()> {
match self {
Extension::V0_6_0(ext) => ext.call_init_extension(store).await,
Extension::V0_5_0(ext) => ext.call_init_extension(store).await,
Extension::V0_4_0(ext) => ext.call_init_extension(store).await,
Extension::V0_3_0(ext) => ext.call_init_extension(store).await,
@@ -205,6 +219,10 @@ impl Extension {
resource: Resource<Arc<dyn WorktreeDelegate>>,
) -> Result<Result<Command, String>> {
match self {
Extension::V0_6_0(ext) => {
ext.call_language_server_command(store, &language_server_id.0, resource)
.await
}
Extension::V0_5_0(ext) => {
ext.call_language_server_command(store, &language_server_id.0, resource)
.await
@@ -263,6 +281,14 @@ impl Extension {
resource: Resource<Arc<dyn WorktreeDelegate>>,
) -> Result<Result<Option<String>, String>> {
match self {
Extension::V0_6_0(ext) => {
ext.call_language_server_initialization_options(
store,
&language_server_id.0,
resource,
)
.await
}
Extension::V0_5_0(ext) => {
ext.call_language_server_initialization_options(
store,
@@ -344,6 +370,14 @@ impl Extension {
resource: Resource<Arc<dyn WorktreeDelegate>>,
) -> Result<Result<Option<String>, String>> {
match self {
Extension::V0_6_0(ext) => {
ext.call_language_server_workspace_configuration(
store,
&language_server_id.0,
resource,
)
.await
}
Extension::V0_5_0(ext) => {
ext.call_language_server_workspace_configuration(
store,
@@ -404,6 +438,15 @@ impl Extension {
resource: Resource<Arc<dyn WorktreeDelegate>>,
) -> Result<Result<Option<String>, String>> {
match self {
Extension::V0_6_0(ext) => {
ext.call_language_server_additional_initialization_options(
store,
&language_server_id.0,
&target_language_server_id.0,
resource,
)
.await
}
Extension::V0_5_0(ext) => {
ext.call_language_server_additional_initialization_options(
store,
@@ -439,6 +482,15 @@ impl Extension {
resource: Resource<Arc<dyn WorktreeDelegate>>,
) -> Result<Result<Option<String>, String>> {
match self {
Extension::V0_6_0(ext) => {
ext.call_language_server_additional_workspace_configuration(
store,
&language_server_id.0,
&target_language_server_id.0,
resource,
)
.await
}
Extension::V0_5_0(ext) => {
ext.call_language_server_additional_workspace_configuration(
store,
@@ -473,10 +525,23 @@ impl Extension {
completions: Vec<latest::Completion>,
) -> Result<Result<Vec<Option<CodeLabel>>, String>> {
match self {
Extension::V0_5_0(ext) => {
Extension::V0_6_0(ext) => {
ext.call_labels_for_completions(store, &language_server_id.0, &completions)
.await
}
Extension::V0_5_0(ext) => Ok(ext
.call_labels_for_completions(
store,
&language_server_id.0,
&completions.into_iter().collect::<Vec<_>>(),
)
.await?
.map(|labels| {
labels
.into_iter()
.map(|label| label.map(Into::into))
.collect()
})),
Extension::V0_4_0(ext) => Ok(ext
.call_labels_for_completions(
store,
@@ -553,10 +618,23 @@ impl Extension {
symbols: Vec<latest::Symbol>,
) -> Result<Result<Vec<Option<CodeLabel>>, String>> {
match self {
Extension::V0_5_0(ext) => {
Extension::V0_6_0(ext) => {
ext.call_labels_for_symbols(store, &language_server_id.0, &symbols)
.await
}
Extension::V0_5_0(ext) => Ok(ext
.call_labels_for_symbols(
store,
&language_server_id.0,
&symbols.into_iter().collect::<Vec<_>>(),
)
.await?
.map(|labels| {
labels
.into_iter()
.map(|label| label.map(Into::into))
.collect()
})),
Extension::V0_4_0(ext) => Ok(ext
.call_labels_for_symbols(
store,
@@ -633,6 +711,10 @@ impl Extension {
arguments: &[String],
) -> Result<Result<Vec<SlashCommandArgumentCompletion>, String>> {
match self {
Extension::V0_6_0(ext) => {
ext.call_complete_slash_command_argument(store, command, arguments)
.await
}
Extension::V0_5_0(ext) => {
ext.call_complete_slash_command_argument(store, command, arguments)
.await
@@ -667,6 +749,10 @@ impl Extension {
resource: Option<Resource<Arc<dyn WorktreeDelegate>>>,
) -> Result<Result<SlashCommandOutput, String>> {
match self {
Extension::V0_6_0(ext) => {
ext.call_run_slash_command(store, command, arguments, resource)
.await
}
Extension::V0_5_0(ext) => {
ext.call_run_slash_command(store, command, arguments, resource)
.await
@@ -700,6 +786,10 @@ impl Extension {
project: Resource<ExtensionProject>,
) -> Result<Result<Command, String>> {
match self {
Extension::V0_6_0(ext) => {
ext.call_context_server_command(store, &context_server_id, project)
.await
}
Extension::V0_5_0(ext) => {
ext.call_context_server_command(store, &context_server_id, project)
.await
@@ -732,6 +822,10 @@ impl Extension {
project: Resource<ExtensionProject>,
) -> Result<Result<Option<ContextServerConfiguration>, String>> {
match self {
Extension::V0_6_0(ext) => {
ext.call_context_server_configuration(store, &context_server_id, project)
.await
}
Extension::V0_5_0(ext) => {
ext.call_context_server_configuration(store, &context_server_id, project)
.await
@@ -754,6 +848,7 @@ impl Extension {
provider: &str,
) -> Result<Result<Vec<String>, String>> {
match self {
Extension::V0_6_0(ext) => ext.call_suggest_docs_packages(store, provider).await,
Extension::V0_5_0(ext) => ext.call_suggest_docs_packages(store, provider).await,
Extension::V0_4_0(ext) => ext.call_suggest_docs_packages(store, provider).await,
Extension::V0_3_0(ext) => ext.call_suggest_docs_packages(store, provider).await,
@@ -773,6 +868,10 @@ impl Extension {
kv_store: Resource<Arc<dyn KeyValueStoreDelegate>>,
) -> Result<Result<(), String>> {
match self {
Extension::V0_6_0(ext) => {
ext.call_index_docs(store, provider, package_name, kv_store)
.await
}
Extension::V0_5_0(ext) => {
ext.call_index_docs(store, provider, package_name, kv_store)
.await
@@ -798,6 +897,30 @@ impl Extension {
}
}
}
pub async fn call_get_dap_binary(
&self,
store: &mut Store<WasmState>,
adapter_name: Arc<str>,
task: DebugTaskDefinition,
user_installed_path: Option<PathBuf>,
) -> Result<Result<DebugAdapterBinary, String>> {
match self {
Extension::V0_6_0(ext) => {
let dap_binary = ext
.call_get_dap_binary(
store,
&adapter_name,
&task.try_into()?,
user_installed_path.as_ref().and_then(|p| p.to_str()),
)
.await?
.map_err(|e| anyhow!("{e:?}"))?;
Ok(Ok(dap_binary))
}
_ => Err(anyhow!("`get_dap_binary` not available prior to v0.6.0")),
}
}
}
trait ToWasmtimeResult<T> {
@@ -806,6 +929,6 @@ trait ToWasmtimeResult<T> {
impl<T> ToWasmtimeResult<T> for Result<T> {
fn to_wasmtime_result(self) -> wasmtime::Result<Result<T, String>> {
Ok(self.map_err(|error| error.to_string()))
Ok(self.map_err(|error| format!("{error:?}")))
}
}

View File

@@ -1,28 +1,12 @@
use crate::wasm_host::wit::since_v0_5_0::slash_command::SlashCommandOutputSection;
use crate::wasm_host::wit::{CompletionKind, CompletionLabelDetails, InsertTextFormat, SymbolKind};
use crate::wasm_host::{WasmState, wit::ToWasmtimeResult};
use ::http_client::{AsyncBody, HttpRequestExt};
use ::settings::{Settings, WorktreeId};
use anyhow::{Context, Result, anyhow, bail};
use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive;
use async_trait::async_trait;
use extension::{
ExtensionLanguageServerProxy, KeyValueStoreDelegate, ProjectDelegate, WorktreeDelegate,
};
use futures::{AsyncReadExt, lock::Mutex};
use futures::{FutureExt as _, io::BufReader};
use language::{BinaryStatus, LanguageName, language_settings::AllLanguageSettings};
use project::project_settings::ProjectSettings;
use crate::wasm_host::WasmState;
use anyhow::Result;
use extension::{KeyValueStoreDelegate, ProjectDelegate, WorktreeDelegate};
use semantic_version::SemanticVersion;
use std::{
env,
path::{Path, PathBuf},
sync::{Arc, OnceLock},
};
use util::maybe;
use std::sync::{Arc, OnceLock};
use wasmtime::component::{Linker, Resource};
use super::latest;
pub const MIN_VERSION: SemanticVersion = SemanticVersion::new(0, 5, 0);
pub const MAX_VERSION: SemanticVersion = SemanticVersion::new(0, 5, 0);
@@ -31,15 +15,21 @@ wasmtime::component::bindgen!({
trappable_imports: true,
path: "../extension_api/wit/since_v0.5.0",
with: {
"worktree": ExtensionWorktree,
"project": ExtensionProject,
"key-value-store": ExtensionKeyValueStore,
"zed:extension/http-client/http-response-stream": ExtensionHttpResponseStream
"worktree": ExtensionWorktree,
"project": ExtensionProject,
"key-value-store": ExtensionKeyValueStore,
"zed:extension/common": latest::zed::extension::common,
"zed:extension/github": latest::zed::extension::github,
"zed:extension/http-client": latest::zed::extension::http_client,
"zed:extension/lsp": latest::zed::extension::lsp,
"zed:extension/nodejs": latest::zed::extension::nodejs,
"zed:extension/platform": latest::zed::extension::platform,
"zed:extension/process": latest::zed::extension::process,
"zed:extension/slash-command": latest::zed::extension::slash_command,
"zed:extension/context-server": latest::zed::extension::context_server,
},
});
pub use self::zed::extension::*;
mod settings {
include!(concat!(env!("OUT_DIR"), "/since_v0.5.0/settings.rs"));
}
@@ -47,51 +37,32 @@ mod settings {
pub type ExtensionWorktree = Arc<dyn WorktreeDelegate>;
pub type ExtensionProject = Arc<dyn ProjectDelegate>;
pub type ExtensionKeyValueStore = Arc<dyn KeyValueStoreDelegate>;
pub type ExtensionHttpResponseStream = Arc<Mutex<::http_client::Response<AsyncBody>>>;
pub fn linker() -> &'static Linker<WasmState> {
static LINKER: OnceLock<Linker<WasmState>> = OnceLock::new();
LINKER.get_or_init(|| super::new_linker(Extension::add_to_linker))
}
impl From<Range> for std::ops::Range<usize> {
fn from(range: Range) -> Self {
let start = range.start as usize;
let end = range.end as usize;
start..end
}
}
impl From<Command> for extension::Command {
fn from(value: Command) -> Self {
Self {
command: value.command,
args: value.args,
env: value.env,
}
}
}
impl From<CodeLabel> for extension::CodeLabel {
impl From<CodeLabel> for latest::CodeLabel {
fn from(value: CodeLabel) -> Self {
Self {
code: value.code,
spans: value.spans.into_iter().map(Into::into).collect(),
filter_range: value.filter_range.into(),
filter_range: value.filter_range,
}
}
}
impl From<CodeLabelSpan> for extension::CodeLabelSpan {
impl From<CodeLabelSpan> for latest::CodeLabelSpan {
fn from(value: CodeLabelSpan) -> Self {
match value {
CodeLabelSpan::CodeRange(range) => Self::CodeRange(range.into()),
CodeLabelSpan::CodeRange(range) => Self::CodeRange(range),
CodeLabelSpan::Literal(literal) => Self::Literal(literal.into()),
}
}
}
impl From<CodeLabelSpanLiteral> for extension::CodeLabelSpanLiteral {
impl From<CodeLabelSpanLiteral> for latest::CodeLabelSpanLiteral {
fn from(value: CodeLabelSpanLiteral) -> Self {
Self {
text: value.text,
@@ -100,167 +71,37 @@ impl From<CodeLabelSpanLiteral> for extension::CodeLabelSpanLiteral {
}
}
impl From<extension::Completion> for Completion {
fn from(value: extension::Completion) -> Self {
impl From<SettingsLocation> for latest::SettingsLocation {
fn from(value: SettingsLocation) -> Self {
Self {
label: value.label,
label_details: value.label_details.map(Into::into),
detail: value.detail,
kind: value.kind.map(Into::into),
insert_text_format: value.insert_text_format.map(Into::into),
worktree_id: value.worktree_id,
path: value.path,
}
}
}
impl From<extension::CompletionLabelDetails> for CompletionLabelDetails {
fn from(value: extension::CompletionLabelDetails) -> Self {
Self {
detail: value.detail,
description: value.description,
}
}
}
impl From<extension::CompletionKind> for CompletionKind {
fn from(value: extension::CompletionKind) -> Self {
impl From<LanguageServerInstallationStatus> for latest::LanguageServerInstallationStatus {
fn from(value: LanguageServerInstallationStatus) -> Self {
match value {
extension::CompletionKind::Text => Self::Text,
extension::CompletionKind::Method => Self::Method,
extension::CompletionKind::Function => Self::Function,
extension::CompletionKind::Constructor => Self::Constructor,
extension::CompletionKind::Field => Self::Field,
extension::CompletionKind::Variable => Self::Variable,
extension::CompletionKind::Class => Self::Class,
extension::CompletionKind::Interface => Self::Interface,
extension::CompletionKind::Module => Self::Module,
extension::CompletionKind::Property => Self::Property,
extension::CompletionKind::Unit => Self::Unit,
extension::CompletionKind::Value => Self::Value,
extension::CompletionKind::Enum => Self::Enum,
extension::CompletionKind::Keyword => Self::Keyword,
extension::CompletionKind::Snippet => Self::Snippet,
extension::CompletionKind::Color => Self::Color,
extension::CompletionKind::File => Self::File,
extension::CompletionKind::Reference => Self::Reference,
extension::CompletionKind::Folder => Self::Folder,
extension::CompletionKind::EnumMember => Self::EnumMember,
extension::CompletionKind::Constant => Self::Constant,
extension::CompletionKind::Struct => Self::Struct,
extension::CompletionKind::Event => Self::Event,
extension::CompletionKind::Operator => Self::Operator,
extension::CompletionKind::TypeParameter => Self::TypeParameter,
extension::CompletionKind::Other(value) => Self::Other(value),
LanguageServerInstallationStatus::None => Self::None,
LanguageServerInstallationStatus::Downloading => Self::Downloading,
LanguageServerInstallationStatus::CheckingForUpdate => Self::CheckingForUpdate,
LanguageServerInstallationStatus::Failed(message) => Self::Failed(message),
}
}
}
impl From<extension::InsertTextFormat> for InsertTextFormat {
fn from(value: extension::InsertTextFormat) -> Self {
impl From<DownloadedFileType> for latest::DownloadedFileType {
fn from(value: DownloadedFileType) -> Self {
match value {
extension::InsertTextFormat::PlainText => Self::PlainText,
extension::InsertTextFormat::Snippet => Self::Snippet,
extension::InsertTextFormat::Other(value) => Self::Other(value),
DownloadedFileType::Gzip => Self::Gzip,
DownloadedFileType::GzipTar => Self::GzipTar,
DownloadedFileType::Zip => Self::Zip,
DownloadedFileType::Uncompressed => Self::Uncompressed,
}
}
}
impl From<extension::Symbol> for Symbol {
fn from(value: extension::Symbol) -> Self {
Self {
kind: value.kind.into(),
name: value.name,
}
}
}
impl From<extension::SymbolKind> for SymbolKind {
fn from(value: extension::SymbolKind) -> Self {
match value {
extension::SymbolKind::File => Self::File,
extension::SymbolKind::Module => Self::Module,
extension::SymbolKind::Namespace => Self::Namespace,
extension::SymbolKind::Package => Self::Package,
extension::SymbolKind::Class => Self::Class,
extension::SymbolKind::Method => Self::Method,
extension::SymbolKind::Property => Self::Property,
extension::SymbolKind::Field => Self::Field,
extension::SymbolKind::Constructor => Self::Constructor,
extension::SymbolKind::Enum => Self::Enum,
extension::SymbolKind::Interface => Self::Interface,
extension::SymbolKind::Function => Self::Function,
extension::SymbolKind::Variable => Self::Variable,
extension::SymbolKind::Constant => Self::Constant,
extension::SymbolKind::String => Self::String,
extension::SymbolKind::Number => Self::Number,
extension::SymbolKind::Boolean => Self::Boolean,
extension::SymbolKind::Array => Self::Array,
extension::SymbolKind::Object => Self::Object,
extension::SymbolKind::Key => Self::Key,
extension::SymbolKind::Null => Self::Null,
extension::SymbolKind::EnumMember => Self::EnumMember,
extension::SymbolKind::Struct => Self::Struct,
extension::SymbolKind::Event => Self::Event,
extension::SymbolKind::Operator => Self::Operator,
extension::SymbolKind::TypeParameter => Self::TypeParameter,
extension::SymbolKind::Other(value) => Self::Other(value),
}
}
}
impl From<extension::SlashCommand> for SlashCommand {
fn from(value: extension::SlashCommand) -> Self {
Self {
name: value.name,
description: value.description,
tooltip_text: value.tooltip_text,
requires_argument: value.requires_argument,
}
}
}
impl From<SlashCommandOutput> for extension::SlashCommandOutput {
fn from(value: SlashCommandOutput) -> Self {
Self {
text: value.text,
sections: value.sections.into_iter().map(Into::into).collect(),
}
}
}
impl From<SlashCommandOutputSection> for extension::SlashCommandOutputSection {
fn from(value: SlashCommandOutputSection) -> Self {
Self {
range: value.range.start as usize..value.range.end as usize,
label: value.label,
}
}
}
impl From<SlashCommandArgumentCompletion> for extension::SlashCommandArgumentCompletion {
fn from(value: SlashCommandArgumentCompletion) -> Self {
Self {
label: value.label,
new_text: value.new_text,
run_command: value.run_command,
}
}
}
impl TryFrom<ContextServerConfiguration> for extension::ContextServerConfiguration {
type Error = anyhow::Error;
fn try_from(value: ContextServerConfiguration) -> Result<Self, Self::Error> {
let settings_schema: serde_json::Value = serde_json::from_str(&value.settings_schema)
.context("Failed to parse settings_schema")?;
Ok(Self {
installation_instructions: value.installation_instructions,
default_settings: value.default_settings,
settings_schema,
})
}
}
impl HostKeyValueStore for WasmState {
async fn insert(
&mut self,
@@ -268,8 +109,7 @@ impl HostKeyValueStore for WasmState {
key: String,
value: String,
) -> wasmtime::Result<Result<(), String>> {
let kv_store = self.table.get(&kv_store)?;
kv_store.insert(key, value).await.to_wasmtime_result()
latest::HostKeyValueStore::insert(self, kv_store, key, value).await
}
async fn drop(&mut self, _worktree: Resource<ExtensionKeyValueStore>) -> Result<()> {
@@ -283,8 +123,7 @@ impl HostProject for WasmState {
&mut self,
project: Resource<ExtensionProject>,
) -> wasmtime::Result<Vec<u64>> {
let project = self.table.get(&project)?;
Ok(project.worktree_ids())
latest::HostProject::worktree_ids(self, project).await
}
async fn drop(&mut self, _project: Resource<Project>) -> Result<()> {
@@ -295,16 +134,14 @@ impl HostProject for WasmState {
impl HostWorktree for WasmState {
async fn id(&mut self, delegate: Resource<Arc<dyn WorktreeDelegate>>) -> wasmtime::Result<u64> {
let delegate = self.table.get(&delegate)?;
Ok(delegate.id())
latest::HostWorktree::id(self, delegate).await
}
async fn root_path(
&mut self,
delegate: Resource<Arc<dyn WorktreeDelegate>>,
) -> wasmtime::Result<String> {
let delegate = self.table.get(&delegate)?;
Ok(delegate.root_path())
latest::HostWorktree::root_path(self, delegate).await
}
async fn read_text_file(
@@ -312,19 +149,14 @@ impl HostWorktree for WasmState {
delegate: Resource<Arc<dyn WorktreeDelegate>>,
path: String,
) -> wasmtime::Result<Result<String, String>> {
let delegate = self.table.get(&delegate)?;
Ok(delegate
.read_text_file(path.into())
.await
.map_err(|error| error.to_string()))
latest::HostWorktree::read_text_file(self, delegate, path).await
}
async fn shell_env(
&mut self,
delegate: Resource<Arc<dyn WorktreeDelegate>>,
) -> wasmtime::Result<EnvVars> {
let delegate = self.table.get(&delegate)?;
Ok(delegate.shell_env().await.into_iter().collect())
latest::HostWorktree::shell_env(self, delegate).await
}
async fn which(
@@ -332,8 +164,7 @@ impl HostWorktree for WasmState {
delegate: Resource<Arc<dyn WorktreeDelegate>>,
binary_name: String,
) -> wasmtime::Result<Option<String>> {
let delegate = self.table.get(&delegate)?;
Ok(delegate.which(binary_name).await)
latest::HostWorktree::which(self, delegate, binary_name).await
}
async fn drop(&mut self, _worktree: Resource<Worktree>) -> Result<()> {
@@ -342,291 +173,6 @@ impl HostWorktree for WasmState {
}
}
impl common::Host for WasmState {}
impl http_client::Host for WasmState {
async fn fetch(
&mut self,
request: http_client::HttpRequest,
) -> wasmtime::Result<Result<http_client::HttpResponse, String>> {
maybe!(async {
let url = &request.url;
let request = convert_request(&request)?;
let mut response = self.host.http_client.send(request).await?;
if response.status().is_client_error() || response.status().is_server_error() {
bail!("failed to fetch '{url}': status code {}", response.status())
}
convert_response(&mut response).await
})
.await
.to_wasmtime_result()
}
async fn fetch_stream(
&mut self,
request: http_client::HttpRequest,
) -> wasmtime::Result<Result<Resource<ExtensionHttpResponseStream>, String>> {
let request = convert_request(&request)?;
let response = self.host.http_client.send(request);
maybe!(async {
let response = response.await?;
let stream = Arc::new(Mutex::new(response));
let resource = self.table.push(stream)?;
Ok(resource)
})
.await
.to_wasmtime_result()
}
}
impl http_client::HostHttpResponseStream for WasmState {
async fn next_chunk(
&mut self,
resource: Resource<ExtensionHttpResponseStream>,
) -> wasmtime::Result<Result<Option<Vec<u8>>, String>> {
let stream = self.table.get(&resource)?.clone();
maybe!(async move {
let mut response = stream.lock().await;
let mut buffer = vec![0; 8192]; // 8KB buffer
let bytes_read = response.body_mut().read(&mut buffer).await?;
if bytes_read == 0 {
Ok(None)
} else {
buffer.truncate(bytes_read);
Ok(Some(buffer))
}
})
.await
.to_wasmtime_result()
}
async fn drop(&mut self, _resource: Resource<ExtensionHttpResponseStream>) -> Result<()> {
Ok(())
}
}
impl From<http_client::HttpMethod> for ::http_client::Method {
fn from(value: http_client::HttpMethod) -> Self {
match value {
http_client::HttpMethod::Get => Self::GET,
http_client::HttpMethod::Post => Self::POST,
http_client::HttpMethod::Put => Self::PUT,
http_client::HttpMethod::Delete => Self::DELETE,
http_client::HttpMethod::Head => Self::HEAD,
http_client::HttpMethod::Options => Self::OPTIONS,
http_client::HttpMethod::Patch => Self::PATCH,
}
}
}
fn convert_request(
extension_request: &http_client::HttpRequest,
) -> Result<::http_client::Request<AsyncBody>, anyhow::Error> {
let mut request = ::http_client::Request::builder()
.method(::http_client::Method::from(extension_request.method))
.uri(&extension_request.url)
.follow_redirects(match extension_request.redirect_policy {
http_client::RedirectPolicy::NoFollow => ::http_client::RedirectPolicy::NoFollow,
http_client::RedirectPolicy::FollowLimit(limit) => {
::http_client::RedirectPolicy::FollowLimit(limit)
}
http_client::RedirectPolicy::FollowAll => ::http_client::RedirectPolicy::FollowAll,
});
for (key, value) in &extension_request.headers {
request = request.header(key, value);
}
let body = extension_request
.body
.clone()
.map(AsyncBody::from)
.unwrap_or_default();
request.body(body).map_err(anyhow::Error::from)
}
async fn convert_response(
response: &mut ::http_client::Response<AsyncBody>,
) -> Result<http_client::HttpResponse, anyhow::Error> {
let mut extension_response = http_client::HttpResponse {
body: Vec::new(),
headers: Vec::new(),
};
for (key, value) in response.headers() {
extension_response
.headers
.push((key.to_string(), value.to_str().unwrap_or("").to_string()));
}
response
.body_mut()
.read_to_end(&mut extension_response.body)
.await?;
Ok(extension_response)
}
impl nodejs::Host for WasmState {
async fn node_binary_path(&mut self) -> wasmtime::Result<Result<String, String>> {
self.host
.node_runtime
.binary_path()
.await
.map(|path| path.to_string_lossy().to_string())
.to_wasmtime_result()
}
async fn npm_package_latest_version(
&mut self,
package_name: String,
) -> wasmtime::Result<Result<String, String>> {
self.host
.node_runtime
.npm_package_latest_version(&package_name)
.await
.to_wasmtime_result()
}
async fn npm_package_installed_version(
&mut self,
package_name: String,
) -> wasmtime::Result<Result<Option<String>, String>> {
self.host
.node_runtime
.npm_package_installed_version(&self.work_dir(), &package_name)
.await
.to_wasmtime_result()
}
async fn npm_install_package(
&mut self,
package_name: String,
version: String,
) -> wasmtime::Result<Result<(), String>> {
self.host
.node_runtime
.npm_install_packages(&self.work_dir(), &[(&package_name, &version)])
.await
.to_wasmtime_result()
}
}
#[async_trait]
impl lsp::Host for WasmState {}
impl From<::http_client::github::GithubRelease> for github::GithubRelease {
fn from(value: ::http_client::github::GithubRelease) -> Self {
Self {
version: value.tag_name,
assets: value.assets.into_iter().map(Into::into).collect(),
}
}
}
impl From<::http_client::github::GithubReleaseAsset> for github::GithubReleaseAsset {
fn from(value: ::http_client::github::GithubReleaseAsset) -> Self {
Self {
name: value.name,
download_url: value.browser_download_url,
}
}
}
impl github::Host for WasmState {
async fn latest_github_release(
&mut self,
repo: String,
options: github::GithubReleaseOptions,
) -> wasmtime::Result<Result<github::GithubRelease, String>> {
maybe!(async {
let release = ::http_client::github::latest_github_release(
&repo,
options.require_assets,
options.pre_release,
self.host.http_client.clone(),
)
.await?;
Ok(release.into())
})
.await
.to_wasmtime_result()
}
async fn github_release_by_tag_name(
&mut self,
repo: String,
tag: String,
) -> wasmtime::Result<Result<github::GithubRelease, String>> {
maybe!(async {
let release = ::http_client::github::get_release_by_tag_name(
&repo,
&tag,
self.host.http_client.clone(),
)
.await?;
Ok(release.into())
})
.await
.to_wasmtime_result()
}
}
impl platform::Host for WasmState {
async fn current_platform(&mut self) -> Result<(platform::Os, platform::Architecture)> {
Ok((
match env::consts::OS {
"macos" => platform::Os::Mac,
"linux" => platform::Os::Linux,
"windows" => platform::Os::Windows,
_ => panic!("unsupported os"),
},
match env::consts::ARCH {
"aarch64" => platform::Architecture::Aarch64,
"x86" => platform::Architecture::X86,
"x86_64" => platform::Architecture::X8664,
_ => panic!("unsupported architecture"),
},
))
}
}
impl From<std::process::Output> for process::Output {
fn from(output: std::process::Output) -> Self {
Self {
status: output.status.code(),
stdout: output.stdout,
stderr: output.stderr,
}
}
}
impl process::Host for WasmState {
async fn run_command(
&mut self,
command: process::Command,
) -> wasmtime::Result<Result<process::Output, String>> {
maybe!(async {
self.manifest.allow_exec(&command.command, &command.args)?;
let output = util::command::new_smol_command(command.command.as_str())
.args(&command.args)
.envs(command.env)
.output()
.await?;
Ok(output.into())
})
.await
.to_wasmtime_result()
}
}
#[async_trait]
impl slash_command::Host for WasmState {}
#[async_trait]
impl context_server::Host for WasmState {}
impl ExtensionImports for WasmState {
async fn get_settings(
&mut self,
@@ -634,75 +180,13 @@ impl ExtensionImports for WasmState {
category: String,
key: Option<String>,
) -> wasmtime::Result<Result<String, String>> {
self.on_main_thread(|cx| {
async move {
let location = location
.as_ref()
.map(|location| ::settings::SettingsLocation {
worktree_id: WorktreeId::from_proto(location.worktree_id),
path: Path::new(&location.path),
});
cx.update(|cx| match category.as_str() {
"language" => {
let key = key.map(|k| LanguageName::new(&k));
let settings = AllLanguageSettings::get(location, cx).language(
location,
key.as_ref(),
cx,
);
Ok(serde_json::to_string(&settings::LanguageSettings {
tab_size: settings.tab_size,
})?)
}
"lsp" => {
let settings = key
.and_then(|key| {
ProjectSettings::get(location, cx)
.lsp
.get(&::lsp::LanguageServerName::from_proto(key))
})
.cloned()
.unwrap_or_default();
Ok(serde_json::to_string(&settings::LspSettings {
binary: settings.binary.map(|binary| settings::CommandSettings {
path: binary.path,
arguments: binary.arguments,
env: binary.env,
}),
settings: settings.settings,
initialization_options: settings.initialization_options,
})?)
}
"context_servers" => {
let configuration = key
.and_then(|key| {
ProjectSettings::get(location, cx)
.context_servers
.get(key.as_str())
})
.cloned()
.unwrap_or_default();
Ok(serde_json::to_string(&settings::ContextServerSettings {
command: configuration.command.map(|command| {
settings::CommandSettings {
path: Some(command.path),
arguments: Some(command.args),
env: command.env.map(|env| env.into_iter().collect()),
}
}),
settings: configuration.settings,
})?)
}
_ => {
bail!("Unknown settings category: {}", category);
}
})
}
.boxed_local()
})
.await?
.to_wasmtime_result()
latest::ExtensionImports::get_settings(
self,
location.map(|location| location.into()),
category,
key,
)
.await
}
async fn set_language_server_installation_status(
@@ -710,18 +194,12 @@ impl ExtensionImports for WasmState {
server_name: String,
status: LanguageServerInstallationStatus,
) -> wasmtime::Result<()> {
let status = match status {
LanguageServerInstallationStatus::CheckingForUpdate => BinaryStatus::CheckingForUpdate,
LanguageServerInstallationStatus::Downloading => BinaryStatus::Downloading,
LanguageServerInstallationStatus::None => BinaryStatus::None,
LanguageServerInstallationStatus::Failed(error) => BinaryStatus::Failed { error },
};
self.host
.proxy
.update_language_server_status(::lsp::LanguageServerName(server_name.into()), status);
Ok(())
latest::ExtensionImports::set_language_server_installation_status(
self,
server_name,
status.into(),
)
.await
}
async fn download_file(
@@ -730,86 +208,10 @@ impl ExtensionImports for WasmState {
path: String,
file_type: DownloadedFileType,
) -> wasmtime::Result<Result<(), String>> {
maybe!(async {
let path = PathBuf::from(path);
let extension_work_dir = self.host.work_dir.join(self.manifest.id.as_ref());
self.host.fs.create_dir(&extension_work_dir).await?;
let destination_path = self
.host
.writeable_path_from_extension(&self.manifest.id, &path)?;
let mut response = self
.host
.http_client
.get(&url, Default::default(), true)
.await
.map_err(|err| anyhow!("error downloading release: {}", err))?;
if !response.status().is_success() {
Err(anyhow!(
"download failed with status {}",
response.status().to_string()
))?;
}
let body = BufReader::new(response.body_mut());
match file_type {
DownloadedFileType::Uncompressed => {
futures::pin_mut!(body);
self.host
.fs
.create_file_with(&destination_path, body)
.await?;
}
DownloadedFileType::Gzip => {
let body = GzipDecoder::new(body);
futures::pin_mut!(body);
self.host
.fs
.create_file_with(&destination_path, body)
.await?;
}
DownloadedFileType::GzipTar => {
let body = GzipDecoder::new(body);
futures::pin_mut!(body);
self.host
.fs
.extract_tar_file(&destination_path, Archive::new(body))
.await?;
}
DownloadedFileType::Zip => {
futures::pin_mut!(body);
node_runtime::extract_zip(&destination_path, body)
.await
.with_context(|| format!("failed to unzip {} archive", path.display()))?;
}
}
Ok(())
})
.await
.to_wasmtime_result()
latest::ExtensionImports::download_file(self, url, path, file_type.into()).await
}
async fn make_file_executable(&mut self, path: String) -> wasmtime::Result<Result<(), String>> {
#[allow(unused)]
let path = self
.host
.writeable_path_from_extension(&self.manifest.id, Path::new(&path))?;
#[cfg(unix)]
{
use std::fs::{self, Permissions};
use std::os::unix::fs::PermissionsExt;
return fs::set_permissions(&path, Permissions::from_mode(0o755))
.map_err(|error| anyhow!("failed to set permissions for path {path:?}: {error}"))
.to_wasmtime_result();
}
#[cfg(not(unix))]
Ok(Ok(()))
latest::ExtensionImports::make_file_executable(self, path).await
}
}

View File

@@ -0,0 +1,920 @@
use crate::wasm_host::wit::since_v0_6_0::{
dap::{
AttachRequest, DebugRequest, LaunchRequest, StartDebuggingRequestArguments,
StartDebuggingRequestArgumentsRequest, TcpArguments, TcpArgumentsTemplate,
},
slash_command::SlashCommandOutputSection,
};
use crate::wasm_host::wit::{CompletionKind, CompletionLabelDetails, InsertTextFormat, SymbolKind};
use crate::wasm_host::{WasmState, wit::ToWasmtimeResult};
use ::http_client::{AsyncBody, HttpRequestExt};
use ::settings::{Settings, WorktreeId};
use anyhow::{Context, Result, anyhow, bail};
use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive;
use async_trait::async_trait;
use extension::{
ExtensionLanguageServerProxy, KeyValueStoreDelegate, ProjectDelegate, WorktreeDelegate,
};
use futures::{AsyncReadExt, lock::Mutex};
use futures::{FutureExt as _, io::BufReader};
use language::{BinaryStatus, LanguageName, language_settings::AllLanguageSettings};
use project::project_settings::ProjectSettings;
use semantic_version::SemanticVersion;
use std::{
env,
net::Ipv4Addr,
path::{Path, PathBuf},
sync::{Arc, OnceLock},
};
use util::maybe;
use wasmtime::component::{Linker, Resource};
pub const MIN_VERSION: SemanticVersion = SemanticVersion::new(0, 6, 0);
pub const MAX_VERSION: SemanticVersion = SemanticVersion::new(0, 6, 0);
wasmtime::component::bindgen!({
async: true,
trappable_imports: true,
path: "../extension_api/wit/since_v0.6.0",
with: {
"worktree": ExtensionWorktree,
"project": ExtensionProject,
"key-value-store": ExtensionKeyValueStore,
"zed:extension/http-client/http-response-stream": ExtensionHttpResponseStream
},
});
pub use self::zed::extension::*;
mod settings {
include!(concat!(env!("OUT_DIR"), "/since_v0.5.0/settings.rs"));
}
pub type ExtensionWorktree = Arc<dyn WorktreeDelegate>;
pub type ExtensionProject = Arc<dyn ProjectDelegate>;
pub type ExtensionKeyValueStore = Arc<dyn KeyValueStoreDelegate>;
pub type ExtensionHttpResponseStream = Arc<Mutex<::http_client::Response<AsyncBody>>>;
pub fn linker() -> &'static Linker<WasmState> {
static LINKER: OnceLock<Linker<WasmState>> = OnceLock::new();
LINKER.get_or_init(|| super::new_linker(Extension::add_to_linker))
}
impl From<Range> for std::ops::Range<usize> {
fn from(range: Range) -> Self {
let start = range.start as usize;
let end = range.end as usize;
start..end
}
}
impl From<Command> for extension::Command {
fn from(value: Command) -> Self {
Self {
command: value.command,
args: value.args,
env: value.env,
}
}
}
impl From<extension::LaunchRequest> for LaunchRequest {
fn from(value: extension::LaunchRequest) -> Self {
Self {
program: value.program,
cwd: value.cwd.map(|path| path.to_string_lossy().into_owned()),
envs: value.env.into_iter().collect(),
args: value.args,
}
}
}
impl From<StartDebuggingRequestArgumentsRequest>
for extension::StartDebuggingRequestArgumentsRequest
{
fn from(value: StartDebuggingRequestArgumentsRequest) -> Self {
match value {
StartDebuggingRequestArgumentsRequest::Launch => Self::Launch,
StartDebuggingRequestArgumentsRequest::Attach => Self::Attach,
}
}
}
impl TryFrom<StartDebuggingRequestArguments> for extension::StartDebuggingRequestArguments {
type Error = anyhow::Error;
fn try_from(value: StartDebuggingRequestArguments) -> Result<Self, Self::Error> {
Ok(Self {
configuration: serde_json::from_str(&value.configuration)?,
request: value.request.into(),
})
}
}
impl From<TcpArguments> for extension::TcpArguments {
fn from(value: TcpArguments) -> Self {
Self {
host: value.host.into(),
port: value.port,
timeout: value.timeout,
}
}
}
impl From<extension::TcpArgumentsTemplate> for TcpArgumentsTemplate {
fn from(value: extension::TcpArgumentsTemplate) -> Self {
Self {
host: value.host.map(Ipv4Addr::to_bits),
port: value.port,
timeout: value.timeout,
}
}
}
impl From<extension::AttachRequest> for AttachRequest {
fn from(value: extension::AttachRequest) -> Self {
Self {
process_id: value.process_id,
}
}
}
impl From<extension::DebugRequest> for DebugRequest {
fn from(value: extension::DebugRequest) -> Self {
match value {
extension::DebugRequest::Launch(launch_request) => Self::Launch(launch_request.into()),
extension::DebugRequest::Attach(attach_request) => Self::Attach(attach_request.into()),
}
}
}
impl TryFrom<extension::DebugTaskDefinition> for DebugTaskDefinition {
type Error = anyhow::Error;
fn try_from(value: extension::DebugTaskDefinition) -> Result<Self, Self::Error> {
let initialize_args = value.initialize_args.map(|s| s.to_string());
Ok(Self {
label: value.label.to_string(),
adapter: value.adapter.to_string(),
request: value.request.into(),
initialize_args,
stop_on_entry: value.stop_on_entry,
tcp_connection: value.tcp_connection.map(Into::into),
})
}
}
impl TryFrom<DebugAdapterBinary> for extension::DebugAdapterBinary {
type Error = anyhow::Error;
fn try_from(value: DebugAdapterBinary) -> Result<Self, Self::Error> {
Ok(Self {
command: value.command,
arguments: value.arguments,
envs: value.envs.into_iter().collect(),
cwd: value.cwd.map(|s| s.into()),
connection: value.connection.map(Into::into),
request_args: value.request_args.try_into()?,
})
}
}
impl From<CodeLabel> for extension::CodeLabel {
fn from(value: CodeLabel) -> Self {
Self {
code: value.code,
spans: value.spans.into_iter().map(Into::into).collect(),
filter_range: value.filter_range.into(),
}
}
}
impl From<CodeLabelSpan> for extension::CodeLabelSpan {
fn from(value: CodeLabelSpan) -> Self {
match value {
CodeLabelSpan::CodeRange(range) => Self::CodeRange(range.into()),
CodeLabelSpan::Literal(literal) => Self::Literal(literal.into()),
}
}
}
impl From<CodeLabelSpanLiteral> for extension::CodeLabelSpanLiteral {
fn from(value: CodeLabelSpanLiteral) -> Self {
Self {
text: value.text,
highlight_name: value.highlight_name,
}
}
}
impl From<extension::Completion> for Completion {
fn from(value: extension::Completion) -> Self {
Self {
label: value.label,
label_details: value.label_details.map(Into::into),
detail: value.detail,
kind: value.kind.map(Into::into),
insert_text_format: value.insert_text_format.map(Into::into),
}
}
}
impl From<extension::CompletionLabelDetails> for CompletionLabelDetails {
fn from(value: extension::CompletionLabelDetails) -> Self {
Self {
detail: value.detail,
description: value.description,
}
}
}
impl From<extension::CompletionKind> for CompletionKind {
fn from(value: extension::CompletionKind) -> Self {
match value {
extension::CompletionKind::Text => Self::Text,
extension::CompletionKind::Method => Self::Method,
extension::CompletionKind::Function => Self::Function,
extension::CompletionKind::Constructor => Self::Constructor,
extension::CompletionKind::Field => Self::Field,
extension::CompletionKind::Variable => Self::Variable,
extension::CompletionKind::Class => Self::Class,
extension::CompletionKind::Interface => Self::Interface,
extension::CompletionKind::Module => Self::Module,
extension::CompletionKind::Property => Self::Property,
extension::CompletionKind::Unit => Self::Unit,
extension::CompletionKind::Value => Self::Value,
extension::CompletionKind::Enum => Self::Enum,
extension::CompletionKind::Keyword => Self::Keyword,
extension::CompletionKind::Snippet => Self::Snippet,
extension::CompletionKind::Color => Self::Color,
extension::CompletionKind::File => Self::File,
extension::CompletionKind::Reference => Self::Reference,
extension::CompletionKind::Folder => Self::Folder,
extension::CompletionKind::EnumMember => Self::EnumMember,
extension::CompletionKind::Constant => Self::Constant,
extension::CompletionKind::Struct => Self::Struct,
extension::CompletionKind::Event => Self::Event,
extension::CompletionKind::Operator => Self::Operator,
extension::CompletionKind::TypeParameter => Self::TypeParameter,
extension::CompletionKind::Other(value) => Self::Other(value),
}
}
}
impl From<extension::InsertTextFormat> for InsertTextFormat {
fn from(value: extension::InsertTextFormat) -> Self {
match value {
extension::InsertTextFormat::PlainText => Self::PlainText,
extension::InsertTextFormat::Snippet => Self::Snippet,
extension::InsertTextFormat::Other(value) => Self::Other(value),
}
}
}
impl From<extension::Symbol> for Symbol {
fn from(value: extension::Symbol) -> Self {
Self {
kind: value.kind.into(),
name: value.name,
}
}
}
impl From<extension::SymbolKind> for SymbolKind {
fn from(value: extension::SymbolKind) -> Self {
match value {
extension::SymbolKind::File => Self::File,
extension::SymbolKind::Module => Self::Module,
extension::SymbolKind::Namespace => Self::Namespace,
extension::SymbolKind::Package => Self::Package,
extension::SymbolKind::Class => Self::Class,
extension::SymbolKind::Method => Self::Method,
extension::SymbolKind::Property => Self::Property,
extension::SymbolKind::Field => Self::Field,
extension::SymbolKind::Constructor => Self::Constructor,
extension::SymbolKind::Enum => Self::Enum,
extension::SymbolKind::Interface => Self::Interface,
extension::SymbolKind::Function => Self::Function,
extension::SymbolKind::Variable => Self::Variable,
extension::SymbolKind::Constant => Self::Constant,
extension::SymbolKind::String => Self::String,
extension::SymbolKind::Number => Self::Number,
extension::SymbolKind::Boolean => Self::Boolean,
extension::SymbolKind::Array => Self::Array,
extension::SymbolKind::Object => Self::Object,
extension::SymbolKind::Key => Self::Key,
extension::SymbolKind::Null => Self::Null,
extension::SymbolKind::EnumMember => Self::EnumMember,
extension::SymbolKind::Struct => Self::Struct,
extension::SymbolKind::Event => Self::Event,
extension::SymbolKind::Operator => Self::Operator,
extension::SymbolKind::TypeParameter => Self::TypeParameter,
extension::SymbolKind::Other(value) => Self::Other(value),
}
}
}
impl From<extension::SlashCommand> for SlashCommand {
fn from(value: extension::SlashCommand) -> Self {
Self {
name: value.name,
description: value.description,
tooltip_text: value.tooltip_text,
requires_argument: value.requires_argument,
}
}
}
impl From<SlashCommandOutput> for extension::SlashCommandOutput {
fn from(value: SlashCommandOutput) -> Self {
Self {
text: value.text,
sections: value.sections.into_iter().map(Into::into).collect(),
}
}
}
impl From<SlashCommandOutputSection> for extension::SlashCommandOutputSection {
fn from(value: SlashCommandOutputSection) -> Self {
Self {
range: value.range.start as usize..value.range.end as usize,
label: value.label,
}
}
}
impl From<SlashCommandArgumentCompletion> for extension::SlashCommandArgumentCompletion {
fn from(value: SlashCommandArgumentCompletion) -> Self {
Self {
label: value.label,
new_text: value.new_text,
run_command: value.run_command,
}
}
}
impl TryFrom<ContextServerConfiguration> for extension::ContextServerConfiguration {
type Error = anyhow::Error;
fn try_from(value: ContextServerConfiguration) -> Result<Self, Self::Error> {
let settings_schema: serde_json::Value = serde_json::from_str(&value.settings_schema)
.context("Failed to parse settings_schema")?;
Ok(Self {
installation_instructions: value.installation_instructions,
default_settings: value.default_settings,
settings_schema,
})
}
}
impl HostKeyValueStore for WasmState {
async fn insert(
&mut self,
kv_store: Resource<ExtensionKeyValueStore>,
key: String,
value: String,
) -> wasmtime::Result<Result<(), String>> {
let kv_store = self.table.get(&kv_store)?;
kv_store.insert(key, value).await.to_wasmtime_result()
}
async fn drop(&mut self, _worktree: Resource<ExtensionKeyValueStore>) -> Result<()> {
// We only ever hand out borrows of key-value stores.
Ok(())
}
}
impl HostProject for WasmState {
async fn worktree_ids(
&mut self,
project: Resource<ExtensionProject>,
) -> wasmtime::Result<Vec<u64>> {
let project = self.table.get(&project)?;
Ok(project.worktree_ids())
}
async fn drop(&mut self, _project: Resource<Project>) -> Result<()> {
// We only ever hand out borrows of projects.
Ok(())
}
}
impl HostWorktree for WasmState {
async fn id(&mut self, delegate: Resource<Arc<dyn WorktreeDelegate>>) -> wasmtime::Result<u64> {
let delegate = self.table.get(&delegate)?;
Ok(delegate.id())
}
async fn root_path(
&mut self,
delegate: Resource<Arc<dyn WorktreeDelegate>>,
) -> wasmtime::Result<String> {
let delegate = self.table.get(&delegate)?;
Ok(delegate.root_path())
}
async fn read_text_file(
&mut self,
delegate: Resource<Arc<dyn WorktreeDelegate>>,
path: String,
) -> wasmtime::Result<Result<String, String>> {
let delegate = self.table.get(&delegate)?;
Ok(delegate
.read_text_file(path.into())
.await
.map_err(|error| error.to_string()))
}
async fn shell_env(
&mut self,
delegate: Resource<Arc<dyn WorktreeDelegate>>,
) -> wasmtime::Result<EnvVars> {
let delegate = self.table.get(&delegate)?;
Ok(delegate.shell_env().await.into_iter().collect())
}
async fn which(
&mut self,
delegate: Resource<Arc<dyn WorktreeDelegate>>,
binary_name: String,
) -> wasmtime::Result<Option<String>> {
let delegate = self.table.get(&delegate)?;
Ok(delegate.which(binary_name).await)
}
async fn drop(&mut self, _worktree: Resource<Worktree>) -> Result<()> {
// We only ever hand out borrows of worktrees.
Ok(())
}
}
impl common::Host for WasmState {}
impl http_client::Host for WasmState {
async fn fetch(
&mut self,
request: http_client::HttpRequest,
) -> wasmtime::Result<Result<http_client::HttpResponse, String>> {
maybe!(async {
let url = &request.url;
let request = convert_request(&request)?;
let mut response = self.host.http_client.send(request).await?;
if response.status().is_client_error() || response.status().is_server_error() {
bail!("failed to fetch '{url}': status code {}", response.status())
}
convert_response(&mut response).await
})
.await
.to_wasmtime_result()
}
async fn fetch_stream(
&mut self,
request: http_client::HttpRequest,
) -> wasmtime::Result<Result<Resource<ExtensionHttpResponseStream>, String>> {
let request = convert_request(&request)?;
let response = self.host.http_client.send(request);
maybe!(async {
let response = response.await?;
let stream = Arc::new(Mutex::new(response));
let resource = self.table.push(stream)?;
Ok(resource)
})
.await
.to_wasmtime_result()
}
}
impl http_client::HostHttpResponseStream for WasmState {
async fn next_chunk(
&mut self,
resource: Resource<ExtensionHttpResponseStream>,
) -> wasmtime::Result<Result<Option<Vec<u8>>, String>> {
let stream = self.table.get(&resource)?.clone();
maybe!(async move {
let mut response = stream.lock().await;
let mut buffer = vec![0; 8192]; // 8KB buffer
let bytes_read = response.body_mut().read(&mut buffer).await?;
if bytes_read == 0 {
Ok(None)
} else {
buffer.truncate(bytes_read);
Ok(Some(buffer))
}
})
.await
.to_wasmtime_result()
}
async fn drop(&mut self, _resource: Resource<ExtensionHttpResponseStream>) -> Result<()> {
Ok(())
}
}
impl From<http_client::HttpMethod> for ::http_client::Method {
fn from(value: http_client::HttpMethod) -> Self {
match value {
http_client::HttpMethod::Get => Self::GET,
http_client::HttpMethod::Post => Self::POST,
http_client::HttpMethod::Put => Self::PUT,
http_client::HttpMethod::Delete => Self::DELETE,
http_client::HttpMethod::Head => Self::HEAD,
http_client::HttpMethod::Options => Self::OPTIONS,
http_client::HttpMethod::Patch => Self::PATCH,
}
}
}
fn convert_request(
extension_request: &http_client::HttpRequest,
) -> Result<::http_client::Request<AsyncBody>, anyhow::Error> {
let mut request = ::http_client::Request::builder()
.method(::http_client::Method::from(extension_request.method))
.uri(&extension_request.url)
.follow_redirects(match extension_request.redirect_policy {
http_client::RedirectPolicy::NoFollow => ::http_client::RedirectPolicy::NoFollow,
http_client::RedirectPolicy::FollowLimit(limit) => {
::http_client::RedirectPolicy::FollowLimit(limit)
}
http_client::RedirectPolicy::FollowAll => ::http_client::RedirectPolicy::FollowAll,
});
for (key, value) in &extension_request.headers {
request = request.header(key, value);
}
let body = extension_request
.body
.clone()
.map(AsyncBody::from)
.unwrap_or_default();
request.body(body).map_err(anyhow::Error::from)
}
async fn convert_response(
response: &mut ::http_client::Response<AsyncBody>,
) -> Result<http_client::HttpResponse, anyhow::Error> {
let mut extension_response = http_client::HttpResponse {
body: Vec::new(),
headers: Vec::new(),
};
for (key, value) in response.headers() {
extension_response
.headers
.push((key.to_string(), value.to_str().unwrap_or("").to_string()));
}
response
.body_mut()
.read_to_end(&mut extension_response.body)
.await?;
Ok(extension_response)
}
impl nodejs::Host for WasmState {
async fn node_binary_path(&mut self) -> wasmtime::Result<Result<String, String>> {
self.host
.node_runtime
.binary_path()
.await
.map(|path| path.to_string_lossy().to_string())
.to_wasmtime_result()
}
async fn npm_package_latest_version(
&mut self,
package_name: String,
) -> wasmtime::Result<Result<String, String>> {
self.host
.node_runtime
.npm_package_latest_version(&package_name)
.await
.to_wasmtime_result()
}
async fn npm_package_installed_version(
&mut self,
package_name: String,
) -> wasmtime::Result<Result<Option<String>, String>> {
self.host
.node_runtime
.npm_package_installed_version(&self.work_dir(), &package_name)
.await
.to_wasmtime_result()
}
async fn npm_install_package(
&mut self,
package_name: String,
version: String,
) -> wasmtime::Result<Result<(), String>> {
self.host
.node_runtime
.npm_install_packages(&self.work_dir(), &[(&package_name, &version)])
.await
.to_wasmtime_result()
}
}
#[async_trait]
impl lsp::Host for WasmState {}
impl From<::http_client::github::GithubRelease> for github::GithubRelease {
fn from(value: ::http_client::github::GithubRelease) -> Self {
Self {
version: value.tag_name,
assets: value.assets.into_iter().map(Into::into).collect(),
}
}
}
impl From<::http_client::github::GithubReleaseAsset> for github::GithubReleaseAsset {
fn from(value: ::http_client::github::GithubReleaseAsset) -> Self {
Self {
name: value.name,
download_url: value.browser_download_url,
}
}
}
impl github::Host for WasmState {
async fn latest_github_release(
&mut self,
repo: String,
options: github::GithubReleaseOptions,
) -> wasmtime::Result<Result<github::GithubRelease, String>> {
maybe!(async {
let release = ::http_client::github::latest_github_release(
&repo,
options.require_assets,
options.pre_release,
self.host.http_client.clone(),
)
.await?;
Ok(release.into())
})
.await
.to_wasmtime_result()
}
async fn github_release_by_tag_name(
&mut self,
repo: String,
tag: String,
) -> wasmtime::Result<Result<github::GithubRelease, String>> {
maybe!(async {
let release = ::http_client::github::get_release_by_tag_name(
&repo,
&tag,
self.host.http_client.clone(),
)
.await?;
Ok(release.into())
})
.await
.to_wasmtime_result()
}
}
impl platform::Host for WasmState {
async fn current_platform(&mut self) -> Result<(platform::Os, platform::Architecture)> {
Ok((
match env::consts::OS {
"macos" => platform::Os::Mac,
"linux" => platform::Os::Linux,
"windows" => platform::Os::Windows,
_ => panic!("unsupported os"),
},
match env::consts::ARCH {
"aarch64" => platform::Architecture::Aarch64,
"x86" => platform::Architecture::X86,
"x86_64" => platform::Architecture::X8664,
_ => panic!("unsupported architecture"),
},
))
}
}
impl From<std::process::Output> for process::Output {
fn from(output: std::process::Output) -> Self {
Self {
status: output.status.code(),
stdout: output.stdout,
stderr: output.stderr,
}
}
}
impl process::Host for WasmState {
async fn run_command(
&mut self,
command: process::Command,
) -> wasmtime::Result<Result<process::Output, String>> {
maybe!(async {
self.manifest.allow_exec(&command.command, &command.args)?;
let output = util::command::new_smol_command(command.command.as_str())
.args(&command.args)
.envs(command.env)
.output()
.await?;
Ok(output.into())
})
.await
.to_wasmtime_result()
}
}
#[async_trait]
impl slash_command::Host for WasmState {}
#[async_trait]
impl context_server::Host for WasmState {}
#[async_trait]
impl dap::Host for WasmState {}
impl ExtensionImports for WasmState {
async fn get_settings(
&mut self,
location: Option<self::SettingsLocation>,
category: String,
key: Option<String>,
) -> wasmtime::Result<Result<String, String>> {
self.on_main_thread(|cx| {
async move {
let location = location
.as_ref()
.map(|location| ::settings::SettingsLocation {
worktree_id: WorktreeId::from_proto(location.worktree_id),
path: Path::new(&location.path),
});
cx.update(|cx| match category.as_str() {
"language" => {
let key = key.map(|k| LanguageName::new(&k));
let settings = AllLanguageSettings::get(location, cx).language(
location,
key.as_ref(),
cx,
);
Ok(serde_json::to_string(&settings::LanguageSettings {
tab_size: settings.tab_size,
})?)
}
"lsp" => {
let settings = key
.and_then(|key| {
ProjectSettings::get(location, cx)
.lsp
.get(&::lsp::LanguageServerName::from_proto(key))
})
.cloned()
.unwrap_or_default();
Ok(serde_json::to_string(&settings::LspSettings {
binary: settings.binary.map(|binary| settings::CommandSettings {
path: binary.path,
arguments: binary.arguments,
env: binary.env,
}),
settings: settings.settings,
initialization_options: settings.initialization_options,
})?)
}
"context_servers" => {
let configuration = key
.and_then(|key| {
ProjectSettings::get(location, cx)
.context_servers
.get(key.as_str())
})
.cloned()
.unwrap_or_default();
Ok(serde_json::to_string(&settings::ContextServerSettings {
command: configuration.command.map(|command| {
settings::CommandSettings {
path: Some(command.path),
arguments: Some(command.args),
env: command.env.map(|env| env.into_iter().collect()),
}
}),
settings: configuration.settings,
})?)
}
_ => {
bail!("Unknown settings category: {}", category);
}
})
}
.boxed_local()
})
.await?
.to_wasmtime_result()
}
async fn set_language_server_installation_status(
&mut self,
server_name: String,
status: LanguageServerInstallationStatus,
) -> wasmtime::Result<()> {
let status = match status {
LanguageServerInstallationStatus::CheckingForUpdate => BinaryStatus::CheckingForUpdate,
LanguageServerInstallationStatus::Downloading => BinaryStatus::Downloading,
LanguageServerInstallationStatus::None => BinaryStatus::None,
LanguageServerInstallationStatus::Failed(error) => BinaryStatus::Failed { error },
};
self.host
.proxy
.update_language_server_status(::lsp::LanguageServerName(server_name.into()), status);
Ok(())
}
async fn download_file(
&mut self,
url: String,
path: String,
file_type: DownloadedFileType,
) -> wasmtime::Result<Result<(), String>> {
maybe!(async {
let path = PathBuf::from(path);
let extension_work_dir = self.host.work_dir.join(self.manifest.id.as_ref());
self.host.fs.create_dir(&extension_work_dir).await?;
let destination_path = self
.host
.writeable_path_from_extension(&self.manifest.id, &path)?;
let mut response = self
.host
.http_client
.get(&url, Default::default(), true)
.await
.map_err(|err| anyhow!("error downloading release: {}", err))?;
if !response.status().is_success() {
Err(anyhow!(
"download failed with status {}",
response.status().to_string()
))?;
}
let body = BufReader::new(response.body_mut());
match file_type {
DownloadedFileType::Uncompressed => {
futures::pin_mut!(body);
self.host
.fs
.create_file_with(&destination_path, body)
.await?;
}
DownloadedFileType::Gzip => {
let body = GzipDecoder::new(body);
futures::pin_mut!(body);
self.host
.fs
.create_file_with(&destination_path, body)
.await?;
}
DownloadedFileType::GzipTar => {
let body = GzipDecoder::new(body);
futures::pin_mut!(body);
self.host
.fs
.extract_tar_file(&destination_path, Archive::new(body))
.await?;
}
DownloadedFileType::Zip => {
futures::pin_mut!(body);
node_runtime::extract_zip(&destination_path, body)
.await
.with_context(|| format!("failed to unzip {} archive", path.display()))?;
}
}
Ok(())
})
.await
.to_wasmtime_result()
}
async fn make_file_executable(&mut self, path: String) -> wasmtime::Result<Result<(), String>> {
#[allow(unused)]
let path = self
.host
.writeable_path_from_extension(&self.manifest.id, Path::new(&path))?;
#[cfg(unix)]
{
use std::fs::{self, Permissions};
use std::os::unix::fs::PermissionsExt;
return fs::set_permissions(&path, Permissions::from_mode(0o755))
.map_err(|error| anyhow!("failed to set permissions for path {path:?}: {error}"))
.to_wasmtime_result();
}
#[cfg(not(unix))]
Ok(Ok(()))
}
}

View File

@@ -158,7 +158,6 @@ impl<'a> Matcher<'a> {
if score <= 0.0 {
return 0.0;
}
let path_len = prefix.len() + path.len();
let mut cur_start = 0;
let mut byte_ix = 0;
@@ -173,8 +172,17 @@ impl<'a> Matcher<'a> {
byte_ix += ch.len_utf8();
char_ix += 1;
}
cur_start = match_char_ix + 1;
self.match_positions[i] = byte_ix;
let matched_ch = prefix
.get(match_char_ix)
.or_else(|| path.get(match_char_ix - prefix.len()))
.unwrap();
byte_ix += matched_ch.len_utf8();
cur_start = match_char_ix + 1;
char_ix = match_char_ix + 1;
}
score
@@ -209,8 +217,11 @@ impl<'a> Matcher<'a> {
let query_char = self.lowercase_query[query_idx];
let limit = self.last_positions[query_idx];
let max_valid_index = (prefix.len() + path_lowercased.len()).saturating_sub(1);
let safe_limit = limit.min(max_valid_index);
let mut last_slash = 0;
for j in path_idx..=limit {
for j in path_idx..=safe_limit {
let extra_lowercase_chars_count = extra_lowercase_chars
.iter()
.take_while(|(i, _)| i < &&j)
@@ -218,10 +229,15 @@ impl<'a> Matcher<'a> {
.sum::<usize>();
let j_regular = j - extra_lowercase_chars_count;
let path_char = if j_regular < prefix.len() {
let path_char = if j < prefix.len() {
lowercase_prefix[j]
} else {
path_lowercased[j - prefix.len()]
let path_index = j - prefix.len();
if path_index < path_lowercased.len() {
path_lowercased[path_index]
} else {
continue;
}
};
let is_path_sep = path_char == MAIN_SEPARATOR;
@@ -490,6 +506,89 @@ mod tests {
);
}
#[test]
fn match_unicode_path_entries() {
let mixed_unicode_paths = vec![
"İolu/oluş",
"İstanbul/code",
"Athens/Şanlıurfa",
"Çanakkale/scripts",
"paris/Düzce_İl",
"Berlin_Önemli_Ğündem",
"KİTAPLIK/london/dosya",
"tokyo/kyoto/fuji",
"new_york/san_francisco",
];
assert_eq!(
match_single_path_query("İo/oluş", false, &mixed_unicode_paths),
vec![("İolu/oluş", vec![0, 2, 4, 6, 8, 10, 12])]
);
assert_eq!(
match_single_path_query("İst/code", false, &mixed_unicode_paths),
vec![("İstanbul/code", vec![0, 2, 4, 6, 8, 10, 12, 14])]
);
assert_eq!(
match_single_path_query("athens/şa", false, &mixed_unicode_paths),
vec![("Athens/Şanlıurfa", vec![0, 1, 2, 3, 4, 5, 6, 7, 9])]
);
assert_eq!(
match_single_path_query("BerlinÖĞ", false, &mixed_unicode_paths),
vec![("Berlin_Önemli_Ğündem", vec![0, 1, 2, 3, 4, 5, 7, 15])]
);
assert_eq!(
match_single_path_query("tokyo/fuji", false, &mixed_unicode_paths),
vec![("tokyo/kyoto/fuji", vec![0, 1, 2, 3, 4, 5, 12, 13, 14, 15])]
);
let mixed_script_paths = vec![
"résumé_Москва",
"naïve_київ_implementation",
"café_北京_app",
"東京_über_driver",
"déjà_vu_cairo",
"seoul_piñata_game",
"voilà_istanbul_result",
];
assert_eq!(
match_single_path_query("résmé", false, &mixed_script_paths),
vec![("résumé_Москва", vec![0, 1, 3, 5, 6])]
);
assert_eq!(
match_single_path_query("café北京", false, &mixed_script_paths),
vec![("café_北京_app", vec![0, 1, 2, 3, 6, 9])]
);
assert_eq!(
match_single_path_query("ista", false, &mixed_script_paths),
vec![("voilà_istanbul_result", vec![7, 8, 9, 10])]
);
let complex_paths = vec![
"document_📚_library",
"project_👨👩👧👦_family",
"flags_🇯🇵🇺🇸🇪🇺_world",
"code_😀😃😄😁_happy",
"photo_👩👩👧👦_album",
];
assert_eq!(
match_single_path_query("doc📚lib", false, &complex_paths),
vec![("document_📚_library", vec![0, 1, 2, 9, 14, 15, 16])]
);
assert_eq!(
match_single_path_query("codehappy", false, &complex_paths),
vec![("code_😀😃😄😁_happy", vec![0, 1, 2, 3, 22, 23, 24, 25, 26])]
);
}
fn match_single_path_query<'a>(
query: &str,
smart_case: bool,

View File

@@ -306,8 +306,7 @@ impl PickerDelegate for BranchListDelegate {
cx.background_executor().clone(),
)
.await
.iter()
.cloned()
.into_iter()
.map(|candidate| BranchEntry {
branch: all_branches[candidate.candidate_id].clone(),
positions: candidate.positions,

View File

@@ -1051,8 +1051,8 @@ impl GitPanel {
repo.checkout_files(
"HEAD",
entries
.iter()
.map(|entries| entries.repo_path.clone())
.into_iter()
.map(|entries| entries.repo_path)
.collect(),
cx,
)

View File

@@ -1559,20 +1559,32 @@ impl Interactivity {
) -> Point<Pixels> {
if let Some(scroll_offset) = self.scroll_offset.as_ref() {
let mut scroll_to_bottom = false;
let mut tracked_scroll_handle = self
.tracked_scroll_handle
.as_ref()
.map(|handle| handle.0.borrow_mut());
if let Some(mut scroll_handle_state) = tracked_scroll_handle.as_deref_mut() {
scroll_handle_state.overflow = style.overflow;
scroll_to_bottom = mem::take(&mut scroll_handle_state.scroll_to_bottom);
if let Some(scroll_handle) = &self.tracked_scroll_handle {
let mut state = scroll_handle.0.borrow_mut();
state.overflow = style.overflow;
scroll_to_bottom = mem::take(&mut state.scroll_to_bottom);
}
let rem_size = window.rem_size();
let padding = style.padding.to_pixels(bounds.size.into(), rem_size);
let padding_size = size(padding.left + padding.right, padding.top + padding.bottom);
let padded_content_size = self.content_size + padding_size;
let scroll_max = (padded_content_size - bounds.size).max(&Size::default());
let padding_size = size(
style
.padding
.left
.to_pixels(bounds.size.width.into(), rem_size)
+ style
.padding
.right
.to_pixels(bounds.size.width.into(), rem_size),
style
.padding
.top
.to_pixels(bounds.size.height.into(), rem_size)
+ style
.padding
.bottom
.to_pixels(bounds.size.height.into(), rem_size),
);
let scroll_max = (self.content_size + padding_size - bounds.size).max(&Size::default());
// Clamp scroll offset in case scroll max is smaller now (e.g., if children
// were removed or the bounds became larger).
let mut scroll_offset = scroll_offset.borrow_mut();
@@ -1584,10 +1596,6 @@ impl Interactivity {
scroll_offset.y = scroll_offset.y.clamp(-scroll_max.height, px(0.));
}
if let Some(mut scroll_handle_state) = tracked_scroll_handle {
scroll_handle_state.padded_content_size = padded_content_size;
}
*scroll_offset
} else {
Point::default()
@@ -2905,7 +2913,6 @@ impl ScrollAnchor {
struct ScrollHandleState {
offset: Rc<RefCell<Point<Pixels>>>,
bounds: Bounds<Pixels>,
padded_content_size: Size<Pixels>,
child_bounds: Vec<Bounds<Pixels>>,
scroll_to_bottom: bool,
overflow: Point<Overflow>,
@@ -2968,11 +2975,6 @@ impl ScrollHandle {
self.0.borrow().child_bounds.get(ix).cloned()
}
/// Get the size of the content with padding of the container.
pub fn padded_content_size(&self) -> Size<Pixels> {
self.0.borrow().padded_content_size
}
/// scroll_to_item scrolls the minimal amount to ensure that the child is
/// fully visible
pub fn scroll_to_item(&self, ix: usize) {

View File

@@ -595,7 +595,7 @@ impl PlatformTextSystem for NoopTextSystem {
.unwrap()
.width
/ metrics.units_per_em as f32;
let mut glyphs = SmallVec::default();
let mut glyphs = Vec::new();
for (ix, c) in text.char_indices() {
if let Some(glyph) = self.glyph_for_char(FontId(0), c) {
glyphs.push(ShapedGlyph {

View File

@@ -1,7 +1,7 @@
use crate::{
Bounds, DevicePixels, Font, FontFeatures, FontId, FontMetrics, FontRun, FontStyle, FontWeight,
GlyphId, LineLayout, Pixels, PlatformTextSystem, Point, RenderGlyphParams, SUBPIXEL_VARIANTS,
ShapedGlyph, SharedString, Size, point, size,
ShapedGlyph, ShapedRun, SharedString, Size, point, size,
};
use anyhow::{Context as _, Ok, Result, anyhow};
use collections::HashMap;
@@ -38,14 +38,16 @@ struct CosmicTextSystemState {
font_system: FontSystem,
scratch: ShapeBuffer,
/// Contains all already loaded fonts, including all faces. Indexed by `FontId`.
loaded_fonts_store: Vec<Arc<CosmicTextFont>>,
/// Contains enabled font features for each loaded font.
features_store: Vec<CosmicFontFeatures>,
loaded_fonts: Vec<LoadedFont>,
/// Caches the `FontId`s associated with a specific family to avoid iterating the font database
/// for every font face in a family.
font_ids_by_family_cache: HashMap<FontKey, SmallVec<[FontId; 4]>>,
/// The name of each font associated with the given font id
postscript_names: HashMap<FontId, String>,
}
struct LoadedFont {
font: Arc<CosmicTextFont>,
features: CosmicFontFeatures,
is_known_emoji_font: bool,
}
impl CosmicTextSystem {
@@ -57,10 +59,8 @@ impl CosmicTextSystem {
font_system,
swash_cache: SwashCache::new(),
scratch: ShapeBuffer::default(),
loaded_fonts_store: Vec::new(),
features_store: Vec::new(),
loaded_fonts: Vec::new(),
font_ids_by_family_cache: HashMap::default(),
postscript_names: HashMap::default(),
}))
}
}
@@ -106,7 +106,7 @@ impl PlatformTextSystem for CosmicTextSystem {
let candidate_properties = candidates
.iter()
.map(|font_id| {
let database_id = state.loaded_fonts_store[font_id.0].id();
let database_id = state.loaded_font(*font_id).font.id();
let face_info = state.font_system.db().face(database_id).expect("");
face_info_into_properties(face_info)
})
@@ -120,7 +120,11 @@ impl PlatformTextSystem for CosmicTextSystem {
}
fn font_metrics(&self, font_id: FontId) -> FontMetrics {
let metrics = self.0.read().loaded_fonts_store[font_id.0]
let metrics = self
.0
.read()
.loaded_font(font_id)
.font
.as_swash()
.metrics(&[]);
@@ -143,9 +147,7 @@ impl PlatformTextSystem for CosmicTextSystem {
fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>> {
let lock = self.0.read();
let glyph_metrics = lock.loaded_fonts_store[font_id.0]
.as_swash()
.glyph_metrics(&[]);
let glyph_metrics = lock.loaded_font(font_id).font.as_swash().glyph_metrics(&[]);
let glyph_id = glyph_id.0 as u16;
// todo(linux): Compute this correctly
// see https://github.com/servo/font-kit/blob/master/src/loaders/freetype.rs#L614-L620
@@ -184,6 +186,10 @@ impl PlatformTextSystem for CosmicTextSystem {
}
impl CosmicTextSystemState {
fn loaded_font(&self, font_id: FontId) -> &LoadedFont {
&self.loaded_fonts[font_id.0]
}
#[profiling::function]
fn add_fonts(&mut self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
let db = self.font_system.db_mut();
@@ -200,12 +206,11 @@ impl CosmicTextSystemState {
Ok(())
}
// todo(linux) handle `FontFeatures`
#[profiling::function]
fn load_family(
&mut self,
name: &str,
_features: &FontFeatures,
features: &FontFeatures,
) -> Result<SmallVec<[FontId; 4]>> {
// TODO: Determine the proper system UI font.
let name = if name == ".SystemUIFont" {
@@ -214,7 +219,6 @@ impl CosmicTextSystemState {
name
};
let mut font_ids = SmallVec::new();
let families = self
.font_system
.db()
@@ -223,6 +227,7 @@ impl CosmicTextSystemState {
.map(|face| (face.id, face.post_script_name.clone()))
.collect::<SmallVec<[_; 4]>>();
let mut loaded_font_ids = SmallVec::new();
for (font_id, postscript_name) in families {
let font = self
.font_system
@@ -242,47 +247,28 @@ impl CosmicTextSystemState {
continue;
};
// Convert features into cosmic_text struct.
let mut font_features = CosmicFontFeatures::new();
for feature in _features.0.iter() {
let name_bytes: [u8; 4] = feature
.0
.as_bytes()
.try_into()
.map_err(|_| anyhow!("Incorrect feature flag format"))?;
let tag = cosmic_text::FeatureTag::new(&name_bytes);
font_features.set(tag, feature.1);
}
let font_id = FontId(self.loaded_fonts_store.len());
font_ids.push(font_id);
self.loaded_fonts_store.push(font);
self.features_store.push(font_features);
self.postscript_names.insert(font_id, postscript_name);
let font_id = FontId(self.loaded_fonts.len());
loaded_font_ids.push(font_id);
self.loaded_fonts.push(LoadedFont {
font,
features: features.try_into()?,
is_known_emoji_font: check_is_known_emoji_font(&postscript_name),
});
}
Ok(font_ids)
Ok(loaded_font_ids)
}
fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
let width = self.loaded_fonts_store[font_id.0]
.as_swash()
.glyph_metrics(&[])
.advance_width(glyph_id.0 as u16);
let height = self.loaded_fonts_store[font_id.0]
.as_swash()
.glyph_metrics(&[])
.advance_height(glyph_id.0 as u16);
Ok(Size { width, height })
let glyph_metrics = self.loaded_font(font_id).font.as_swash().glyph_metrics(&[]);
Ok(Size {
width: glyph_metrics.advance_width(glyph_id.0 as u16),
height: glyph_metrics.advance_height(glyph_id.0 as u16),
})
}
fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId> {
let glyph_id = self.loaded_fonts_store[font_id.0]
.as_swash()
.charmap()
.map(ch);
let glyph_id = self.loaded_font(font_id).font.as_swash().charmap().map(ch);
if glyph_id == 0 {
None
} else {
@@ -290,15 +276,11 @@ impl CosmicTextSystemState {
}
}
fn is_emoji(&self, font_id: FontId) -> bool {
// TODO: Include other common emoji fonts
self.postscript_names
.get(&font_id)
.map_or(false, |postscript_name| postscript_name == "NotoColorEmoji")
}
fn raster_bounds(&mut self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
let font = &self.loaded_fonts_store[params.font_id.0];
let font = &self.loaded_fonts[params.font_id.0].font;
let subpixel_shift = params
.subpixel_variant
.map(|v| v as f32 / (SUBPIXEL_VARIANTS as f32 * params.scale_factor));
let image = self
.swash_cache
.get_image(
@@ -307,7 +289,7 @@ impl CosmicTextSystemState {
font.id(),
params.glyph_id.0 as u16,
(params.font_size * params.scale_factor).into(),
(0.0, 0.0),
(subpixel_shift.x, subpixel_shift.y.trunc()),
cosmic_text::CacheKeyFlags::empty(),
)
.0,
@@ -330,7 +312,7 @@ impl CosmicTextSystemState {
Err(anyhow!("glyph bounds are empty"))
} else {
let bitmap_size = glyph_bounds.size;
let font = &self.loaded_fonts_store[params.font_id.0];
let font = &self.loaded_fonts[params.font_id.0].font;
let subpixel_shift = params
.subpixel_variant
.map(|v| v as f32 / (SUBPIXEL_VARIANTS as f32 * params.scale_factor));
@@ -361,27 +343,31 @@ impl CosmicTextSystemState {
}
}
/// This is used when cosmic_text has chosen a fallback font instead of using the requested
/// font, typically to handle some unicode characters. When this happens, `loaded_fonts` may not
/// yet have an entry for this fallback font, and so one is added.
///
/// Note that callers shouldn't use this `FontId` somewhere that will retrieve the corresponding
/// `LoadedFont.features`, as it will have an arbitrarily chosen or empty value. The only
/// current use of this field is for the *input* of `layout_line`, and so it's fine to use
/// `font_id_for_cosmic_id` when computing the *output* of `layout_line`.
fn font_id_for_cosmic_id(&mut self, id: cosmic_text::fontdb::ID) -> FontId {
if let Some(ix) = self
.loaded_fonts_store
.loaded_fonts
.iter()
.position(|font| font.id() == id)
.position(|loaded_font| loaded_font.font.id() == id)
{
FontId(ix)
} else {
// This matches the behavior of the mac text system
let font = self.font_system.get_font(id).unwrap();
let face = self
.font_system
.db()
.faces()
.find(|info| info.id == id)
.unwrap();
let face = self.font_system.db().face(id).unwrap();
let font_id = FontId(self.loaded_fonts_store.len());
self.loaded_fonts_store.push(font);
self.postscript_names
.insert(font_id, face.post_script_name.clone());
let font_id = FontId(self.loaded_fonts.len());
self.loaded_fonts.push(LoadedFont {
font,
features: CosmicFontFeatures::new(),
is_known_emoji_font: check_is_known_emoji_font(&face.post_script_name),
});
font_id
}
@@ -392,62 +378,74 @@ impl CosmicTextSystemState {
let mut attrs_list = AttrsList::new(&Attrs::new());
let mut offs = 0;
for run in font_runs {
let font = &self.loaded_fonts_store[run.font_id.0];
let font = self.font_system.db().face(font.id()).unwrap();
let features = self.features_store[run.font_id.0].clone();
let loaded_font = self.loaded_font(run.font_id);
let font = self.font_system.db().face(loaded_font.font.id()).unwrap();
attrs_list.add_span(
offs..(offs + run.len),
&Attrs::new()
.metadata(run.font_id.0)
.family(Family::Name(&font.families.first().unwrap().0))
.stretch(font.stretch)
.style(font.style)
.weight(font.weight)
.font_features(features),
.font_features(loaded_font.features.clone()),
);
offs += run.len;
}
let mut line = ShapeLine::new(
let line = ShapeLine::new(
&mut self.font_system,
text,
&attrs_list,
cosmic_text::Shaping::Advanced,
4,
);
let mut layout = Vec::with_capacity(1);
let mut layout_lines = Vec::with_capacity(1);
line.layout_to_buffer(
&mut self.scratch,
font_size.0,
None, // We do our own wrapping
cosmic_text::Wrap::None,
None,
&mut layout,
&mut layout_lines,
None,
);
let layout = layout_lines.first().unwrap();
let mut runs = Vec::new();
let layout = layout.first().unwrap();
let mut runs: Vec<ShapedRun> = Vec::new();
for glyph in &layout.glyphs {
let font_id = glyph.font_id;
let font_id = self.font_id_for_cosmic_id(font_id);
let is_emoji = self.is_emoji(font_id);
let mut glyphs = SmallVec::new();
let mut font_id = FontId(glyph.metadata);
let mut loaded_font = self.loaded_font(font_id);
if loaded_font.font.id() != glyph.font_id {
font_id = self.font_id_for_cosmic_id(glyph.font_id);
loaded_font = self.loaded_font(font_id);
}
let is_emoji = loaded_font.is_known_emoji_font;
// HACK: Prevent crash caused by variation selectors.
if glyph.glyph_id == 3 && is_emoji {
continue;
}
// todo(linux) this is definitely wrong, each glyph in glyphs from cosmic-text is a cluster with one glyph, ShapedRun takes a run of glyphs with the same font and direction
glyphs.push(ShapedGlyph {
let shaped_glyph = ShapedGlyph {
id: GlyphId(glyph.glyph_id as u32),
position: point(glyph.x.into(), glyph.y.into()),
index: glyph.start,
is_emoji,
});
};
runs.push(crate::ShapedRun { font_id, glyphs });
if let Some(last_run) = runs
.last_mut()
.filter(|last_run| last_run.font_id == font_id)
{
last_run.glyphs.push(shaped_glyph);
} else {
runs.push(ShapedRun {
font_id,
glyphs: vec![shaped_glyph],
});
}
}
LineLayout {
@@ -461,6 +459,26 @@ impl CosmicTextSystemState {
}
}
impl TryFrom<&FontFeatures> for CosmicFontFeatures {
type Error = anyhow::Error;
fn try_from(features: &FontFeatures) -> Result<Self> {
let mut result = CosmicFontFeatures::new();
for feature in features.0.iter() {
let name_bytes: [u8; 4] = feature
.0
.as_bytes()
.try_into()
.map_err(|_| anyhow!("Incorrect feature flag format"))?;
let tag = cosmic_text::FeatureTag::new(&name_bytes);
result.set(tag, feature.1);
}
Ok(result)
}
}
impl From<RectF> for Bounds<f32> {
fn from(rect: RectF) -> Self {
Bounds {
@@ -558,3 +576,8 @@ fn face_info_into_properties(
},
}
}
fn check_is_known_emoji_font(postscript_name: &str) -> bool {
// TODO: Include other common emoji fonts
postscript_name == "NotoColorEmoji"
}

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