Compare commits

...

61 Commits

Author SHA1 Message Date
Nate Butler
1d62f5a58c Theme chaos!
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-07-10 12:51:48 -04:00
Nate Butler
bef7ba8e35 Remove unneded ThemeColorField
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-07-10 11:59:43 -04:00
Danilo Leal
ad8b823555 Improve the LSP popover menu design (#34081)
- Add a slightly different bolt icon SVG so it sits better when with an
indicator
- Attempt to clarify what happens when clicking any of the menu items
- Add descriptions to the tooltips to clarify what each indicator color
means
- Add section titles to clarify in which category each menu item is
sitting on

Release Notes:

- N/A
2025-07-08 19:51:24 -03:00
Dino
139af02737 vim: Fix and improve horizontal scrolling (#33590)
This Pull Request introduces various changes to the editor's horizontal
scrolling, mostly focused on vim mode's horizontal scroll motions (`z
l`, `z h`, `z shift-l`, `z shift-h`). In order to make it easier to
review, the logical changes have been split into different sections.

## Cursor Position Update

Changes introduced on https://github.com/zed-industries/zed/pull/32558
added both `z l` and `z h` to vim mode but it only scrolled the editor's
content, without changing the cursor position. This doesn't reflect the
actual behavior of those motions in vim, so these two commits tackled
that, ensuring that the cursor position is updated, only when the cursor
is on the left or right edges of the editor:

-
ea3b866a76
-
805f41a913

## Horizontal Autoscroll Fix

After introducing the cursor position update to both `z l` and `z h` it
was noted that there was a bug with using `z l`, followed by `0` and
then `z l` again, as on the second use `z l` the cursor would not be
updated. This would only happen on the first line in the editor, and it
was concluded that it was because the
`editor::scroll::autoscroll::Editor.autoscroll_horizontally` method was
directly updating the scroll manager's anchor offset, instead of using
the `editor::scroll::Editor.set_scroll_position_internal` method, like
is being done by the vertical autoscroll
(`editor::scroll::autoscroll::Editor.autoscroll_vertically`).

This wouldn't update the scroll manager's anchor, which would still
think it was at `(0, 1)` so the cursor position would not be updated.
The changes in [this
commit](3957f02e18)
updated the horizontal autoscrolling method to also leverage
`set_scroll_position_internal`.

## Visible Column Count & Page Width Scroll Amount

The changes in
d83652c3ae
add a `visible_column_count` field to `editor::scroll::ScrollManager`
struct, which allowed the introduction of the `ScrollAmount::PageWidth`
enum.

With these changes, two new actions are introduced,
`vim::normal::scroll::HalfPageRight` and
`vim::normal::scroll::HalfPageLeft` (in
7f344304d5),
which move the editor half page to the right and half page to the left,
as well as the cursor position, which have also been mapped to `z
shift-l` and `z shift-h`, respectively.

Closes #17219 

Release Notes:

- Improved `z l` and `z h` to actually move the cursor position, similar
to vim's behavior
- Added `z shift-l` and `z shift-h` to scroll half of the page width's
to the right or to the left, respectively

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-07-08 14:48:48 -06:00
Ben Kunkle
6b7c30d7ad keymap_ui: Editor for action input in modal (#34080)
Closes #ISSUE

Adds a very simple editor for editing action input to the edit keybind
modal. No auto-complete yet.

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-07-08 14:39:55 -05:00
Marshall Bowers
1220049089 Add feature flag to use cloud.zed.dev instead of llm.zed.dev (#34076)
This PR adds a new `zed-cloud` feature flag that can be used to send
traffic to `cloud.zed.dev` instead of `llm.zed.dev`.

This is just so Zed staff can test the new infrastructure. When we're
ready for prime-time we'll reroute traffic on the server.

Release Notes:

- N/A
2025-07-08 18:44:51 +00:00
Julia Ryan
01bdef130b nix: Fix generate-licenses failure (#34072)
We should maybe add `generate-licenses` to the sensitivity list for
running nix in CI given that nix uses a workaround for it.

Release Notes:

- N/A
2025-07-08 18:05:11 +00:00
Peter Tripp
9a3720edd3 Disable word completions by default for plaintext/markdown (#34065)
This disables word based completions in Plain Text and Markdown buffers
by default.

Word-based completion when typing natural language can be quite
disruptive, in particular at the end of a line (e.g. markdown style
lists) where `enter` will accept word completions rather than insert a
newline (see screenshot). I think the default, empty buffer experience
in Zed should be closer to a zed-mode experience -- just an editor
getting out of your way to let you type and not having to mash
escape/cmd-z repeatedly to undo a over-aggressive completion.

<img width="265" alt="Screenshot 2025-07-08 at 11 57 26"
src="https://github.com/user-attachments/assets/131f73a8-4687-45bf-ad53-f611c0af9387"
/>

- Context:
https://github.com/zed-industries/zed/issues/4957#issuecomment-3049513501
- Follow-up to: https://github.com/zed-industries/zed/pull/26410

Re-enable the existing behavior with:
```json
  "languages": {
    "Plain Text": { "completions": { "words": "fallback" } },
    "Markdown": { "completions": { "words": "fallback" } },
  },
```
Or disable Word based completions everywhere with:
```json
  "completions": { 
    "words": "fallback"
  },
```

Release Notes:

- Disable word-completions by default in Plain Text and Markdown Buffers
2025-07-08 17:49:54 +00:00
fantacell
11ddecb995 helix: Change keymap (#33925)
Might close #33838 for now

Keymaps that work both in vim and helix, but only in normal mode, not
the more general `VimControl` context are written separately. This makes
the file shorter by combining them and also adds one more keymap.

Release Notes:

- N/A
2025-07-08 11:47:39 -06:00
Peter Tripp
684e14e55b Move Perplexity extension to dedicated repository (#34070)
Move Perplexity extension to:
https://github.com/zed-extensions/perplexity

Release Notes:

- N/A
2025-07-08 13:40:00 -04:00
Gwen Lg
263080c4c4 Allow local build of remote_server dev to be deployed to different linux than local (#33395)
setup local build of `remote_server` to not depend of the local linux
libraries by :
- enable `vendored-libgit2` feature of git2
- setup target triple to `unknown-linux-musl` (mirror bundle-linux
script)
- add flag ` -C target-feature=+crt-static` in `RUSTFLAGS` env var
(mirror bundle-linux script)

Bonus:
Add an option to setup mold as linker of local build.

Closes #33341

Release Notes:

 - N/A
2025-07-08 16:31:20 +00:00
Alisina Bahadori
925464cfc6 Improve terminal rendering performance (#33345)
Closes #18263

Improvements:

• **Batch text rendering** - Combine adjacent cells with identical
styling into single text runs to reduce draw calls
• **Throttle hyperlink searches** - Limit hyperlink detection to every
100ms or when mouse moves >5px to reduce CPU usage
• **Pre-allocate collections** - Use `Vec::with_capacity()` for cells,
runs, and regions to minimize reallocations
• **Optimize background regions** - Merge adjacent background rectangles
to reduce number of draw operations
• **Cache selection text** - Only compute terminal selection string when
selection exists

Release Notes:

- Improved terminal rendering performance.

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-07-08 09:05:01 -06:00
Anthony Eid
bcac748c2b Add support for Nushell in shell builder (#33806)
We also swap out env variables before sending them to shells now in the
task system. This fixed issues Fish and Nushell had where an empty
argument could be sent into a command when no argument should be sent.
This only happened from task's generated by Zed.

Closes #31297 #31240

Release Notes:

- Fix bug where spawning a Zed generated task or debug session with Fish
or Nushell failed
2025-07-08 14:57:37 +00:00
张小白
0ca0914cca windows: Add support for SSH (#29145)
Closes #19892

This PR builds on top of #20587 and improves upon it.

Release Notes:

- N/A

---------

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2025-07-08 14:34:57 +00:00
Danilo Leal
8bd739d869 git panel: Add some design refinements (#34064)
Things like borders, border colors, which icons are being used, button
sizes, and spacing. There is more to do here: polish that we're using a
bunch of divs for spacing, arbitrary pixel values for tokens we have in
the system, etc. This is just a quick pass!

Release Notes:

- git panel: Polished the panel spacing, border colors, and icons.
2025-07-08 11:23:36 -03:00
Joseph T. Lyons
60e9ab8f93 Delete access tokens on user delete (#34036)
Trying to delete a user record from our admin panel throws the following
error:

`update or delete on table "users" violates foreign key constraint
"access_tokens_user_id_fkey" on table "access_tokens"
Detail: Key (id)=(....) is still referenced from table "access_tokens".`

We need to add a cascade delete to the `access_tokens` table.

Release Notes:

- N/A
2025-07-08 10:08:17 -04:00
Danilo Leal
3327f90e0f agent: Add setting to control terminal card expanded state (#34061)
Similar to https://github.com/zed-industries/zed/pull/34040, this PR
allows to control via settings whether the terminal card in the agent
panel should be expanded. It is set to true by default.

Release Notes:

- agent: Added a setting to control whether terminal cards are expanded
in the agent panel, thus showing or hiding the full command output.
2025-07-08 10:43:35 -03:00
curiouslad
5e15c05a9d editor: Fix diagnostic popovers not being scrollable (#33581)
Closes #32673

Release Notes:

- Fixed diagnostic popovers not being scrollable
2025-07-08 16:14:22 +03:00
Smit Barmase
1f3575ad6e project_panel: Only show sticky item shadow when list is scrolled (#34050)
Follow up: https://github.com/zed-industries/zed/pull/34042

- Removes `top_slot_items` from `uniform_list` in favor of using
existing `decorations`
- Add condition to only show shadow for sticky item when list is
scrolled and scrollable

Release Notes:

- N/A
2025-07-08 14:22:24 +05:30
Danilo Leal
f1db3b4e1d agent: Add setting to control edit card expanded state (#34040)
This PR adds the `expand_edit_card` setting, which controls whether edit
cards in the agent panel are expanded, thus showing or not the full diff
of a given file's AI-driven change. I personally prefer to have these
cards collapsed by default as I am mostly reviewing diffs using either
the review multibuffer or the diffs within the file's buffer itself.
Didn't want to change the default behavior as that was intentionally
chosen, so here we are! :)

Open to feedback about the setting name; I've iterated between a few
options and don't necessarily feel like the current one is the best.

Release Notes:

- agent: Added a setting to control whether edit cards are expanded in
the agent panel, thus showing or hiding the full diff of a file's
changes.
2025-07-08 01:19:09 -03:00
Danilo Leal
02d0e725a8 agent: Allow clicking on the whole edit file row to trigger review (#34041)
Just a small quality-of-life type of PR that makes clicking anywhere
until the "Review" button trigger the action that that button triggers
(i.e., opens the review multibuffer).

<img
src="https://github.com/user-attachments/assets/8936ed75-50fd-49f9-89ef-b6c4301a8eba"
width="600" />

Release Notes:

- agent: Added the ability to click the whole file row in the edits bar
to trigger the review multibuffer.
2025-07-08 01:19:00 -03:00
Danilo Leal
211d6205b9 project panel: Add a shadow in the last sticky item (#34042)
Follow-up to https://github.com/zed-industries/zed/pull/33994. This PR
adds a subtle shadow—built from an absolute-positioned div, due to
layering of items—to the last sticky item in the project panel when that
setting is turned on. This helps understand the block of items that is
currently sticky.

<img
src="https://github.com/user-attachments/assets/0e030e93-9bc6-42ff-8d0d-3e46f1986152"
width="300"/>

Would love to add indent guides to the items that are sticky as a next
step.

Release Notes:

- project panel: When `sticky_scroll` is true, the last item will now
have a subtle shadow to help visualizing the block of items that are
currently sticky.
2025-07-08 01:18:52 -03:00
Piotr Osiewicz
4693f16759 debugger: Remove PHP debug adapter (#34020)
This commit removes the PHP debug adapter in favor of a new version
(0.3.0) of PHP extension.
The name of a debug adapter has been changed from "PHP" to "Xdebug",
which makes this a breaking change in user-configured scenarios

Release Notes:

- debugger: PHP debug adapter is no longer shipped in core Zed editor;
it is now available in PHP extension (starting with version 0.3.0). The
adapter has been renamed from `PHP` to `Xdebug`, which might break your
user-defined debug scenarios.
2025-07-08 01:36:32 +02:00
Piotr Osiewicz
c0dc758f24 debugger: Rely on LocalDapCommand in more places (#34035)
- **debugger: Move cacheable property onto LocalDapCommand**
- **debugger/session: Relax method bounds to use LocalDapCommand**


Release Notes:

- N/A
2025-07-07 23:05:37 +00:00
Richard Feldman
9b7632d5f6 Automatically adjust ANSI color contrast (#34033)
Closes #33253 in a way that doesn't regress #32175 - namely,
automatically adjusts the contrast between the foreground and background
text in the terminal such that it's above a certain threshold. The
threshold is configurable in settings, and can be set to 0 to turn off
this feature and use exactly the colors the theme specifies even if they
are illegible.

## One Light Theme Before
<img width="220" alt="Screenshot 2025-07-07 at 6 00 47 PM"
src="https://github.com/user-attachments/assets/096754a6-f79f-4fea-a86e-cb7b8ff45d60"
/>

(Last row is highlighted because otherwise the text is unreadable; the
foreground and background are the same color.)

## One Light Theme After

(This is with the new default contrast adjustment setting.)

<img width="215" alt="Screenshot 2025-07-07 at 6 22 02 PM"
src="https://github.com/user-attachments/assets/b082fefe-76f5-4231-b704-ff387983a3cb"
/>

This approach was inspired by @mitchellh's use of automatic contrast
adjustment in [Ghostty](https://ghostty.org/) - thanks, Mitchell! The
main difference is that we're using APCA's formula instead of WCAG for
[these
reasons](https://khan-tw.medium.com/wcag2-are-you-still-using-it-ui-contrast-visibility-standard-readability-contrast-f34eb73e89ee).

Release Notes:

- Added automatic dynamic contrast adjustment for terminal foreground
and background colors
2025-07-07 22:39:11 +00:00
Ben Kunkle
877ef5e1b1 keymap_ui: Add auto-complete for context in keybind editor (#34031)
Closes #ISSUE

Implements a very basic completion provider that is attached to the
context editor in the keybind editing modal.

The context identifiers used for completions are scraped from the
default, vim, and base keymaps on demand.

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-07-07 21:54:51 +00:00
Bennet Bo Fenner
66a1c356bf agent: Fix max token count mismatch when not using burn mode (#34025)
Closes #31854

Release Notes:

- agent: Fixed an issue where the maximum token count would be displayed
incorrectly when burn mode was not being used.
2025-07-07 23:13:24 +02:00
Julia Ryan
a9107dfaeb Edit debug tasks (#32908)
Release Notes:

- Added the ability to edit LSP provided debug tasks

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-07-07 14:04:21 -07:00
Oleksiy Syvokon
d549993c73 tools: Send stale file notifications only once (#34026)
Previously, we sent notifications repeatedly until the agent read a
file, which was often inefficient. With this change, we now send a
notification only once (unless the files are modified again, in which
case we'll send another notification).

Release Notes:

- N/A
2025-07-07 22:30:01 +03:00
Cole Miller
e0c860c42a debugger: Fix issues with restarting sessions (#33932)
Restarting sessions was broken in #33273 when we moved away from calling
`kill` in the shutdown sequence. This PR re-adds that `kill` call so
that old debug adapter processes will be cleaned up when sessions are
restarted within Zed. This doesn't re-introduce the issue that motivated
the original changes to the shutdown sequence, because we still send
Disconnect/Terminate to debug adapters when quitting Zed without killing
the process directly.

We also now remove manually-restarted sessions eagerly from the session
list.

Closes #33916 

Release Notes:

- debugger: Fixed not being able to restart sessions for Debugpy and
other adapters that communicate over TCP.
- debugger: Fixed debug adapter processes not being cleaned up.

---------

Co-authored-by: Remco Smits <djsmits12@gmail.com>
2025-07-07 18:28:59 +00:00
Peter Tripp
2a6ef006f4 Make script/generate-license fail on WARN too (#34008)
Current main shows this on `script/generate-licenses`:
```
[WARN] failed to validate all files specified in clarification for crate ring 0.17.14: checksum mismatch, expected '76b39f9b371688eac9d8323f96ee80b3aef5ecbc2217f25377bd4e4a615296a9'
```

Ring fixed it's licenses ambiguity upstream. This warning was
identifying that their license text (multiple licenses concatenated) had
changed (sha mismatch) and thus our license clarification was invalid.

Tested the script to confirm this [now
fails](https://github.com/zed-industries/zed/actions/runs/16118890720/job/45479355992?pr=34008)
under CI and then removed the ring clarification because it is no longer
required and now passes.

Release Notes:

- N/A
2025-07-07 13:56:53 -04:00
Smit Barmase
38febed02d languages: Fix detents case line after typing : in Python (#34017)
Closes #34002

`decrease_indent_patterns` should only contain mapping which are at same
indent level with each other, which is not true for `match` and `case`
mapping.

Caused in https://github.com/zed-industries/zed/pull/33370

Release Notes:

- N/A
2025-07-07 22:41:29 +05:30
Alex Povel
8cc3b094d2 editor: Add action to sort lines by length (#33622)
This change introduces a new `Action` implementation to sort lines by
their `char`
length. It reuses the same calculation as used for getting the caret
column position,
i.e. `TextSummary`. The motivation is to e.g. handle source code where
this sort of
order matters
([example](fdf537c3d3/tests/readme.rs (L529-L535))).

Tested manually via `cargo build && ./target/debug/zed .`: the new
action shows up in the command palette, and testing it on `.mailmap`
entries turns those from

```text
Agus Zubiaga <agus@zed.dev>
Agus Zubiaga <agus@zed.dev> <hi@aguz.me>
Alex Viscreanu <alexviscreanu@gmail.com>
Alex Viscreanu <alexviscreanu@gmail.com> <alexandru.viscreanu@kiwi.com>
Alexander Mankuta <alex@pointless.one>
Alexander Mankuta <alex@pointless.one> <alex+github@pointless.one>
amtoaer <amtoaer@gmail.com>
amtoaer <amtoaer@gmail.com> <amtoaer@outlook.com>
Andrei Zvonimir Crnković <andrei@0x7f.dev>
Andrei Zvonimir Crnković <andrei@0x7f.dev> <andreicek@0x7f.dev>
Angelk90 <angelo.k90@hotmail.it>
Angelk90 <angelo.k90@hotmail.it> <20476002+Angelk90@users.noreply.github.com>
Antonio Scandurra <me@as-cii.com>
Antonio Scandurra <me@as-cii.com> <antonio@zed.dev>
Ben Kunkle <ben@zed.dev>
Ben Kunkle <ben@zed.dev> <ben.kunkle@gmail.com>
Bennet Bo Fenner <bennet@zed.dev>
Bennet Bo Fenner <bennet@zed.dev> <53836821+bennetbo@users.noreply.github.com>
Bennet Bo Fenner <bennet@zed.dev> <bennetbo@gmx.de>
Boris Cherny <boris@anthropic.com>
Boris Cherny <boris@anthropic.com> <boris@performancejs.com>
Brian Tan <brian.tan88@gmail.com>
Chris Hayes <chris+git@hayes.software>
Christian Bergschneider <christian.bergschneider@gmx.de>
Christian Bergschneider <christian.bergschneider@gmx.de> <magiclake@gmx.de>
Conrad Irwin <conrad@zed.dev>
Conrad Irwin <conrad@zed.dev> <conrad.irwin@gmail.com>
Dairon Medina <dairon.medina@gmail.com>
Danilo Leal <danilo@zed.dev>
Danilo Leal <danilo@zed.dev> <67129314+danilo-leal@users.noreply.github.com>
Edwin Aronsson <75266237+4teapo@users.noreply.github.com>
Elvis Pranskevichus <elvis@geldata.com>
Elvis Pranskevichus <elvis@geldata.com> <elvis@magic.io>
Evren Sen <nervenes@icloud.com>
Evren Sen <nervenes@icloud.com> <146845123+evrensen467@users.noreply.github.com>
Evren Sen <nervenes@icloud.com> <146845123+evrsen@users.noreply.github.com>
Fernando Tagawa <tagawafernando@gmail.com>
Fernando Tagawa <tagawafernando@gmail.com> <fernando.tagawa.gamail.com@gmail.com>
Finn Evers <dev@bahn.sh>
Finn Evers <dev@bahn.sh> <75036051+MrSubidubi@users.noreply.github.com>
Finn Evers <dev@bahn.sh> <finn.evers@outlook.de>
Gowtham K <73059450+dovakin0007@users.noreply.github.com>
Greg Morenz <greg-morenz@droid.cafe>
Greg Morenz <greg-morenz@droid.cafe> <morenzg@gmail.com>
Ihnat Aŭtuška <autushka.ihnat@gmail.com>
Ivan Žužak <izuzak@gmail.com>
Ivan Žužak <izuzak@gmail.com> <ivan.zuzak@github.com>
Joseph T. Lyons <JosephTLyons@gmail.com>
Joseph T. Lyons <JosephTLyons@gmail.com> <JosephTLyons@users.noreply.github.com>
Julia <floc@unpromptedtirade.com>
Julia <floc@unpromptedtirade.com> <30666851+ForLoveOfCats@users.noreply.github.com>
Kaylee Simmons <kay@the-simmons.net>
Kaylee Simmons <kay@the-simmons.net> <kay@zed.dev>
Kaylee Simmons <kay@the-simmons.net> <keith@the-simmons.net>
Kaylee Simmons <kay@the-simmons.net> <keith@zed.dev>
Kirill Bulatov <kirill@zed.dev>
Kirill Bulatov <kirill@zed.dev> <mail4score@gmail.com>
Kyle Caverly <kylebcaverly@gmail.com>
Kyle Caverly <kylebcaverly@gmail.com> <kyle@zed.dev>
Lilith Iris <itslirissama@gmail.com>
Lilith Iris <itslirissama@gmail.com> <83819417+Irilith@users.noreply.github.com>
LoganDark <contact@logandark.mozmail.com>
LoganDark <contact@logandark.mozmail.com> <git@logandark.mozmail.com>
LoganDark <contact@logandark.mozmail.com> <github@logandark.mozmail.com>
Marko Kungla <marko.kungla@gmail.com>
Marko Kungla <marko.kungla@gmail.com> <marko@mkungla.dev>
Marshall Bowers <git@maxdeviant.com>
Marshall Bowers <git@maxdeviant.com> <elliott.codes@gmail.com>
Marshall Bowers <git@maxdeviant.com> <marshall@zed.dev>
Matt Fellenz <matt@felle.nz>
Matt Fellenz <matt@felle.nz> <matt+github@felle.nz>
Max Brunsfeld <maxbrunsfeld@gmail.com>
Max Brunsfeld <maxbrunsfeld@gmail.com> <max@zed.dev>
Max Linke <maxlinke88@gmail.com>
Max Linke <maxlinke88@gmail.com> <kain88-de@users.noreply.github.com>
Michael Sloan <michael@zed.dev>
Michael Sloan <michael@zed.dev> <mgsloan@gmail.com>
Michael Sloan <michael@zed.dev> <mgsloan@google.com>
Mikayla Maki <mikayla@zed.dev>
Mikayla Maki <mikayla@zed.dev> <mikayla.c.maki@gmail.com>
Mikayla Maki <mikayla@zed.dev> <mikayla.c.maki@icloud.com>
Morgan Krey <morgan@zed.dev>
Muhammad Talal Anwar <mail@talal.io>
Muhammad Talal Anwar <mail@talal.io> <talalanwar@outlook.com>
Nate Butler <iamnbutler@gmail.com>
Nate Butler <iamnbutler@gmail.com> <nate@zed.dev>
Nathan Sobo <nathan@zed.dev>
Nathan Sobo <nathan@zed.dev> <nathan@warp.dev>
Nathan Sobo <nathan@zed.dev> <nathansobo@gmail.com>
Nigel Jose <nigelmjose@gmail.com>
Nigel Jose <nigelmjose@gmail.com> <nigel.jose@student.manchester.ac.uk>
Peter Tripp <peter@zed.dev>
Peter Tripp <peter@zed.dev> <petertripp@gmail.com>
Petros Amoiridis <petros@hey.com>
Petros Amoiridis <petros@hey.com> <petros@zed.dev>
Piotr Osiewicz <piotr@zed.dev>
Piotr Osiewicz <piotr@zed.dev> <24362066+osiewicz@users.noreply.github.com>
Pocæus <github@pocaeus.com>
Pocæus <github@pocaeus.com> <pseudomata@proton.me>
Rashid Almheiri <r.muhairi@pm.me>
Rashid Almheiri <r.muhairi@pm.me> <69181766+huwaireb@users.noreply.github.com>
Richard Feldman <oss@rtfeldman.com>
Richard Feldman <oss@rtfeldman.com> <richard@zed.dev>
Robert Clover <git@clo4.net>
Robert Clover <git@clo4.net> <robert@clover.gdn>
Roy Williams <roy.williams.iii@gmail.com>
Roy Williams <roy.williams.iii@gmail.com> <roy@anthropic.com>
Sebastijan Kelnerič <sebastijan.kelneric@sebba.dev>
Sebastijan Kelnerič <sebastijan.kelneric@sebba.dev> <sebastijan.kelneric@vichava.com>
Sergey Onufrienko <sergey@onufrienko.com>
Shish <webmaster@shishnet.org>
Shish <webmaster@shishnet.org> <shish@shishnet.org>
Smit Barmase <0xtimsb@gmail.com>
Smit Barmase <0xtimsb@gmail.com> <smit@zed.dev>
Thomas <github.thomaub@gmail.com>
Thomas <github.thomaub@gmail.com> <thomas.aubry94@gmail.com>
Thomas <github.thomaub@gmail.com> <thomas.aubry@paylead.fr>
Thomas Heartman <thomasheartman+github@gmail.com>
Thomas Heartman <thomasheartman+github@gmail.com> <thomas@getunleash.io>
Thomas Mickley-Doyle <tmickleydoyle@gmail.com>
Thomas Mickley-Doyle <tmickleydoyle@gmail.com> <thomas@zed.dev>
Thorben Kröger <dev@thorben.net>
Thorben Kröger <dev@thorben.net> <thorben.kroeger@hexagon.com>
Thorsten Ball <mrnugget@gmail.com>
Thorsten Ball <mrnugget@gmail.com> <me@thorstenball.com>
Thorsten Ball <mrnugget@gmail.com> <thorsten@zed.dev>
Tristan Hume <tris.hume@gmail.com>
Tristan Hume <tris.hume@gmail.com> <tristan@anthropic.com>
Uladzislau Kaminski <i@uladkaminski.com>
Uladzislau Kaminski <i@uladkaminski.com> <uladzislau_kaminski@epam.com>
Vitaly Slobodin <vitaliy.slobodin@gmail.com>
Vitaly Slobodin <vitaliy.slobodin@gmail.com> <vitaly_slobodin@fastmail.com>
Will Bradley <williambbradley@gmail.com>
Will Bradley <williambbradley@gmail.com> <will@zed.dev>
WindSoilder <WindSoilder@outlook.com>
张小白 <364772080@qq.com>
````

into

```text
张小白 <364772080@qq.com>
Ben Kunkle <ben@zed.dev>
Finn Evers <dev@bahn.sh>
Agus Zubiaga <agus@zed.dev>
amtoaer <amtoaer@gmail.com>
Peter Tripp <peter@zed.dev>
Pocæus <github@pocaeus.com>
Danilo Leal <danilo@zed.dev>
Matt Fellenz <matt@felle.nz>
Morgan Krey <morgan@zed.dev>
Nathan Sobo <nathan@zed.dev>
Robert Clover <git@clo4.net>
Conrad Irwin <conrad@zed.dev>
Ivan Žužak <izuzak@gmail.com>
Mikayla Maki <mikayla@zed.dev>
Piotr Osiewicz <piotr@zed.dev>
Shish <webmaster@shishnet.org>
Evren Sen <nervenes@icloud.com>
Kirill Bulatov <kirill@zed.dev>
Michael Sloan <michael@zed.dev>
Angelk90 <angelo.k90@hotmail.it>
Max Linke <maxlinke88@gmail.com>
Smit Barmase <0xtimsb@gmail.com>
Thorben Kröger <dev@thorben.net>
Antonio Scandurra <me@as-cii.com>
Bennet Bo Fenner <bennet@zed.dev>
Brian Tan <brian.tan88@gmail.com>
Julia <floc@unpromptedtirade.com>
Nigel Jose <nigelmjose@gmail.com>
Petros Amoiridis <petros@hey.com>
Rashid Almheiri <r.muhairi@pm.me>
Thomas <github.thomaub@gmail.com>
Boris Cherny <boris@anthropic.com>
Nate Butler <iamnbutler@gmail.com>
Thorsten Ball <mrnugget@gmail.com>
Tristan Hume <tris.hume@gmail.com>
Richard Feldman <oss@rtfeldman.com>
Greg Morenz <greg-morenz@droid.cafe>
Kaylee Simmons <kay@the-simmons.net>
Lilith Iris <itslirissama@gmail.com>
Marshall Bowers <git@maxdeviant.com>
Muhammad Talal Anwar <mail@talal.io>
Kyle Caverly <kylebcaverly@gmail.com>
Marko Kungla <marko.kungla@gmail.com>
WindSoilder <WindSoilder@outlook.com>
Alexander Mankuta <alex@pointless.one>
Chris Hayes <chris+git@hayes.software>
Max Brunsfeld <maxbrunsfeld@gmail.com>
Dairon Medina <dairon.medina@gmail.com>
Elvis Pranskevichus <elvis@geldata.com>
Agus Zubiaga <agus@zed.dev> <hi@aguz.me>
Alex Viscreanu <alexviscreanu@gmail.com>
Ihnat Aŭtuška <autushka.ihnat@gmail.com>
Joseph T. Lyons <JosephTLyons@gmail.com>
Uladzislau Kaminski <i@uladkaminski.com>
Will Bradley <williambbradley@gmail.com>
LoganDark <contact@logandark.mozmail.com>
Roy Williams <roy.williams.iii@gmail.com>
Sergey Onufrienko <sergey@onufrienko.com>
Andrei Zvonimir Crnković <andrei@0x7f.dev>
Fernando Tagawa <tagawafernando@gmail.com>
Vitaly Slobodin <vitaliy.slobodin@gmail.com>
Nathan Sobo <nathan@zed.dev> <nathan@warp.dev>
Thomas Mickley-Doyle <tmickleydoyle@gmail.com>
Ben Kunkle <ben@zed.dev> <ben.kunkle@gmail.com>
Smit Barmase <0xtimsb@gmail.com> <smit@zed.dev>
Finn Evers <dev@bahn.sh> <finn.evers@outlook.de>
Robert Clover <git@clo4.net> <robert@clover.gdn>
amtoaer <amtoaer@gmail.com> <amtoaer@outlook.com>
Nate Butler <iamnbutler@gmail.com> <nate@zed.dev>
Thomas Heartman <thomasheartman+github@gmail.com>
Kaylee Simmons <kay@the-simmons.net> <kay@zed.dev>
Peter Tripp <peter@zed.dev> <petertripp@gmail.com>
Petros Amoiridis <petros@hey.com> <petros@zed.dev>
Pocæus <github@pocaeus.com> <pseudomata@proton.me>
Antonio Scandurra <me@as-cii.com> <antonio@zed.dev>
Bennet Bo Fenner <bennet@zed.dev> <bennetbo@gmx.de>
Matt Fellenz <matt@felle.nz> <matt+github@felle.nz>
Michael Sloan <michael@zed.dev> <mgsloan@gmail.com>
Nathan Sobo <nathan@zed.dev> <nathansobo@gmail.com>
Sebastijan Kelnerič <sebastijan.kelneric@sebba.dev>
Shish <webmaster@shishnet.org> <shish@shishnet.org>
Kaylee Simmons <kay@the-simmons.net> <keith@zed.dev>
Kyle Caverly <kylebcaverly@gmail.com> <kyle@zed.dev>
Max Brunsfeld <maxbrunsfeld@gmail.com> <max@zed.dev>
Michael Sloan <michael@zed.dev> <mgsloan@google.com>
Ivan Žužak <izuzak@gmail.com> <ivan.zuzak@github.com>
Richard Feldman <oss@rtfeldman.com> <richard@zed.dev>
Thorsten Ball <mrnugget@gmail.com> <thorsten@zed.dev>
Conrad Irwin <conrad@zed.dev> <conrad.irwin@gmail.com>
Kirill Bulatov <kirill@zed.dev> <mail4score@gmail.com>
Marshall Bowers <git@maxdeviant.com> <marshall@zed.dev>
Will Bradley <williambbradley@gmail.com> <will@zed.dev>
Christian Bergschneider <christian.bergschneider@gmx.de>
Elvis Pranskevichus <elvis@geldata.com> <elvis@magic.io>
Greg Morenz <greg-morenz@droid.cafe> <morenzg@gmail.com>
Thorsten Ball <mrnugget@gmail.com> <me@thorstenball.com>
Edwin Aronsson <75266237+4teapo@users.noreply.github.com>
Gowtham K <73059450+dovakin0007@users.noreply.github.com>
Marko Kungla <marko.kungla@gmail.com> <marko@mkungla.dev>
Mikayla Maki <mikayla@zed.dev> <mikayla.c.maki@gmail.com>
Mikayla Maki <mikayla@zed.dev> <mikayla.c.maki@icloud.com>
Tristan Hume <tris.hume@gmail.com> <tristan@anthropic.com>
Thomas <github.thomaub@gmail.com> <thomas.aubry@paylead.fr>
Boris Cherny <boris@anthropic.com> <boris@performancejs.com>
Kaylee Simmons <kay@the-simmons.net> <keith@the-simmons.net>
Thomas <github.thomaub@gmail.com> <thomas.aubry94@gmail.com>
Muhammad Talal Anwar <mail@talal.io> <talalanwar@outlook.com>
Roy Williams <roy.williams.iii@gmail.com> <roy@anthropic.com>
Marshall Bowers <git@maxdeviant.com> <elliott.codes@gmail.com>
Thorben Kröger <dev@thorben.net> <thorben.kroeger@hexagon.com>
Andrei Zvonimir Crnković <andrei@0x7f.dev> <andreicek@0x7f.dev>
Thomas Mickley-Doyle <tmickleydoyle@gmail.com> <thomas@zed.dev>
Alexander Mankuta <alex@pointless.one> <alex+github@pointless.one>
LoganDark <contact@logandark.mozmail.com> <git@logandark.mozmail.com>
Max Linke <maxlinke88@gmail.com> <kain88-de@users.noreply.github.com>
Alex Viscreanu <alexviscreanu@gmail.com> <alexandru.viscreanu@kiwi.com>
Finn Evers <dev@bahn.sh> <75036051+MrSubidubi@users.noreply.github.com>
Nigel Jose <nigelmjose@gmail.com> <nigel.jose@student.manchester.ac.uk>
Uladzislau Kaminski <i@uladkaminski.com> <uladzislau_kaminski@epam.com>
LoganDark <contact@logandark.mozmail.com> <github@logandark.mozmail.com>
Thomas Heartman <thomasheartman+github@gmail.com> <thomas@getunleash.io>
Christian Bergschneider <christian.bergschneider@gmx.de> <magiclake@gmx.de>
Evren Sen <nervenes@icloud.com> <146845123+evrsen@users.noreply.github.com>
Piotr Osiewicz <piotr@zed.dev> <24362066+osiewicz@users.noreply.github.com>
Vitaly Slobodin <vitaliy.slobodin@gmail.com> <vitaly_slobodin@fastmail.com>
Danilo Leal <danilo@zed.dev> <67129314+danilo-leal@users.noreply.github.com>
Angelk90 <angelo.k90@hotmail.it> <20476002+Angelk90@users.noreply.github.com>
Bennet Bo Fenner <bennet@zed.dev> <53836821+bennetbo@users.noreply.github.com>
Rashid Almheiri <r.muhairi@pm.me> <69181766+huwaireb@users.noreply.github.com>
Evren Sen <nervenes@icloud.com> <146845123+evrensen467@users.noreply.github.com>
Joseph T. Lyons <JosephTLyons@gmail.com> <JosephTLyons@users.noreply.github.com>
Lilith Iris <itslirissama@gmail.com> <83819417+Irilith@users.noreply.github.com>
Fernando Tagawa <tagawafernando@gmail.com> <fernando.tagawa.gamail.com@gmail.com>
Julia <floc@unpromptedtirade.com> <30666851+ForLoveOfCats@users.noreply.github.com>
Sebastijan Kelnerič <sebastijan.kelneric@sebba.dev> <sebastijan.kelneric@vichava.com>
```

which looks good. There's a bit of Unicode in there -- though no
grapheme clusters.
Column number calculations do not seem to handle grapheme clusters
either (?) so I
thought this is OK.

Open questions are:

- should this be added to vim mode as well?
- is `TextSummary` the way to go here? Is it perhaps too expensive? (it
seems fine -- manually counting `char`s seems more brittle -- this way
it will stay in sync with column number calculations)

---

Team, I realize you [ask for a discussion to be opened
first](86161aa427/CONTRIBUTING.md (L32)),
so apologies for not doing that!

It turned out hacking on Zed was much easier than expected (it's really
nice!), and this change is small, adding a variation to an existing
feature. Hope that's fine.

Release Notes:

- Added feature to sort lines by their length

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-07-07 11:02:35 -06:00
Oleksiy Syvokon
d87603dd60 agent: Send stale file notifications using the project_notifications tool (#34005)
This commit introduces the `project_notifications` tool, which
proactively pushes notifications to the agent.

Unlike other tools, `Thread` automatically invokes this tool on every
turn, even when the LLM doesn't ask for it. When notifications are
available, the tool use and results are inserted into the thread,
simulating an LLM tool call.

As with other tools, users can disable `project_notifications` in
Profiles if they do not want them.

Currently, the tool only notifies users about stale files: that is,
files that have been edited by the user while the agent is also working
on them. In the future, notifications may be expanded to include
compiler diagnostics, long-running processes, and more.

Release Notes:

- Added `project_notifications` tool
2025-07-07 19:48:18 +03:00
Ben Kunkle
de9053c7ca keymap_ui: Add ability to edit context (#34019)
Closes #ISSUE

Adds a context input to the keybind edit modal. Also fixes some bugs in
the keymap update function to handle context changes gracefully. The
current keybind update strategy implemented in this PR is
* when the context doesn't change, just update the binding in place
* when the context changes, but the binding is the only binding in the
keymap section, update the binding _and_ context in place
* when the context changes, and the binding is _not_ the only binding in
the keymap section, remove the existing binding and create a new section
with the update context and binding so as to avoid impacting other
bindings

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-07-07 16:44:19 +00:00
Hilmar Wiegand
ddf3d99265 Add g-w rewrap keybind for vim visual mode (#33853)
There are both `g q` and `g w` keybinds for rewrapping in normal mode,
but `g w` is missing in visual mode. This PR adds that keybind.

Release Notes:

- Add `g w` rewrap keybind for vim visual mode
2025-07-07 10:18:55 -06:00
Richard Feldman
c35af6c2e2 Fix panic with Helix mode changing case (#34016)
Closes #33750

Release Notes:

- Fixed a panic when trying to change case in Helix mode
2025-07-07 15:51:45 +00:00
Piotr Osiewicz
6cb382c49f debugger: Make exception breakpoints persistent (#34014)
Closes #33053
Release Notes:

- Exception breakpoint state is now persisted across debugging sessions.
2025-07-07 17:40:14 +02:00
Oleksiy Syvokon
966e75b610 tools: Ensure properties always exists in JSON Schema (#34015)
OpenAI API requires `properties` to be present, even if it's empty.

Release Notes:

- N/A
2025-07-07 15:34:14 +00:00
Peter Tripp
861ca05fb9 Support loading environment from plan9 rc shell (#33599)
Closes: https://github.com/zed-industries/zed/issues/33511

Add support for loading environment from Plan9 shell
Document esoteric shell behavior.
Remove two useless tests.

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

Release Notes:

- Add support for loading environment variables from Plan9 `rc` shell.
2025-07-07 10:56:38 -04:00
Peter Tripp
f785853239 ssh: Fix incorrect handling of ssh paths that exist locally (#33743)
- Closes: https://github.com/zed-industries/zed/issues/33733

I also tested that remote canonicalization of symlink directories still
works. (e.g. `zed ssh://hostname/~/foo` where `foo -> foobar` will open
`~/foobar` on the remote).

I believe this has been broken since 2024-10-11 from
https://github.com/zed-industries/zed/pull/19057. CC: @SomeoneToIgnore.
I guess I'm the only person silly enough to run `zed
ssh://hostname/tmp`.

Release Notes:

- ssh: Fixed an issue where Zed incorrectly canonicalized paths locally
prior to connecting to the ssh remote.
2025-07-07 10:55:37 -04:00
Kirill Bulatov
82aee6bcf7 Another lsp tool UI migration (#34009)
https://github.com/user-attachments/assets/54182f0d-43e9-4482-89b9-94db5ddaabf8

Release Notes:

- N/A
2025-07-07 14:28:18 +00:00
Cole Miller
955580dae6 Adjust Go outline query for method definition to avoid pesky whitespace (#33971)
Closes #33951 

There's an adjustment that kicks in to extend `name_ranges` when we
capture more than one `@name` for an outline `@item`. That was happening
here because we captured both the parameter name for the method receiver
and the name of the method as `@name`. It seems like only the second one
should have that annotation.

Release Notes:

- Fixed extraneous leading space in `$ZED_SYMBOL` when used with Go
methods.
2025-07-07 09:51:30 -04:00
张小白
c99e42a3d6 windows: Properly handle surrogates (#34006)
Closes #33791

Surrogate pairs are now handled correctly, so input from tools like
`WinCompose` is properly received.

Release Notes:

- N/A
2025-07-07 20:21:48 +08:00
Bennet Bo Fenner
018dbfba09 agent: Show line numbers of symbols when using @symbol (#34004)
Closes #ISSUE

Release Notes:

- N/A
2025-07-07 10:53:59 +00:00
Umesh Yadav
30a441b714 agent_ui: Fix disabled context servers not showing in agent setting (#33856)
Previously if I set enabled: false for one the context servers in
settings.json it will not show up in the settings in agent panel when I
start zed. But if I enabled it from settings it properly showed up. We
were filtering the configuration to only get the enabled context servers
from settings.json. This PR adds fetching all of them.

Release Notes:

- agent: Show context servers which are disabled in settings in agent
panel settings.
2025-07-07 12:02:33 +02:00
Liam
83562fca77 copilot: Indicate whether a request is initiated by an agent to Copilot API (#33895)
Per [GitHub's documentation for VSCode's agent
mode](https://docs.github.com/en/copilot/how-tos/chat/asking-github-copilot-questions-in-your-ide#agent-mode),
a premium request is charged per user-submitted prompt. rather than per
individual request the agent makes to an LLM. This PR matches Zed's
functionality to VSCode's, accurately indicating to GitHub's API whether
a given request is initiated by the user or by an agent, allowing a user
to be metered only for prompts they send.

See also: #31068

Release Notes:

- Improve Copilot premium request tracking
2025-07-07 10:24:17 +02:00
Smit Barmase
6b456ede49 languages: Fix string override to match just string_fragment part of template_string (#33997)
Closes #33703

`template_string` consists of `template_substitution` and
`string_fragment` chunks. `template_substitution` should not be
considered a string.

```ts
const variable = `this is a string_fragment but ${this.is.template_substitution}`;
```

Release Notes:

- Fixed auto-complete not showing on typing `.` character in template
literal string in JavaScript and TypeScript files.
2025-07-07 11:45:54 +05:30
Smit Barmase
6efc5ecefe project_panel: Add Sticky Scroll (#33994)
Closes #7243

- Adds `top_slot_items` to `uniform_list` component to offset list
items.
- Adds `ToPosition` scroll strategy to `uniform_list` to scroll list to
specified index.
- Adds `sticky_items` component which can be used along with
`uniform_list` to add sticky functionality to any view that implements
uniform list.


https://github.com/user-attachments/assets/eb508fa4-167e-4595-911b-52651537284c

Release Notes:

- Added sticky scroll to the project panel, which keeps parent
directories visible while scrolling. This feature is enabled by default.
To disable it, toggle `sticky_scroll` in settings.
2025-07-07 08:32:42 +05:30
feeiyu
2246b01c4b Allow remote loading for DAP-only extensions (#33981)
Closes #ISSUE


![image](https://github.com/user-attachments/assets/8e1766e4-5d89-4263-875d-ad0dff5c55c2)


Release Notes:

- Allow remote loading for DAP-only extensions
2025-07-06 14:52:16 +02:00
Remco Smits
01295aa687 debugger: Fix the JavaScript debug terminal scenario (#33924)
There were a couple of things preventing this from working:

- our hack to stop the node REPL from appearing broke in recent versions
of the JS DAP that started passing `--experimental-network-inspection`
by default
- we had lost the ability to create a debug terminal without specifying
a program

This PR fixes those issues. We also fixed environment variables from the
**runInTerminal** request not getting passed to the spawned program.

Release Notes:

- Debugger: Fix RunInTerminal not working for JavaScript debugger.

---------

Co-authored-by: Cole Miller <cole@zed.dev>
2025-07-05 19:48:55 -04:00
Remco Smits
66e45818af debugger: Improve debug console autocompletions (#33868)
Partially fixes:
https://github.com/zed-industries/zed/discussions/33777#discussioncomment-13646294

### Improves debug console autocompletion behavior

This PR fixes a regression in completion trigger support for the debug
console, as we only looked if a completion trigger, was in the beginning
of the search text, but we also had to check if the current text is a
word so we also show completions for variables/input that doesn't start
with any of the completion triggers.

We now also leverage DAP provided information to sort completion items
more effectively. This results in improved prioritization, showing
variable completions above classes and global scope types.

I also added for completion the documentation field, that directly comes
from the DAP server. NOTE: I haven't found an adapter that returns this,
but it needs to have.

**Before**
<img width="1200" alt="Screenshot 2025-07-03 at 21 00 19"
src="https://github.com/user-attachments/assets/611e8d38-e302-4995-a425-ce2c0a1843d4"
/>

**After**
<img width="1200" alt="Screenshot 2025-07-03 at 20 59 38"
src="https://github.com/user-attachments/assets/ab1312db-bbad-49b7-872d-712d6ec708d7"
/>

Release Notes:

- Debugger: Improve autocompletion sorting for debug console
- Debugger: Fix autocompletion menu now shown when you type
- Debugger: Fix completion item showing up twice for some adapters
2025-07-05 16:20:41 +02:00
Smit Barmase
76fe33245f project_panel: Fix indent guide collapse on secondary click for multiple worktrees (#33939)
Release Notes:

- Fixed issue where `cmd`/`ctrl` click on indent guide would not
collapse directory in case of multiple projects.
2025-07-05 05:57:37 +05:30
Alvaro Parker
44d1f512f8 Use /usr/bin/env to run bash restart script (#33936)
Closes #33935

Release Notes:

- Use `/usr/bin/env` to launch the bash restart script
2025-07-05 00:27:21 +00:00
abhimanyu maurya
69fd23e947 Fix path parsing for goto syntax provided by Haskell language server (#33697)
path parsing for multiline errors provided by haskell language server
where not working correctly

<img width="875" alt="image"
src="https://github.com/user-attachments/assets/967d2e03-e167-4055-9c8e-31531cca1471"
/>

while its being parsed correctly in vscode

<img width="934" alt="image"
src="https://github.com/user-attachments/assets/a881cf0e-f06e-4b44-8363-6295bcc825fd"
/>

Release Notes:

- Fixed path parsing paths in the format of the Haskell language server
2025-07-05 02:54:43 +03:00
Vitaly Slobodin
4ad47fc9cc emmet: Enable in HTML/ERB files (#33865)
Closes [#133](https://github.com/zed-extensions/ruby/issues/133)

This PR enables the Emmet LS in `HTML/ERB` files that we added in the
https://github.com/zed-extensions/ruby/pull/113. Let me know if I picked
the right approach here.

Thanks!

Release Notes:

- N/A
2025-07-05 02:51:25 +03:00
Vitaly Slobodin
0555bbd0ec ruby: Document how to use erb-formatter for ERB files (#33872)
Hi! This is a small pull request that adds a new section about
configuring the `erb-formatter` for formatting ERB files. Thanks.

Release Notes:

- N/A
2025-07-05 02:50:51 +03:00
Jacob Duba
d3da0a809e Update documentation for tailwindcss language server now that HTML/ERB is it's own file. (#33684)
Hello,

Recently my tailwind auto completion broke in ERB files. I noticed that
HTML/ERB is it's own file type now. It used to be ERB. This broke the
previous tailwindcss ERB configuration. I made the attached change to my
configuration and it works now.
2025-07-05 02:50:09 +03:00
xdBronch
31ec7ef2ec Debugger: check for supports_single_thread_execution_requests in continue (#33937)
i found this to break my ability to continue with an lldb fork i use

Release Notes:

- N/A
2025-07-04 22:26:20 +00:00
Ryan Hawkins
75928f4859 Sync extension debuggers to remote host (#33876)
Closes #33835

Release Notes:

- Fixed debugger extensions not working in remote projects.
2025-07-04 23:26:09 +02:00
Cole Miller
543a7b123a debugger: Fix errors in JavaScript DAP schema (#33884)
`program` isn't required, and in fact our built-in `JavaScript debug
terminal` configuration doesn't have it.

Also add `node-terminal` to the list of allowed types.

Co-authored-by: Michael <michael@zed.dev>

Release Notes:

- N/A
2025-07-04 13:33:10 -04:00
177 changed files with 8816 additions and 7377 deletions

View File

@@ -33,7 +33,6 @@ workspace-members = [
"zed_emmet",
"zed_glsl",
"zed_html",
"perplexity",
"zed_proto",
"zed_ruff",
"slash_commands_example",

View File

@@ -65,7 +65,7 @@ jobs:
else
echo "run_docs=false" >> $GITHUB_OUTPUT
fi
if [[ $(git diff --name-only $COMPARE_REV ${{ github.sha }} | grep '^Cargo.lock') ]]; then
if [[ $(git diff --name-only $COMPARE_REV ${{ github.sha }} | grep -P '^(Cargo.lock|script/.*licenses)') ]]; then
echo "run_license=true" >> $GITHUB_OUTPUT
else
echo "run_license=false" >> $GITHUB_OUTPUT

View File

@@ -5,9 +5,7 @@
"build": {
"label": "Build Zed",
"command": "cargo",
"args": [
"build"
]
"args": ["build"]
}
},
{
@@ -16,9 +14,7 @@
"build": {
"label": "Build Zed",
"command": "cargo",
"args": [
"build"
]
"args": ["build"]
}
},
}
]

40
Cargo.lock generated
View File

@@ -538,6 +538,8 @@ dependencies = [
"anyhow",
"futures 0.3.31",
"gpui",
"net",
"parking_lot",
"smol",
"tempfile",
"util",
@@ -4324,6 +4326,7 @@ dependencies = [
"futures 0.3.31",
"fuzzy",
"gpui",
"indoc",
"itertools 0.14.0",
"language",
"log",
@@ -4344,6 +4347,7 @@ dependencies = [
"tasks_ui",
"telemetry",
"terminal_view",
"text",
"theme",
"tree-sitter",
"tree-sitter-go",
@@ -8951,6 +8955,7 @@ dependencies = [
"credentials_provider",
"deepseek",
"editor",
"feature_flags",
"fs",
"futures 0.3.31",
"google_ai",
@@ -9023,7 +9028,6 @@ dependencies = [
"itertools 0.14.0",
"language",
"lsp",
"picker",
"project",
"release_channel",
"serde_json",
@@ -10230,6 +10234,18 @@ dependencies = [
"jni-sys",
]
[[package]]
name = "net"
version = "0.1.0"
dependencies = [
"anyhow",
"async-io",
"smol",
"tempfile",
"windows 0.61.1",
"workspace-hack",
]
[[package]]
name = "new_debug_unreachable"
version = "1.0.6"
@@ -11335,14 +11351,6 @@ version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "perplexity"
version = "0.1.0"
dependencies = [
"serde",
"zed_extension_api 0.6.0",
]
[[package]]
name = "pest"
version = "2.8.0"
@@ -12534,6 +12542,7 @@ dependencies = [
"prost 0.9.0",
"prost-build 0.9.0",
"serde",
"typed-path",
"workspace-hack",
]
@@ -13197,6 +13206,7 @@ dependencies = [
"fs",
"futures 0.3.31",
"git",
"git2",
"git_hosting_providers",
"gpui",
"gpui_tokio",
@@ -17035,6 +17045,12 @@ dependencies = [
"utf-8",
]
[[package]]
name = "typed-path"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c462d18470a2857aa657d338af5fa67170bb48bcc80a296710ce3b0802a32566"
[[package]]
name = "typeid"
version = "1.0.3"
@@ -18281,6 +18297,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"client",
"feature_flags",
"futures 0.3.31",
"gpui",
"http_client",
@@ -18358,7 +18375,6 @@ dependencies = [
"language",
"picker",
"project",
"schemars",
"serde",
"settings",
"telemetry",
@@ -20144,9 +20160,9 @@ dependencies = [
[[package]]
name = "zed_llm_client"
version = "0.8.5"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c740e29260b8797ad252c202ea09a255b3cbc13f30faaf92fb6b2490336106e0"
checksum = "6607f74dee2a18a9ce0f091844944a0e59881359ab62e0768fb0618f55d4c1dc"
dependencies = [
"anyhow",
"serde",

View File

@@ -99,6 +99,7 @@ members = [
"crates/migrator",
"crates/mistral",
"crates/multi_buffer",
"crates/net",
"crates/node_runtime",
"crates/notifications",
"crates/ollama",
@@ -188,7 +189,6 @@ members = [
"extensions/emmet",
"extensions/glsl",
"extensions/html",
"extensions/perplexity",
"extensions/proto",
"extensions/ruff",
"extensions/slash-commands-example",
@@ -311,6 +311,7 @@ menu = { path = "crates/menu" }
migrator = { path = "crates/migrator" }
mistral = { path = "crates/mistral" }
multi_buffer = { path = "crates/multi_buffer" }
net = { path = "crates/net" }
node_runtime = { path = "crates/node_runtime" }
notifications = { path = "crates/notifications" }
ollama = { path = "crates/ollama" }
@@ -625,7 +626,7 @@ wasmtime = { version = "29", default-features = false, features = [
wasmtime-wasi = "29"
which = "6.0.0"
workspace-hack = "0.1.0"
zed_llm_client = "= 0.8.5"
zed_llm_client = "= 0.8.6"
zstd = "0.11"
[workspace.dependencies.async-stripe]
@@ -660,6 +661,7 @@ features = [
"Win32_Graphics_Gdi",
"Win32_Graphics_Imaging",
"Win32_Graphics_Imaging_D2D",
"Win32_Networking_WinSock",
"Win32_Security",
"Win32_Security_Credentials",
"Win32_Storage_FileSystem",

View File

@@ -1,3 +1,3 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.75776 5.50003H8.49988C8.70769 5.50003 8.89518 5.62971 8.95455 5.82346C9.04049 6.01876 8.9858 6.23906 8.82956 6.37656L4.82971 9.87643C4.65315 10.0295 4.39488 10.042 4.20614 9.90455C4.01724 9.76705 3.94849 9.51706 4.04052 9.30301L5.24219 6.49999H3.48601C3.2918 6.49999 3.10524 6.37031 3.03197 6.17657C2.9587 5.98126 3.014 5.76096 3.1708 5.62346L7.17018 2.12375C7.34674 1.97001 7.60454 1.95829 7.7936 2.09547C7.98265 2.23275 8.0514 2.48218 7.95922 2.69695L6.75776 5.50003Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.98749 1.67322C7.08029 1.71878 7.15543 1.79374 7.20121 1.88643C7.24699 1.97912 7.26084 2.08434 7.24061 2.18572L6.72812 4.75007H9.28122C9.37107 4.75006 9.45903 4.77588 9.53463 4.82445C9.61022 4.87302 9.67027 4.94229 9.70761 5.02402C9.74495 5.10574 9.75801 5.19648 9.74524 5.28542C9.73247 5.37437 9.69441 5.45776 9.63559 5.52569L5.57313 10.2131C5.50536 10.2912 5.41366 10.3447 5.31233 10.3653C5.211 10.3858 5.10571 10.3723 5.01285 10.3268C4.92 10.2813 4.8448 10.2064 4.79896 10.1137C4.75311 10.021 4.7392 9.9158 4.75939 9.81439L5.27188 7.25004H2.71878C2.62893 7.25005 2.54097 7.22423 2.46537 7.17566C2.38978 7.12709 2.32973 7.05782 2.29239 6.97609C2.25505 6.89437 2.24199 6.80363 2.25476 6.71469C2.26753 6.62574 2.30559 6.54235 2.36441 6.47443L6.42687 1.78697C6.49466 1.70879 6.58641 1.65524 6.68782 1.63467C6.78923 1.61409 6.89459 1.62765 6.98749 1.67322Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 601 B

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -189,6 +189,8 @@
"z shift-r": "editor::UnfoldAll",
"z l": "vim::ColumnRight",
"z h": "vim::ColumnLeft",
"z shift-l": "vim::HalfPageRight",
"z shift-h": "vim::HalfPageLeft",
"shift-z shift-q": ["pane::CloseActiveItem", { "save_intent": "skip" }],
"shift-z shift-z": ["pane::CloseActiveItem", { "save_intent": "save_all" }],
// Count support
@@ -218,35 +220,18 @@
"context": "vim_mode == normal",
"bindings": {
"ctrl-[": "editor::Cancel",
"escape": "editor::Cancel",
":": "command_palette::Toggle",
"c": "vim::PushChange",
"shift-c": "vim::ChangeToEndOfLine",
"d": "vim::PushDelete",
"delete": "vim::DeleteRight",
"shift-d": "vim::DeleteToEndOfLine",
"shift-j": "vim::JoinLines",
"g shift-j": "vim::JoinLinesNoWhitespace",
"y": "vim::PushYank",
"shift-y": "vim::YankLine",
"i": "vim::InsertBefore",
"shift-i": "vim::InsertFirstNonWhitespace",
"a": "vim::InsertAfter",
"shift-a": "vim::InsertEndOfLine",
"x": "vim::DeleteRight",
"shift-x": "vim::DeleteLeft",
"o": "vim::InsertLineBelow",
"shift-o": "vim::InsertLineAbove",
"~": "vim::ChangeCase",
"ctrl-a": "vim::Increment",
"ctrl-x": "vim::Decrement",
"p": "vim::Paste",
"shift-p": ["vim::Paste", { "before": true }],
"u": "vim::Undo",
"ctrl-r": "vim::Redo",
"r": "vim::PushReplace",
"s": "vim::Substitute",
"shift-s": "vim::SubstituteLine",
">": "vim::PushIndent",
"<": "vim::PushOutdent",
"=": "vim::PushAutoIndent",
@@ -256,11 +241,8 @@
"g ~": "vim::PushOppositeCase",
"g ?": "vim::PushRot13",
// "g ?": "vim::PushRot47",
"\"": "vim::PushRegister",
"g w": "vim::PushRewrap",
"g q": "vim::PushRewrap",
"ctrl-pagedown": "pane::ActivateNextItem",
"ctrl-pageup": "pane::ActivatePreviousItem",
"insert": "vim::InsertBefore",
// tree-sitter related commands
"[ x": "vim::SelectLargerSyntaxNode",
@@ -327,6 +309,7 @@
"g shift-r": ["vim::Paste", { "preserve_clipboard": true }],
"g c": "vim::ToggleComments",
"g q": "vim::Rewrap",
"g w": "vim::Rewrap",
"g ?": "vim::ConvertToRot13",
// "g ?": "vim::ConvertToRot47",
"\"": "vim::PushRegister",
@@ -363,18 +346,11 @@
}
},
{
"context": "vim_mode == helix_normal && !menu",
"context": "(vim_mode == normal || vim_mode == helix_normal) && !menu",
"bindings": {
"escape": "editor::Cancel",
"ctrl-[": "editor::Cancel",
":": "command_palette::Toggle",
"left": "vim::WrappingLeft",
"right": "vim::WrappingRight",
"h": "vim::WrappingLeft",
"l": "vim::WrappingRight",
"shift-d": "vim::DeleteToEndOfLine",
"shift-j": "vim::JoinLines",
"y": "editor::Copy",
"shift-y": "vim::YankLine",
"i": "vim::InsertBefore",
"shift-i": "vim::InsertFirstNonWhitespace",
@@ -388,27 +364,39 @@
"p": "vim::Paste",
"shift-p": ["vim::Paste", { "before": true }],
"u": "vim::Undo",
"r": "vim::PushReplace",
"s": "vim::Substitute",
"shift-s": "vim::SubstituteLine",
"\"": "vim::PushRegister",
"ctrl-pagedown": "pane::ActivateNextItem",
"ctrl-pageup": "pane::ActivatePreviousItem"
}
},
{
"context": "vim_mode == helix_normal && !menu",
"bindings": {
"ctrl-[": "editor::Cancel",
":": "command_palette::Toggle",
"left": "vim::WrappingLeft",
"right": "vim::WrappingRight",
"h": "vim::WrappingLeft",
"l": "vim::WrappingRight",
"y": "editor::Copy",
"alt-;": "vim::OtherEnd",
"ctrl-r": "vim::Redo",
"f": ["vim::PushFindForward", { "before": false, "multiline": true }],
"t": ["vim::PushFindForward", { "before": true, "multiline": true }],
"shift-f": ["vim::PushFindBackward", { "after": false, "multiline": true }],
"shift-t": ["vim::PushFindBackward", { "after": true, "multiline": true }],
"r": "vim::PushReplace",
"s": "vim::Substitute",
"shift-s": "vim::SubstituteLine",
">": "vim::Indent",
"<": "vim::Outdent",
"=": "vim::AutoIndent",
"g u": "vim::PushLowercase",
"g shift-u": "vim::PushUppercase",
"g ~": "vim::PushOppositeCase",
"\"": "vim::PushRegister",
"g q": "vim::PushRewrap",
"g w": "vim::PushRewrap",
"ctrl-pagedown": "pane::ActivateNextItem",
"ctrl-pageup": "pane::ActivatePreviousItem",
"insert": "vim::InsertBefore",
".": "vim::Repeat",
"alt-.": "vim::RepeatFind",
// tree-sitter related commands
"[ x": "editor::SelectLargerSyntaxNode",
@@ -428,7 +416,6 @@
"g h": "vim::StartOfLine",
"g s": "vim::FirstNonWhitespace", // "g s" default behavior is "space s"
"g e": "vim::EndOfDocument",
"g y": "editor::GoToTypeDefinition",
"g r": "editor::FindAllReferences", // zed specific
"g t": "vim::WindowTop",
"g c": "vim::WindowMiddle",

View File

@@ -617,6 +617,8 @@
// 3. Mark files with errors and warnings:
// "all"
"show_diagnostics": "all",
// Whether to stick parent directories at top of the project panel.
"sticky_scroll": true,
// Settings related to indent guides in the project panel.
"indent_guides": {
// When to show indent guides in the project panel.
@@ -808,6 +810,7 @@
"edit_file": true,
"fetch": true,
"list_directory": true,
"project_notifications": true,
"move_path": true,
"now": true,
"find_path": true,
@@ -827,6 +830,7 @@
"diagnostics": true,
"fetch": true,
"list_directory": true,
"project_notifications": true,
"now": true,
"find_path": true,
"read_file": true,
@@ -853,7 +857,15 @@
// its response, or needs user input.
// Default: false
"play_sound_when_agent_done": false
"play_sound_when_agent_done": false,
/// Whether to have edit cards in the agent panel expanded, showing a preview of the full diff.
///
/// Default: true
"expand_edit_card": true,
/// Whether to have terminal cards in the agent panel expanded, showing the whole command output.
///
/// Default: true
"expand_terminal_card": true
},
// The settings for slash commands.
"slash_commands": {
@@ -1348,7 +1360,7 @@
// 5. Never show the scrollbar:
// "never"
"show": null
}
},
// Set the terminal's font size. If this option is not included,
// the terminal will default to matching the buffer's font size.
// "font_size": 15,
@@ -1365,6 +1377,21 @@
// Default: 10_000, maximum: 100_000 (all bigger values set will be treated as 100_000), 0 disables the scrolling.
// Existing terminals will not pick up this change until they are recreated.
// "max_scroll_history_lines": 10000,
// The minimum APCA perceptual contrast between foreground and background colors.
// APCA (Accessible Perceptual Contrast Algorithm) is more accurate than WCAG 2.x,
// especially for dark mode. Values range from 0 to 106.
//
// Based on APCA Readability Criterion (ARC) Bronze Simple Mode:
// https://readtech.org/ARC/tests/bronze-simple-mode/
// - 0: No contrast adjustment
// - 45: Minimum for large fluent text (36px+)
// - 60: Minimum for other content text
// - 75: Minimum for body text
// - 90: Preferred for body text
//
// Most terminal themes have APCA values of 40-70.
// A value of 45 preserves colorful themes while ensuring legibility.
"minimum_contrast": 45
},
"code_actions_on_format": {},
// Settings related to running tasks.
@@ -1576,6 +1603,9 @@
"use_on_type_format": false,
"allow_rewrap": "anywhere",
"soft_wrap": "editor_width",
"completions": {
"words": "disabled"
},
"prettier": {
"allowed": true
}
@@ -1589,6 +1619,9 @@
}
},
"Plain Text": {
"completions": {
"words": "disabled"
},
"allow_rewrap": "anywhere"
},
"Python": {

View File

@@ -3,13 +3,6 @@
// For more documentation on how to configure debug tasks,
// see: https://zed.dev/docs/debugger
[
{
"label": "Debug active PHP file",
"adapter": "PHP",
"program": "$ZED_FILE",
"request": "launch",
"cwd": "$ZED_WORKTREE_ROOT"
},
{
"label": "Debug active Python file",
"adapter": "Debugpy",

View File

@@ -6,398 +6,7 @@
{
"name": "One Dark",
"appearance": "dark",
"style": {
"border": "#464b57ff",
"border.variant": "#363c46ff",
"border.focused": "#47679eff",
"border.selected": "#293b5bff",
"border.transparent": "#00000000",
"border.disabled": "#414754ff",
"elevated_surface.background": "#2f343eff",
"surface.background": "#2f343eff",
"background": "#3b414dff",
"element.background": "#2e343eff",
"element.hover": "#363c46ff",
"element.active": "#454a56ff",
"element.selected": "#454a56ff",
"element.disabled": "#2e343eff",
"drop_target.background": "#83899480",
"ghost_element.background": "#00000000",
"ghost_element.hover": "#363c46ff",
"ghost_element.active": "#454a56ff",
"ghost_element.selected": "#454a56ff",
"ghost_element.disabled": "#2e343eff",
"text": "#dce0e5ff",
"text.muted": "#a9afbcff",
"text.placeholder": "#878a98ff",
"text.disabled": "#878a98ff",
"text.accent": "#74ade8ff",
"icon": "#dce0e5ff",
"icon.muted": "#a9afbcff",
"icon.disabled": "#878a98ff",
"icon.placeholder": "#a9afbcff",
"icon.accent": "#74ade8ff",
"status_bar.background": "#3b414dff",
"title_bar.background": "#3b414dff",
"title_bar.inactive_background": "#2e343eff",
"toolbar.background": "#282c33ff",
"tab_bar.background": "#2f343eff",
"tab.inactive_background": "#2f343eff",
"tab.active_background": "#282c33ff",
"search.match_background": "#74ade866",
"panel.background": "#2f343eff",
"panel.focused_border": null,
"pane.focused_border": null,
"scrollbar.thumb.background": "#c8ccd44c",
"scrollbar.thumb.hover_background": "#363c46ff",
"scrollbar.thumb.border": "#363c46ff",
"scrollbar.track.background": "#00000000",
"scrollbar.track.border": "#2e333cff",
"editor.foreground": "#acb2beff",
"editor.background": "#282c33ff",
"editor.gutter.background": "#282c33ff",
"editor.subheader.background": "#2f343eff",
"editor.active_line.background": "#2f343ebf",
"editor.highlighted_line.background": "#2f343eff",
"editor.line_number": "#4e5a5f",
"editor.active_line_number": "#d0d4da",
"editor.hover_line_number": "#acb0b4",
"editor.invisible": "#878a98ff",
"editor.wrap_guide": "#c8ccd40d",
"editor.active_wrap_guide": "#c8ccd41a",
"editor.document_highlight.read_background": "#74ade81a",
"editor.document_highlight.write_background": "#555a6366",
"terminal.background": "#282c33ff",
"terminal.foreground": "#dce0e5ff",
"terminal.bright_foreground": "#dce0e5ff",
"terminal.dim_foreground": "#282c33ff",
"terminal.ansi.black": "#282c33ff",
"terminal.ansi.bright_black": "#525561ff",
"terminal.ansi.dim_black": "#dce0e5ff",
"terminal.ansi.red": "#d07277ff",
"terminal.ansi.bright_red": "#673a3cff",
"terminal.ansi.dim_red": "#eab7b9ff",
"terminal.ansi.green": "#a1c181ff",
"terminal.ansi.bright_green": "#4d6140ff",
"terminal.ansi.dim_green": "#d1e0bfff",
"terminal.ansi.yellow": "#dec184ff",
"terminal.ansi.bright_yellow": "#e5c07bff",
"terminal.ansi.dim_yellow": "#f1dfc1ff",
"terminal.ansi.blue": "#74ade8ff",
"terminal.ansi.bright_blue": "#385378ff",
"terminal.ansi.dim_blue": "#bed5f4ff",
"terminal.ansi.magenta": "#be5046ff",
"terminal.ansi.bright_magenta": "#5e2b26ff",
"terminal.ansi.dim_magenta": "#e6a79eff",
"terminal.ansi.cyan": "#6eb4bfff",
"terminal.ansi.bright_cyan": "#3a565bff",
"terminal.ansi.dim_cyan": "#b9d9dfff",
"terminal.ansi.white": "#dce0e5ff",
"terminal.ansi.bright_white": "#dce0e5ff",
"terminal.ansi.dim_white": "#575d65ff",
"link_text.hover": "#74ade8ff",
"version_control.added": "#27a657ff",
"version_control.modified": "#d3b020ff",
"version_control.deleted": "#e06c76ff",
"version_control.conflict_marker.ours": "#a1c1811a",
"version_control.conflict_marker.theirs": "#74ade81a",
"conflict": "#dec184ff",
"conflict.background": "#dec1841a",
"conflict.border": "#5d4c2fff",
"created": "#a1c181ff",
"created.background": "#a1c1811a",
"created.border": "#38482fff",
"deleted": "#d07277ff",
"deleted.background": "#d072771a",
"deleted.border": "#4c2b2cff",
"error": "#d07277ff",
"error.background": "#d072771a",
"error.border": "#4c2b2cff",
"hidden": "#878a98ff",
"hidden.background": "#696b771a",
"hidden.border": "#414754ff",
"hint": "#788ca6ff",
"hint.background": "#5a6f891a",
"hint.border": "#293b5bff",
"ignored": "#878a98ff",
"ignored.background": "#696b771a",
"ignored.border": "#464b57ff",
"info": "#74ade8ff",
"info.background": "#74ade81a",
"info.border": "#293b5bff",
"modified": "#dec184ff",
"modified.background": "#dec1841a",
"modified.border": "#5d4c2fff",
"predictive": "#5a6a87ff",
"predictive.background": "#5a6a871a",
"predictive.border": "#38482fff",
"renamed": "#74ade8ff",
"renamed.background": "#74ade81a",
"renamed.border": "#293b5bff",
"success": "#a1c181ff",
"success.background": "#a1c1811a",
"success.border": "#38482fff",
"unreachable": "#a9afbcff",
"unreachable.background": "#8389941a",
"unreachable.border": "#464b57ff",
"warning": "#dec184ff",
"warning.background": "#dec1841a",
"warning.border": "#5d4c2fff",
"players": [
{
"cursor": "#74ade8ff",
"background": "#74ade8ff",
"selection": "#74ade83d"
},
{
"cursor": "#be5046ff",
"background": "#be5046ff",
"selection": "#be50463d"
},
{
"cursor": "#bf956aff",
"background": "#bf956aff",
"selection": "#bf956a3d"
},
{
"cursor": "#b477cfff",
"background": "#b477cfff",
"selection": "#b477cf3d"
},
{
"cursor": "#6eb4bfff",
"background": "#6eb4bfff",
"selection": "#6eb4bf3d"
},
{
"cursor": "#d07277ff",
"background": "#d07277ff",
"selection": "#d072773d"
},
{
"cursor": "#dec184ff",
"background": "#dec184ff",
"selection": "#dec1843d"
},
{
"cursor": "#a1c181ff",
"background": "#a1c181ff",
"selection": "#a1c1813d"
}
],
"syntax": {
"attribute": {
"color": "#74ade8ff",
"font_style": null,
"font_weight": null
},
"boolean": {
"color": "#bf956aff",
"font_style": null,
"font_weight": null
},
"comment": {
"color": "#5d636fff",
"font_style": null,
"font_weight": null
},
"comment.doc": {
"color": "#878e98ff",
"font_style": null,
"font_weight": null
},
"constant": {
"color": "#dfc184ff",
"font_style": null,
"font_weight": null
},
"constructor": {
"color": "#73ade9ff",
"font_style": null,
"font_weight": null
},
"embedded": {
"color": "#dce0e5ff",
"font_style": null,
"font_weight": null
},
"emphasis": {
"color": "#74ade8ff",
"font_style": null,
"font_weight": null
},
"emphasis.strong": {
"color": "#bf956aff",
"font_style": null,
"font_weight": 700
},
"enum": {
"color": "#d07277ff",
"font_style": null,
"font_weight": null
},
"function": {
"color": "#73ade9ff",
"font_style": null,
"font_weight": null
},
"hint": {
"color": "#788ca6ff",
"font_style": null,
"font_weight": 700
},
"keyword": {
"color": "#b477cfff",
"font_style": null,
"font_weight": null
},
"label": {
"color": "#74ade8ff",
"font_style": null,
"font_weight": null
},
"link_text": {
"color": "#73ade9ff",
"font_style": "normal",
"font_weight": null
},
"link_uri": {
"color": "#6eb4bfff",
"font_style": null,
"font_weight": null
},
"namespace": {
"color": "#dce0e5ff",
"font_style": null,
"font_weight": null
},
"number": {
"color": "#bf956aff",
"font_style": null,
"font_weight": null
},
"operator": {
"color": "#6eb4bfff",
"font_style": null,
"font_weight": null
},
"predictive": {
"color": "#5a6a87ff",
"font_style": "italic",
"font_weight": null
},
"preproc": {
"color": "#dce0e5ff",
"font_style": null,
"font_weight": null
},
"primary": {
"color": "#acb2beff",
"font_style": null,
"font_weight": null
},
"property": {
"color": "#d07277ff",
"font_style": null,
"font_weight": null
},
"punctuation": {
"color": "#acb2beff",
"font_style": null,
"font_weight": null
},
"punctuation.bracket": {
"color": "#b2b9c6ff",
"font_style": null,
"font_weight": null
},
"punctuation.delimiter": {
"color": "#b2b9c6ff",
"font_style": null,
"font_weight": null
},
"punctuation.list_marker": {
"color": "#d07277ff",
"font_style": null,
"font_weight": null
},
"punctuation.special": {
"color": "#b1574bff",
"font_style": null,
"font_weight": null
},
"selector": {
"color": "#dfc184ff",
"font_style": null,
"font_weight": null
},
"selector.pseudo": {
"color": "#74ade8ff",
"font_style": null,
"font_weight": null
},
"string": {
"color": "#a1c181ff",
"font_style": null,
"font_weight": null
},
"string.escape": {
"color": "#878e98ff",
"font_style": null,
"font_weight": null
},
"string.regex": {
"color": "#bf956aff",
"font_style": null,
"font_weight": null
},
"string.special": {
"color": "#bf956aff",
"font_style": null,
"font_weight": null
},
"string.special.symbol": {
"color": "#bf956aff",
"font_style": null,
"font_weight": null
},
"tag": {
"color": "#74ade8ff",
"font_style": null,
"font_weight": null
},
"text.literal": {
"color": "#a1c181ff",
"font_style": null,
"font_weight": null
},
"title": {
"color": "#d07277ff",
"font_style": null,
"font_weight": 400
},
"type": {
"color": "#6eb4bfff",
"font_style": null,
"font_weight": null
},
"variable": {
"color": "#acb2beff",
"font_style": null,
"font_weight": null
},
"variable.special": {
"color": "#bf956aff",
"font_style": null,
"font_weight": null
},
"variant": {
"color": "#73ade9ff",
"font_style": null,
"font_weight": null
}
}
}
"style": {}
},
{
"name": "One Light",

View File

@@ -0,0 +1,3 @@
[The following is an auto-generated notification; do not reply]
These files have changed since the last read:

View File

@@ -23,10 +23,11 @@ use gpui::{
};
use language_model::{
ConfiguredModel, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
LanguageModelId, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
LanguageModelRequestTool, LanguageModelToolResult, LanguageModelToolResultContent,
LanguageModelToolUseId, MessageContent, ModelRequestLimitReachedError, PaymentRequiredError,
Role, SelectedModel, StopReason, TokenUsage,
LanguageModelExt as _, LanguageModelId, LanguageModelRegistry, LanguageModelRequest,
LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolResult,
LanguageModelToolResultContent, LanguageModelToolUse, LanguageModelToolUseId, MessageContent,
ModelRequestLimitReachedError, PaymentRequiredError, Role, SelectedModel, StopReason,
TokenUsage,
};
use postage::stream::Stream as _;
use project::{
@@ -45,7 +46,7 @@ use std::{
time::{Duration, Instant},
};
use thiserror::Error;
use util::{ResultExt as _, post_inc};
use util::{ResultExt as _, debug_panic, post_inc};
use uuid::Uuid;
use zed_llm_client::{CompletionIntent, CompletionRequestStatus, UsageLimit};
@@ -1248,6 +1249,8 @@ impl Thread {
self.remaining_turns -= 1;
self.flush_notifications(model.clone(), intent, cx);
let request = self.to_completion_request(model.clone(), intent, cx);
self.stream_completion(request, model, intent, window, cx);
@@ -1481,6 +1484,111 @@ impl Thread {
request
}
/// Insert auto-generated notifications (if any) to the thread
fn flush_notifications(
&mut self,
model: Arc<dyn LanguageModel>,
intent: CompletionIntent,
cx: &mut Context<Self>,
) {
match intent {
CompletionIntent::UserPrompt | CompletionIntent::ToolResults => {
if let Some(pending_tool_use) = self.attach_tracked_files_state(model, cx) {
cx.emit(ThreadEvent::ToolFinished {
tool_use_id: pending_tool_use.id.clone(),
pending_tool_use: Some(pending_tool_use),
});
}
}
CompletionIntent::ThreadSummarization
| CompletionIntent::ThreadContextSummarization
| CompletionIntent::CreateFile
| CompletionIntent::EditFile
| CompletionIntent::InlineAssist
| CompletionIntent::TerminalInlineAssist
| CompletionIntent::GenerateGitCommitMessage => {}
};
}
fn attach_tracked_files_state(
&mut self,
model: Arc<dyn LanguageModel>,
cx: &mut App,
) -> Option<PendingToolUse> {
let action_log = self.action_log.read(cx);
action_log.unnotified_stale_buffers(cx).next()?;
// Represent notification as a simulated `project_notifications` tool call
let tool_name = Arc::from("project_notifications");
let Some(tool) = self.tools.read(cx).tool(&tool_name, cx) else {
debug_panic!("`project_notifications` tool not found");
return None;
};
if !self.profile.is_tool_enabled(tool.source(), tool.name(), cx) {
return None;
}
let input = serde_json::json!({});
let request = Arc::new(LanguageModelRequest::default()); // unused
let window = None;
let tool_result = tool.run(
input,
request,
self.project.clone(),
self.action_log.clone(),
model.clone(),
window,
cx,
);
let tool_use_id =
LanguageModelToolUseId::from(format!("project_notifications_{}", self.messages.len()));
let tool_use = LanguageModelToolUse {
id: tool_use_id.clone(),
name: tool_name.clone(),
raw_input: "{}".to_string(),
input: serde_json::json!({}),
is_input_complete: true,
};
let tool_output = cx.background_executor().block(tool_result.output);
// Attach a project_notification tool call to the latest existing
// Assistant message. We cannot create a new Assistant message
// because thinking models require a `thinking` block that we
// cannot mock. We cannot send a notification as a normal
// (non-tool-use) User message because this distracts Agent
// too much.
let tool_message_id = self
.messages
.iter()
.enumerate()
.rfind(|(_, message)| message.role == Role::Assistant)
.map(|(_, message)| message.id)?;
let tool_use_metadata = ToolUseMetadata {
model: model.clone(),
thread_id: self.id.clone(),
prompt_id: self.last_prompt_id.clone(),
};
self.tool_use
.request_tool_use(tool_message_id, tool_use, tool_use_metadata.clone(), cx);
let pending_tool_use = self.tool_use.insert_tool_output(
tool_use_id.clone(),
tool_name,
tool_output,
self.configured_model.as_ref(),
self.completion_mode,
);
pending_tool_use
}
pub fn stream_completion(
&mut self,
request: LanguageModelRequest,
@@ -1504,6 +1612,10 @@ impl Thread {
prompt_id: prompt_id.clone(),
};
let completion_mode = request
.mode
.unwrap_or(zed_llm_client::CompletionMode::Normal);
self.last_received_chunk_at = Some(Instant::now());
let task = cx.spawn(async move |thread, cx| {
@@ -1853,7 +1965,11 @@ impl Thread {
.unwrap_or(0)
// We know the context window was exceeded in practice, so if our estimate was
// lower than max tokens, the estimate was wrong; return that we exceeded by 1.
.max(model.max_token_count().saturating_add(1))
.max(
model
.max_token_count_for_mode(completion_mode)
.saturating_add(1),
)
});
thread.exceeded_window_error = Some(ExceededWindowError {
model_id: model.id(),
@@ -2401,6 +2517,7 @@ impl Thread {
hallucinated_tool_name,
Err(anyhow!("Missing tool call: {error_message}")),
self.configured_model.as_ref(),
self.completion_mode,
);
cx.emit(ThreadEvent::MissingToolUse {
@@ -2427,6 +2544,7 @@ impl Thread {
tool_name,
Err(anyhow!("Error parsing input JSON: {error}")),
self.configured_model.as_ref(),
self.completion_mode,
);
let ui_text = if let Some(pending_tool_use) = &pending_tool_use {
pending_tool_use.ui_text.clone()
@@ -2502,6 +2620,7 @@ impl Thread {
tool_name,
output,
thread.configured_model.as_ref(),
thread.completion_mode,
);
thread.tool_finished(tool_use_id, pending_tool_use, false, window, cx);
})
@@ -2978,7 +3097,9 @@ impl Thread {
return TotalTokenUsage::default();
};
let max = model.model.max_token_count();
let max = model
.model
.max_token_count_for_mode(self.completion_mode().into());
let index = self
.messages
@@ -3005,7 +3126,9 @@ impl Thread {
pub fn total_token_usage(&self) -> Option<TotalTokenUsage> {
let model = self.configured_model.as_ref()?;
let max = model.model.max_token_count();
let max = model
.model
.max_token_count_for_mode(self.completion_mode().into());
if let Some(exceeded_error) = &self.exceeded_window_error {
if model.model.id() == exceeded_error.model_id {
@@ -3071,6 +3194,7 @@ impl Thread {
tool_name,
err,
self.configured_model.as_ref(),
self.completion_mode,
);
self.tool_finished(tool_use_id.clone(), None, true, window, cx);
}
@@ -3156,10 +3280,13 @@ mod tests {
const TEST_RATE_LIMIT_RETRY_SECS: u64 = 30;
use agent_settings::{AgentProfileId, AgentSettings, LanguageModelParameters};
use assistant_tool::ToolRegistry;
use assistant_tools;
use futures::StreamExt;
use futures::future::BoxFuture;
use futures::stream::BoxStream;
use gpui::TestAppContext;
use http_client;
use indoc::indoc;
use language_model::fake_provider::{FakeLanguageModel, FakeLanguageModelProvider};
use language_model::{
LanguageModelCompletionError, LanguageModelName, LanguageModelProviderId,
@@ -3487,6 +3614,134 @@ fn main() {{
);
}
#[gpui::test]
async fn test_stale_buffer_notification(cx: &mut TestAppContext) {
init_test_settings(cx);
let project = create_test_project(
cx,
json!({"code.rs": "fn main() {\n println!(\"Hello, world!\");\n}"}),
)
.await;
let (_workspace, _thread_store, thread, context_store, model) =
setup_test_environment(cx, project.clone()).await;
// Add a buffer to the context. This will be a tracked buffer
let buffer = add_file_to_context(&project, &context_store, "test/code.rs", cx)
.await
.unwrap();
let context = context_store
.read_with(cx, |store, _| store.context().next().cloned())
.unwrap();
let loaded_context = cx
.update(|cx| load_context(vec![context], &project, &None, cx))
.await;
// Insert user message and assistant response
thread.update(cx, |thread, cx| {
thread.insert_user_message("Explain this code", loaded_context, None, Vec::new(), cx);
thread.insert_assistant_message(
vec![MessageSegment::Text("This code prints 42.".into())],
cx,
);
});
// We shouldn't have a stale buffer notification yet
let notifications = thread.read_with(cx, |thread, _| {
find_tool_uses(thread, "project_notifications")
});
assert!(
notifications.is_empty(),
"Should not have stale buffer notification before buffer is modified"
);
// Modify the buffer
buffer.update(cx, |buffer, cx| {
buffer.edit(
[(1..1, "\n println!(\"Added a new line\");\n")],
None,
cx,
);
});
// Insert another user message
thread.update(cx, |thread, cx| {
thread.insert_user_message(
"What does the code do now?",
ContextLoadResult::default(),
None,
Vec::new(),
cx,
)
});
// Check for the stale buffer warning
thread.update(cx, |thread, cx| {
thread.flush_notifications(model.clone(), CompletionIntent::UserPrompt, cx)
});
let notifications = thread.read_with(cx, |thread, _cx| {
find_tool_uses(thread, "project_notifications")
});
let [notification] = notifications.as_slice() else {
panic!("Should have a `project_notifications` tool use");
};
let Some(notification_content) = notification.content.to_str() else {
panic!("`project_notifications` should return text");
};
let expected_content = indoc! {"[The following is an auto-generated notification; do not reply]
These files have changed since the last read:
- code.rs
"};
assert_eq!(notification_content, expected_content);
// Insert another user message and flush notifications again
thread.update(cx, |thread, cx| {
thread.insert_user_message(
"Can you tell me more?",
ContextLoadResult::default(),
None,
Vec::new(),
cx,
)
});
thread.update(cx, |thread, cx| {
thread.flush_notifications(model.clone(), CompletionIntent::UserPrompt, cx)
});
// There should be no new notifications (we already flushed one)
let notifications = thread.read_with(cx, |thread, _cx| {
find_tool_uses(thread, "project_notifications")
});
assert_eq!(
notifications.len(),
1,
"Should still have only one notification after second flush - no duplicates"
);
}
fn find_tool_uses(thread: &Thread, tool_name: &str) -> Vec<LanguageModelToolResult> {
thread
.messages()
.flat_map(|message| {
thread
.tool_results_for_message(message.id)
.into_iter()
.filter(|result| result.tool_name == tool_name.into())
.cloned()
.collect::<Vec<_>>()
})
.collect()
}
#[gpui::test]
async fn test_storing_profile_setting_per_thread(cx: &mut TestAppContext) {
init_test_settings(cx);
@@ -5052,6 +5307,14 @@ fn main() {{
language_model::init_settings(cx);
ThemeSettings::register(cx);
ToolRegistry::default_global(cx);
assistant_tool::init(cx);
let http_client = Arc::new(http_client::HttpClientWithUrl::new(
http_client::FakeHttpClient::with_200_response(),
"http://localhost".to_string(),
None,
));
assistant_tools::init(http_client, cx);
});
}

View File

@@ -2,6 +2,7 @@ use crate::{
thread::{MessageId, PromptId, ThreadId},
thread_store::SerializedMessage,
};
use agent_settings::CompletionMode;
use anyhow::Result;
use assistant_tool::{
AnyToolCard, Tool, ToolResultContent, ToolResultOutput, ToolUseStatus, ToolWorkingSet,
@@ -11,8 +12,9 @@ use futures::{FutureExt as _, future::Shared};
use gpui::{App, Entity, SharedString, Task, Window};
use icons::IconName;
use language_model::{
ConfiguredModel, LanguageModel, LanguageModelRequest, LanguageModelToolResult,
LanguageModelToolResultContent, LanguageModelToolUse, LanguageModelToolUseId, Role,
ConfiguredModel, LanguageModel, LanguageModelExt, LanguageModelRequest,
LanguageModelToolResult, LanguageModelToolResultContent, LanguageModelToolUse,
LanguageModelToolUseId, Role,
};
use project::Project;
use std::sync::Arc;
@@ -400,6 +402,7 @@ impl ToolUseState {
tool_name: Arc<str>,
output: Result<ToolResultOutput>,
configured_model: Option<&ConfiguredModel>,
completion_mode: CompletionMode,
) -> Option<PendingToolUse> {
let metadata = self.tool_use_metadata_by_id.remove(&tool_use_id);
@@ -426,7 +429,10 @@ impl ToolUseState {
// Protect from overly large output
let tool_output_limit = configured_model
.map(|model| model.model.max_token_count() as usize * BYTES_PER_TOKEN_ESTIMATE)
.map(|model| {
model.model.max_token_count_for_mode(completion_mode.into()) as usize
* BYTES_PER_TOKEN_ESTIMATE
})
.unwrap_or(usize::MAX);
let content = match tool_result {

View File

@@ -67,6 +67,8 @@ pub struct AgentSettings {
pub model_parameters: Vec<LanguageModelParameters>,
pub preferred_completion_mode: CompletionMode,
pub enable_feedback: bool,
pub expand_edit_card: bool,
pub expand_terminal_card: bool,
}
impl AgentSettings {
@@ -291,6 +293,14 @@ pub struct AgentSettingsContent {
///
/// Default: true
enable_feedback: Option<bool>,
/// Whether to have edit cards in the agent panel expanded, showing a preview of the full diff.
///
/// Default: true
expand_edit_card: Option<bool>,
/// Whether to have terminal cards in the agent panel expanded, showing the whole command output.
///
/// Default: true
expand_terminal_card: Option<bool>,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Default)]
@@ -441,6 +451,11 @@ impl Settings for AgentSettings {
value.preferred_completion_mode,
);
merge(&mut settings.enable_feedback, value.enable_feedback);
merge(&mut settings.expand_edit_card, value.expand_edit_card);
merge(
&mut settings.expand_terminal_card,
value.expand_terminal_card,
);
settings
.model_parameters

View File

@@ -436,7 +436,7 @@ impl AgentConfiguration {
window: &mut Window,
cx: &mut Context<Self>,
) -> impl IntoElement {
let context_server_ids = self.context_server_store.read(cx).all_server_ids().clone();
let context_server_ids = self.context_server_store.read(cx).configured_server_ids();
v_flex()
.p(DynamicSpacing::Base16.rems(cx))

View File

@@ -426,6 +426,7 @@ impl ContextPicker {
this.add_recent_file(project_path.clone(), window, cx);
})
},
None,
)
}
RecentEntry::Thread(thread) => {
@@ -443,6 +444,7 @@ impl ContextPicker {
.detach_and_log_err(cx);
})
},
None,
)
}
}

View File

@@ -686,6 +686,7 @@ impl ContextPickerCompletionProvider {
let mut label = CodeLabel::plain(symbol.name.clone(), None);
label.push_str(" ", None);
label.push_str(&file_name, comment_id);
label.push_str(&format!(" L{}", symbol.range.start.0.row + 1), comment_id);
let new_text = format!("{} ", MentionLink::for_symbol(&symbol.name, &full_path));
let new_text_len = new_text.len();

View File

@@ -1160,7 +1160,7 @@ impl MessageEditor {
})
.child(
h_flex()
.id("file-name")
.id(("file-name", index))
.pr_8()
.gap_1p5()
.max_w_full()
@@ -1171,9 +1171,16 @@ impl MessageEditor {
.gap_0p5()
.children(file_name)
.children(file_path),
), // TODO: Implement line diff
// .child(Label::new("+").color(Color::Created))
// .child(Label::new("-").color(Color::Deleted)),
)
.on_click({
let buffer = buffer.clone();
cx.listener(move |this, _, window, cx| {
this.handle_file_click(buffer.clone(), window, cx);
})
}), // TODO: Implement line diff
// .child(Label::new("+").color(Color::Created))
// .child(Label::new("-").color(Color::Deleted)),
//
)
.child(
h_flex()

View File

@@ -38,8 +38,8 @@ use language::{
language_settings::{SoftWrap, all_language_settings},
};
use language_model::{
ConfigurationError, LanguageModelImage, LanguageModelProviderTosView, LanguageModelRegistry,
Role,
ConfigurationError, LanguageModelExt, LanguageModelImage, LanguageModelProviderTosView,
LanguageModelRegistry, Role,
};
use multi_buffer::MultiBufferRow;
use picker::{Picker, popover_menu::PickerPopoverMenu};
@@ -3063,7 +3063,7 @@ fn token_state(context: &Entity<AssistantContext>, cx: &App) -> Option<TokenStat
.default_model()?
.model;
let token_count = context.read(cx).token_count()?;
let max_token_count = model.max_token_count();
let max_token_count = model.max_token_count_for_mode(context.read(cx).completion_mode().into());
let token_state = if max_token_count.saturating_sub(token_count) == 0 {
TokenState::NoTokensLeft {
max_token_count,

View File

@@ -15,6 +15,8 @@ path = "src/askpass.rs"
anyhow.workspace = true
futures.workspace = true
gpui.workspace = true
net.workspace = true
parking_lot.workspace = true
smol.workspace = true
tempfile.workspace = true
util.workspace = true

View File

@@ -1,21 +1,14 @@
use std::path::{Path, PathBuf};
use std::time::Duration;
use std::{ffi::OsStr, time::Duration};
#[cfg(unix)]
use anyhow::Context as _;
use anyhow::{Context as _, Result};
use futures::channel::{mpsc, oneshot};
#[cfg(unix)]
use futures::{AsyncBufReadExt as _, io::BufReader};
#[cfg(unix)]
use futures::{AsyncWriteExt as _, FutureExt as _, select_biased};
use futures::{SinkExt, StreamExt};
use futures::{
AsyncBufReadExt as _, AsyncWriteExt as _, FutureExt as _, SinkExt, StreamExt, io::BufReader,
select_biased,
};
use gpui::{AsyncApp, BackgroundExecutor, Task};
#[cfg(unix)]
use smol::fs;
#[cfg(unix)]
use smol::net::unix::UnixListener;
#[cfg(unix)]
use util::{ResultExt as _, fs::make_file_executable, get_shell_safe_zed_path};
use util::ResultExt as _;
#[derive(PartialEq, Eq)]
pub enum AskPassResult {
@@ -42,41 +35,56 @@ impl AskPassDelegate {
Self { tx, _task: task }
}
pub async fn ask_password(&mut self, prompt: String) -> anyhow::Result<String> {
pub async fn ask_password(&mut self, prompt: String) -> Result<String> {
let (tx, rx) = oneshot::channel();
self.tx.send((prompt, tx)).await?;
Ok(rx.await?)
}
}
#[cfg(unix)]
pub struct AskPassSession {
script_path: PathBuf,
#[cfg(not(target_os = "windows"))]
script_path: std::path::PathBuf,
#[cfg(target_os = "windows")]
askpass_helper: String,
#[cfg(target_os = "windows")]
secret: std::sync::Arc<parking_lot::Mutex<String>>,
_askpass_task: Task<()>,
askpass_opened_rx: Option<oneshot::Receiver<()>>,
askpass_kill_master_rx: Option<oneshot::Receiver<()>>,
}
#[cfg(unix)]
#[cfg(not(target_os = "windows"))]
const ASKPASS_SCRIPT_NAME: &str = "askpass.sh";
#[cfg(target_os = "windows")]
const ASKPASS_SCRIPT_NAME: &str = "askpass.ps1";
impl AskPassSession {
/// This will create a new AskPassSession.
/// You must retain this session until the master process exits.
#[must_use]
pub async fn new(
executor: &BackgroundExecutor,
mut delegate: AskPassDelegate,
) -> anyhow::Result<Self> {
pub async fn new(executor: &BackgroundExecutor, mut delegate: AskPassDelegate) -> Result<Self> {
use net::async_net::UnixListener;
use util::fs::make_file_executable;
#[cfg(target_os = "windows")]
let secret = std::sync::Arc::new(parking_lot::Mutex::new(String::new()));
let temp_dir = tempfile::Builder::new().prefix("zed-askpass").tempdir()?;
let askpass_socket = temp_dir.path().join("askpass.sock");
let askpass_script_path = temp_dir.path().join("askpass.sh");
let askpass_script_path = temp_dir.path().join(ASKPASS_SCRIPT_NAME);
let (askpass_opened_tx, askpass_opened_rx) = oneshot::channel::<()>();
let listener =
UnixListener::bind(&askpass_socket).context("failed to create askpass socket")?;
let zed_path = get_shell_safe_zed_path()?;
let listener = UnixListener::bind(&askpass_socket).context("creating askpass socket")?;
#[cfg(not(target_os = "windows"))]
let zed_path = util::get_shell_safe_zed_path()?;
#[cfg(target_os = "windows")]
let zed_path = std::env::current_exe()
.context("finding current executable path for use in askpass")?;
let (askpass_kill_master_tx, askpass_kill_master_rx) = oneshot::channel::<()>();
let mut kill_tx = Some(askpass_kill_master_tx);
#[cfg(target_os = "windows")]
let askpass_secret = secret.clone();
let askpass_task = executor.spawn(async move {
let mut askpass_opened_tx = Some(askpass_opened_tx);
@@ -93,10 +101,14 @@ impl AskPassSession {
if let Some(password) = delegate
.ask_password(prompt.to_string())
.await
.context("failed to get askpass password")
.context("getting askpass password")
.log_err()
{
stream.write_all(password.as_bytes()).await.log_err();
#[cfg(target_os = "windows")]
{
*askpass_secret.lock() = password;
}
} else {
if let Some(kill_tx) = kill_tx.take() {
kill_tx.send(()).log_err();
@@ -112,34 +124,49 @@ impl AskPassSession {
});
// Create an askpass script that communicates back to this process.
let askpass_script = format!(
"{shebang}\n{print_args} | {zed_exe} --askpass={askpass_socket} 2> /dev/null \n",
zed_exe = zed_path,
askpass_socket = askpass_socket.display(),
print_args = "printf '%s\\0' \"$@\"",
shebang = "#!/bin/sh",
);
fs::write(&askpass_script_path, askpass_script).await?;
let askpass_script = generate_askpass_script(&zed_path, &askpass_socket);
fs::write(&askpass_script_path, askpass_script)
.await
.with_context(|| format!("creating askpass script at {askpass_script_path:?}"))?;
make_file_executable(&askpass_script_path).await?;
#[cfg(target_os = "windows")]
let askpass_helper = format!(
"powershell.exe -ExecutionPolicy Bypass -File {}",
askpass_script_path.display()
);
Ok(Self {
#[cfg(not(target_os = "windows"))]
script_path: askpass_script_path,
#[cfg(target_os = "windows")]
secret,
#[cfg(target_os = "windows")]
askpass_helper,
_askpass_task: askpass_task,
askpass_kill_master_rx: Some(askpass_kill_master_rx),
askpass_opened_rx: Some(askpass_opened_rx),
})
}
pub fn script_path(&self) -> &Path {
#[cfg(not(target_os = "windows"))]
pub fn script_path(&self) -> impl AsRef<OsStr> {
&self.script_path
}
#[cfg(target_os = "windows")]
pub fn script_path(&self) -> impl AsRef<OsStr> {
&self.askpass_helper
}
// This will run the askpass task forever, resolving as many authentication requests as needed.
// The caller is responsible for examining the result of their own commands and cancelling this
// future when this is no longer needed. Note that this can only be called once, but due to the
// drop order this takes an &mut, so you can `drop()` it after you're done with the master process.
pub async fn run(&mut self) -> AskPassResult {
let connection_timeout = Duration::from_secs(10);
// This is the default timeout setting used by VSCode.
let connection_timeout = Duration::from_secs(17);
let askpass_opened_rx = self.askpass_opened_rx.take().expect("Only call run once");
let askpass_kill_master_rx = self
.askpass_kill_master_rx
@@ -158,14 +185,19 @@ impl AskPassSession {
}
}
}
/// This will return the password that was last set by the askpass script.
#[cfg(target_os = "windows")]
pub fn get_password(&self) -> String {
self.secret.lock().clone()
}
}
/// The main function for when Zed is running in netcat mode for use in askpass.
/// Called from both the remote server binary and the zed binary in their respective main functions.
#[cfg(unix)]
pub fn main(socket: &str) {
use net::UnixStream;
use std::io::{self, Read, Write};
use std::os::unix::net::UnixStream;
use std::process::exit;
let mut stream = match UnixStream::connect(socket) {
@@ -182,6 +214,10 @@ pub fn main(socket: &str) {
exit(1);
}
#[cfg(target_os = "windows")]
while buffer.last().map_or(false, |&b| b == b'\n' || b == b'\r') {
buffer.pop();
}
if buffer.last() != Some(&b'\0') {
buffer.push(b'\0');
}
@@ -202,28 +238,28 @@ pub fn main(socket: &str) {
exit(1);
}
}
#[cfg(not(unix))]
pub fn main(_socket: &str) {}
#[cfg(not(unix))]
pub struct AskPassSession {
path: PathBuf,
#[inline]
#[cfg(not(target_os = "windows"))]
fn generate_askpass_script(zed_path: &str, askpass_socket: &std::path::Path) -> String {
format!(
"{shebang}\n{print_args} | {zed_exe} --askpass={askpass_socket} 2> /dev/null \n",
zed_exe = zed_path,
askpass_socket = askpass_socket.display(),
print_args = "printf '%s\\0' \"$@\"",
shebang = "#!/bin/sh",
)
}
#[cfg(not(unix))]
impl AskPassSession {
pub async fn new(_: &BackgroundExecutor, _: AskPassDelegate) -> anyhow::Result<Self> {
Ok(Self {
path: PathBuf::new(),
})
}
pub fn script_path(&self) -> &Path {
&self.path
}
pub async fn run(&mut self) -> AskPassResult {
futures::FutureExt::fuse(smol::Timer::after(Duration::from_secs(20))).await;
AskPassResult::Timedout
}
#[inline]
#[cfg(target_os = "windows")]
fn generate_askpass_script(zed_path: &std::path::Path, askpass_socket: &std::path::Path) -> String {
format!(
r#"
$ErrorActionPreference = 'Stop';
($args -join [char]0) | & "{zed_exe}" --askpass={askpass_socket} 2> $null
"#,
zed_exe = zed_path.display(),
askpass_socket = askpass_socket.display(),
)
}

View File

@@ -1,5 +1,6 @@
use anyhow::{Context as _, Result};
use buffer_diff::BufferDiff;
use clock;
use collections::BTreeMap;
use futures::{FutureExt, StreamExt, channel::mpsc};
use gpui::{App, AppContext, AsyncApp, Context, Entity, Subscription, Task, WeakEntity};
@@ -17,6 +18,8 @@ pub struct ActionLog {
edited_since_project_diagnostics_check: bool,
/// The project this action log is associated with
project: Entity<Project>,
/// Tracks which buffer versions have already been notified as changed externally
notified_versions: BTreeMap<Entity<Buffer>, clock::Global>,
}
impl ActionLog {
@@ -26,6 +29,7 @@ impl ActionLog {
tracked_buffers: BTreeMap::default(),
edited_since_project_diagnostics_check: false,
project,
notified_versions: BTreeMap::default(),
}
}
@@ -51,6 +55,7 @@ impl ActionLog {
) -> &mut TrackedBuffer {
let status = if is_created {
if let Some(tracked) = self.tracked_buffers.remove(&buffer) {
self.notified_versions.remove(&buffer);
match tracked.status {
TrackedBufferStatus::Created {
existing_file_content,
@@ -106,7 +111,7 @@ impl ActionLog {
TrackedBuffer {
buffer: buffer.clone(),
diff_base,
unreviewed_edits: unreviewed_edits,
unreviewed_edits,
snapshot: text_snapshot.clone(),
status,
version: buffer.read(cx).version(),
@@ -165,6 +170,7 @@ impl ActionLog {
// If the buffer had been edited by a tool, but it got
// deleted externally, we want to stop tracking it.
self.tracked_buffers.remove(&buffer);
self.notified_versions.remove(&buffer);
}
cx.notify();
}
@@ -178,6 +184,7 @@ impl ActionLog {
// resurrected externally, we want to clear the edits we
// were tracking and reset the buffer's state.
self.tracked_buffers.remove(&buffer);
self.notified_versions.remove(&buffer);
self.track_buffer_internal(buffer, false, cx);
}
cx.notify();
@@ -483,6 +490,7 @@ impl ActionLog {
match tracked_buffer.status {
TrackedBufferStatus::Created { .. } => {
self.tracked_buffers.remove(&buffer);
self.notified_versions.remove(&buffer);
cx.notify();
}
TrackedBufferStatus::Modified => {
@@ -508,6 +516,7 @@ impl ActionLog {
match tracked_buffer.status {
TrackedBufferStatus::Deleted => {
self.tracked_buffers.remove(&buffer);
self.notified_versions.remove(&buffer);
cx.notify();
}
_ => {
@@ -616,6 +625,7 @@ impl ActionLog {
};
self.tracked_buffers.remove(&buffer);
self.notified_versions.remove(&buffer);
cx.notify();
task
}
@@ -629,6 +639,7 @@ impl ActionLog {
// Clear all tracked edits for this buffer and start over as if we just read it.
self.tracked_buffers.remove(&buffer);
self.notified_versions.remove(&buffer);
self.buffer_read(buffer.clone(), cx);
cx.notify();
save
@@ -713,6 +724,33 @@ impl ActionLog {
.collect()
}
/// Returns stale buffers that haven't been notified yet
pub fn unnotified_stale_buffers<'a>(
&'a self,
cx: &'a App,
) -> impl Iterator<Item = &'a Entity<Buffer>> {
self.stale_buffers(cx).filter(|buffer| {
let buffer_entity = buffer.read(cx);
self.notified_versions
.get(buffer)
.map_or(true, |notified_version| {
*notified_version != buffer_entity.version
})
})
}
/// Marks the given buffers as notified at their current versions
pub fn mark_buffers_as_notified(
&mut self,
buffers: impl IntoIterator<Item = Entity<Buffer>>,
cx: &App,
) {
for buffer in buffers {
let version = buffer.read(cx).version.clone();
self.notified_versions.insert(buffer, version);
}
}
/// Iterate over buffers changed since last read or edited by the model
pub fn stale_buffers<'a>(&'a self, cx: &'a App) -> impl Iterator<Item = &'a Entity<Buffer>> {
self.tracked_buffers

View File

@@ -25,10 +25,15 @@ fn preprocess_json_schema(json: &mut Value) -> Result<()> {
// `additionalProperties` defaults to `false` unless explicitly specified.
// This prevents models from hallucinating tool parameters.
if let Value::Object(obj) = json {
if let Some(Value::String(type_str)) = obj.get("type") {
if type_str == "object" && !obj.contains_key("additionalProperties") {
if matches!(obj.get("type"), Some(Value::String(s)) if s == "object") {
if !obj.contains_key("additionalProperties") {
obj.insert("additionalProperties".to_string(), Value::Bool(false));
}
// OpenAI API requires non-missing `properties`
if !obj.contains_key("properties") {
obj.insert("properties".to_string(), Value::Object(Default::default()));
}
}
}
Ok(())

View File

@@ -11,6 +11,7 @@ mod list_directory_tool;
mod move_path_tool;
mod now_tool;
mod open_tool;
mod project_notifications_tool;
mod read_file_tool;
mod schema;
mod templates;
@@ -45,6 +46,7 @@ pub use edit_file_tool::{EditFileMode, EditFileToolInput};
pub use find_path_tool::FindPathToolInput;
pub use grep_tool::{GrepTool, GrepToolInput};
pub use open_tool::OpenTool;
pub use project_notifications_tool::ProjectNotificationsTool;
pub use read_file_tool::{ReadFileTool, ReadFileToolInput};
pub use terminal_tool::TerminalTool;
@@ -61,6 +63,7 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
registry.register_tool(ListDirectoryTool);
registry.register_tool(NowTool);
registry.register_tool(OpenTool);
registry.register_tool(ProjectNotificationsTool);
registry.register_tool(FindPathTool);
registry.register_tool(ReadFileTool);
registry.register_tool(GrepTool);

View File

@@ -4,6 +4,7 @@ use crate::{
schema::json_schema_for,
ui::{COLLAPSED_LINES, ToolOutputPreview},
};
use agent_settings;
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{
ActionLog, AnyToolCard, Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput,
@@ -14,7 +15,7 @@ use editor::{Editor, EditorMode, MinimapVisibility, MultiBuffer, PathKey};
use futures::StreamExt;
use gpui::{
Animation, AnimationExt, AnyWindowHandle, App, AppContext, AsyncApp, Entity, Task,
TextStyleRefinement, WeakEntity, pulsating_between, px,
TextStyleRefinement, Transformation, WeakEntity, percentage, pulsating_between, px,
};
use indoc::formatdoc;
use language::{
@@ -515,7 +516,9 @@ pub struct EditFileToolCard {
impl EditFileToolCard {
pub fn new(path: PathBuf, project: Entity<Project>, window: &mut Window, cx: &mut App) -> Self {
let expand_edit_card = agent_settings::AgentSettings::get_global(cx).expand_edit_card;
let multibuffer = cx.new(|_| MultiBuffer::without_headers(Capability::ReadOnly));
let editor = cx.new(|cx| {
let mut editor = Editor::new(
EditorMode::Full {
@@ -556,7 +559,7 @@ impl EditFileToolCard {
diff_task: None,
preview_expanded: true,
error_expanded: None,
full_height_expanded: true,
full_height_expanded: expand_edit_card,
total_lines: None,
}
}
@@ -755,6 +758,13 @@ impl ToolCard for EditFileToolCard {
_ => None,
};
let running_or_pending = match status {
ToolUseStatus::Running | ToolUseStatus::Pending => Some(()),
_ => None,
};
let should_show_loading = running_or_pending.is_some() && !self.full_height_expanded;
let path_label_button = h_flex()
.id(("edit-tool-path-label-button", self.editor.entity_id()))
.w_full()
@@ -863,6 +873,18 @@ impl ToolCard for EditFileToolCard {
header.bg(codeblock_header_bg)
})
.child(path_label_button)
.when(should_show_loading, |header| {
header.pr_1p5().child(
Icon::new(IconName::ArrowCircle)
.size(IconSize::XSmall)
.color(Color::Info)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
),
)
})
.when_some(error_message, |header, error_message| {
header.child(
h_flex()

View File

@@ -0,0 +1,224 @@
use crate::schema::json_schema_for;
use anyhow::Result;
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{AnyWindowHandle, App, Entity, Task};
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::fmt::Write as _;
use std::sync::Arc;
use ui::IconName;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct ProjectUpdatesToolInput {}
pub struct ProjectNotificationsTool;
impl Tool for ProjectNotificationsTool {
fn name(&self) -> String {
"project_notifications".to_string()
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
false
}
fn may_perform_edits(&self) -> bool {
false
}
fn description(&self) -> String {
include_str!("./project_notifications_tool/description.md").to_string()
}
fn icon(&self) -> IconName {
IconName::Envelope
}
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
json_schema_for::<ProjectUpdatesToolInput>(format)
}
fn ui_text(&self, _input: &serde_json::Value) -> String {
"Check project notifications".into()
}
fn run(
self: Arc<Self>,
_input: serde_json::Value,
_request: Arc<LanguageModelRequest>,
_project: Entity<Project>,
action_log: Entity<ActionLog>,
_model: Arc<dyn LanguageModel>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let mut stale_files = String::new();
let mut notified_buffers = Vec::new();
for stale_file in action_log.read(cx).unnotified_stale_buffers(cx) {
if let Some(file) = stale_file.read(cx).file() {
writeln!(&mut stale_files, "- {}", file.path().display()).ok();
notified_buffers.push(stale_file.clone());
}
}
if !notified_buffers.is_empty() {
action_log.update(cx, |log, cx| {
log.mark_buffers_as_notified(notified_buffers, cx);
});
}
let response = if stale_files.is_empty() {
"No new notifications".to_string()
} else {
// NOTE: Changes to this prompt require a symmetric update in the LLM Worker
const HEADER: &str = include_str!("./project_notifications_tool/prompt_header.txt");
format!("{HEADER}{stale_files}").replace("\r\n", "\n")
};
Task::ready(Ok(response.into())).into()
}
}
#[cfg(test)]
mod tests {
use super::*;
use assistant_tool::ToolResultContent;
use gpui::{AppContext, TestAppContext};
use language_model::{LanguageModelRequest, fake_provider::FakeLanguageModelProvider};
use project::{FakeFs, Project};
use serde_json::json;
use settings::SettingsStore;
use std::sync::Arc;
use util::path;
#[gpui::test]
async fn test_stale_buffer_notification(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
path!("/test"),
json!({"code.rs": "fn main() {\n println!(\"Hello, world!\");\n}"}),
)
.await;
let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let buffer_path = project
.read_with(cx, |project, cx| {
project.find_project_path("test/code.rs", cx)
})
.unwrap();
let buffer = project
.update(cx, |project, cx| {
project.open_buffer(buffer_path.clone(), cx)
})
.await
.unwrap();
// Start tracking the buffer
action_log.update(cx, |log, cx| {
log.buffer_read(buffer.clone(), cx);
});
// Run the tool before any changes
let tool = Arc::new(ProjectNotificationsTool);
let provider = Arc::new(FakeLanguageModelProvider);
let model: Arc<dyn LanguageModel> = Arc::new(provider.test_model());
let request = Arc::new(LanguageModelRequest::default());
let tool_input = json!({});
let result = cx.update(|cx| {
tool.clone().run(
tool_input.clone(),
request.clone(),
project.clone(),
action_log.clone(),
model.clone(),
None,
cx,
)
});
let response = result.output.await.unwrap();
let response_text = match &response.content {
ToolResultContent::Text(text) => text.clone(),
_ => panic!("Expected text response"),
};
assert_eq!(
response_text.as_str(),
"No new notifications",
"Tool should return 'No new notifications' when no stale buffers"
);
// Modify the buffer (makes it stale)
buffer.update(cx, |buffer, cx| {
buffer.edit([(1..1, "\nChange!\n")], None, cx);
});
// Run the tool again
let result = cx.update(|cx| {
tool.clone().run(
tool_input.clone(),
request.clone(),
project.clone(),
action_log.clone(),
model.clone(),
None,
cx,
)
});
// This time the buffer is stale, so the tool should return a notification
let response = result.output.await.unwrap();
let response_text = match &response.content {
ToolResultContent::Text(text) => text.clone(),
_ => panic!("Expected text response"),
};
let expected_content = "[The following is an auto-generated notification; do not reply]\n\nThese files have changed since the last read:\n- code.rs\n";
assert_eq!(
response_text.as_str(),
expected_content,
"Tool should return the stale buffer notification"
);
// Run the tool once more without any changes - should get no new notifications
let result = cx.update(|cx| {
tool.run(
tool_input.clone(),
request.clone(),
project.clone(),
action_log,
model.clone(),
None,
cx,
)
});
let response = result.output.await.unwrap();
let response_text = match &response.content {
ToolResultContent::Text(text) => text.clone(),
_ => panic!("Expected text response"),
};
assert_eq!(
response_text.as_str(),
"No new notifications",
"Tool should return 'No new notifications' when running again without changes"
);
}
fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
language::init(cx);
Project::init_settings(cx);
assistant_tool::init(cx);
});
}
}

View File

@@ -0,0 +1,3 @@
This tool reports which files have been modified by the user since the agent last accessed them.
It serves as a notification mechanism to inform the agent of recent changes. No immediate action is required in response to these updates.

View File

@@ -0,0 +1,3 @@
[The following is an auto-generated notification; do not reply]
These files have changed since the last read:

View File

@@ -2,12 +2,13 @@ use crate::{
schema::json_schema_for,
ui::{COLLAPSED_LINES, ToolOutputPreview},
};
use agent_settings;
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolCard, ToolResult, ToolUseStatus};
use futures::{FutureExt as _, future::Shared};
use gpui::{
AnyWindowHandle, App, AppContext, Empty, Entity, EntityId, Task, TextStyleRefinement,
WeakEntity, Window,
Animation, AnimationExt, AnyWindowHandle, App, AppContext, Empty, Entity, EntityId, Task,
TextStyleRefinement, Transformation, WeakEntity, Window, percentage,
};
use language::LineEnding;
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
@@ -218,7 +219,7 @@ impl Tool for TerminalTool {
.update(cx, |project, cx| {
project.create_terminal(
TerminalKind::Task(task::SpawnInTerminal {
command: program,
command: Some(program),
args,
cwd,
env,
@@ -247,6 +248,7 @@ impl Tool for TerminalTool {
command_markdown.clone(),
working_dir.clone(),
cx.entity_id(),
cx,
)
});
@@ -441,7 +443,10 @@ impl TerminalToolCard {
input_command: Entity<Markdown>,
working_dir: Option<PathBuf>,
entity_id: EntityId,
cx: &mut Context<Self>,
) -> Self {
let expand_terminal_card =
agent_settings::AgentSettings::get_global(cx).expand_terminal_card;
Self {
input_command,
working_dir,
@@ -453,7 +458,7 @@ impl TerminalToolCard {
finished_with_empty_output: false,
original_content_len: 0,
content_line_count: 0,
preview_expanded: true,
preview_expanded: expand_terminal_card,
start_instant: Instant::now(),
elapsed_time: None,
}
@@ -518,6 +523,46 @@ impl ToolCard for TerminalToolCard {
.color(Color::Muted),
),
)
.when(!self.command_finished, |header| {
header.child(
Icon::new(IconName::ArrowCircle)
.size(IconSize::XSmall)
.color(Color::Info)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
),
)
})
.when(tool_failed || command_failed, |header| {
header.child(
div()
.id(("terminal-tool-error-code-indicator", self.entity_id))
.child(
Icon::new(IconName::Close)
.size(IconSize::Small)
.color(Color::Error),
)
.when(command_failed && self.exit_status.is_some(), |this| {
this.tooltip(Tooltip::text(format!(
"Exited with code {}",
self.exit_status
.and_then(|status| status.code())
.unwrap_or(-1),
)))
})
.when(
!command_failed && tool_failed && status.error().is_some(),
|this| {
this.tooltip(Tooltip::text(format!(
"Error: {}",
status.error().unwrap(),
)))
},
),
)
})
.when(self.was_content_truncated, |header| {
let tooltip = if self.content_line_count + 10 > terminal::MAX_SCROLL_HISTORY_LINES {
"Output exceeded terminal max lines and was \
@@ -555,34 +600,6 @@ impl ToolCard for TerminalToolCard {
.size(LabelSize::Small),
)
})
.when(tool_failed || command_failed, |header| {
header.child(
div()
.id(("terminal-tool-error-code-indicator", self.entity_id))
.child(
Icon::new(IconName::Close)
.size(IconSize::Small)
.color(Color::Error),
)
.when(command_failed && self.exit_status.is_some(), |this| {
this.tooltip(Tooltip::text(format!(
"Exited with code {}",
self.exit_status
.and_then(|status| status.code())
.unwrap_or(-1),
)))
})
.when(
!command_failed && tool_failed && status.error().is_some(),
|this| {
this.tooltip(Tooltip::text(format!(
"Error: {}",
status.error().unwrap(),
)))
},
),
)
})
.when(!self.finished_with_empty_output, |header| {
header.child(
Disclosure::new(
@@ -634,6 +651,7 @@ impl ToolCard for TerminalToolCard {
div()
.pt_2()
.border_t_1()
.when(tool_failed || command_failed, |card| card.border_dashed())
.border_color(border_color)
.bg(cx.theme().colors().editor_background)
.rounded_b_md()

View File

@@ -26,7 +26,7 @@ CREATE UNIQUE INDEX "index_users_on_github_user_id" ON "users" ("github_user_id"
CREATE TABLE "access_tokens" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
"user_id" INTEGER REFERENCES users (id),
"user_id" INTEGER REFERENCES users (id) ON DELETE CASCADE,
"impersonated_user_id" INTEGER REFERENCES users (id),
"hash" VARCHAR(128)
);

View File

@@ -0,0 +1,3 @@
ALTER TABLE access_tokens DROP CONSTRAINT access_tokens_user_id_fkey;
ALTER TABLE access_tokens ADD CONSTRAINT access_tokens_user_id_fkey
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;

View File

@@ -44,3 +44,53 @@ async fn test_accepted_tos(db: &Arc<Database>) {
let user = db.get_user_by_id(user_id).await.unwrap().unwrap();
assert!(user.accepted_tos_at.is_none());
}
test_both_dbs!(
test_destroy_user_cascade_deletes_access_tokens,
test_destroy_user_cascade_deletes_access_tokens_postgres,
test_destroy_user_cascade_deletes_access_tokens_sqlite
);
async fn test_destroy_user_cascade_deletes_access_tokens(db: &Arc<Database>) {
let user_id = db
.create_user(
"user1@example.com",
Some("user1"),
false,
NewUserParams {
github_login: "user1".to_string(),
github_user_id: 12345,
},
)
.await
.unwrap()
.user_id;
let user = db.get_user_by_id(user_id).await.unwrap();
assert!(user.is_some());
let token_1_id = db
.create_access_token(user_id, None, "token-1", 10)
.await
.unwrap();
let token_2_id = db
.create_access_token(user_id, None, "token-2", 10)
.await
.unwrap();
let token_1 = db.get_access_token(token_1_id).await;
let token_2 = db.get_access_token(token_2_id).await;
assert!(token_1.is_ok());
assert!(token_2.is_ok());
db.destroy_user(user_id).await.unwrap();
let user = db.get_user_by_id(user_id).await.unwrap();
assert!(user.is_none());
let token_1 = db.get_access_token(token_1_id).await;
let token_2 = db.get_access_token(token_2_id).await;
assert!(token_1.is_err());
assert!(token_2.is_err());
}

View File

@@ -528,6 +528,7 @@ impl CopilotChat {
pub async fn stream_completion(
request: Request,
is_user_initiated: bool,
mut cx: AsyncApp,
) -> Result<BoxStream<'static, Result<ResponseEvent>>> {
let this = cx
@@ -562,7 +563,14 @@ impl CopilotChat {
};
let api_url = configuration.api_url_from_endpoint(&token.api_endpoint);
stream_completion(client.clone(), token.api_key, api_url.into(), request).await
stream_completion(
client.clone(),
token.api_key,
api_url.into(),
request,
is_user_initiated,
)
.await
}
pub fn set_configuration(
@@ -697,6 +705,7 @@ async fn stream_completion(
api_key: String,
completion_url: Arc<str>,
request: Request,
is_user_initiated: bool,
) -> Result<BoxStream<'static, Result<ResponseEvent>>> {
let is_vision_request = request.messages.iter().any(|message| match message {
ChatMessage::User { content }
@@ -707,6 +716,8 @@ async fn stream_completion(
_ => false,
});
let request_initiator = if is_user_initiated { "user" } else { "agent" };
let mut request_builder = HttpRequest::builder()
.method(Method::POST)
.uri(completion_url.as_ref())
@@ -719,7 +730,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("X-Initiator", request_initiator);
if is_vision_request {
request_builder =

View File

@@ -2,7 +2,7 @@ use crate::{
adapters::DebugAdapterBinary,
transport::{IoKind, LogKind, TransportDelegate},
};
use anyhow::Result;
use anyhow::{Context as _, Result};
use dap_types::{
messages::{Message, Response},
requests::Request,
@@ -108,7 +108,11 @@ impl DebugAdapterClient {
arguments: Some(serialized_arguments),
};
self.transport_delegate
.add_pending_request(sequence_id, callback_tx);
.pending_requests
.lock()
.as_mut()
.context("client is closed")?
.insert(sequence_id, callback_tx);
log::debug!(
"Client {} send `{}` request with sequence_id: {}",

View File

@@ -49,7 +49,6 @@ pub enum IoKind {
StdErr,
}
type Requests = Arc<Mutex<HashMap<u64, oneshot::Sender<Result<Response>>>>>;
type LogHandlers = Arc<Mutex<SmallVec<[(LogKind, IoHandler); 2]>>>;
pub trait Transport: Send + Sync {
@@ -93,18 +92,14 @@ async fn start(
pub(crate) struct TransportDelegate {
log_handlers: LogHandlers,
pub(crate) pending_requests: Requests,
// TODO this should really be some kind of associative channel
pub(crate) pending_requests:
Arc<Mutex<Option<HashMap<u64, oneshot::Sender<Result<Response>>>>>>,
pub(crate) transport: Mutex<Box<dyn Transport>>,
pub(crate) server_tx: smol::lock::Mutex<Option<Sender<Message>>>,
tasks: Mutex<Vec<Task<()>>>,
}
impl Drop for TransportDelegate {
fn drop(&mut self) {
self.transport.lock().kill()
}
}
impl TransportDelegate {
pub(crate) async fn start(binary: &DebugAdapterBinary, cx: &mut AsyncApp) -> Result<Self> {
let log_handlers: LogHandlers = Default::default();
@@ -113,7 +108,7 @@ impl TransportDelegate {
transport: Mutex::new(transport),
log_handlers,
server_tx: Default::default(),
pending_requests: Default::default(),
pending_requests: Arc::new(Mutex::new(Some(HashMap::default()))),
tasks: Default::default(),
})
}
@@ -154,16 +149,26 @@ impl TransportDelegate {
.await
{
Ok(()) => {
pending_requests.lock().drain().for_each(|(_, request)| {
request
.send(Err(anyhow!("debugger shutdown unexpectedly")))
.ok();
});
pending_requests
.lock()
.take()
.into_iter()
.flatten()
.for_each(|(_, request)| {
request
.send(Err(anyhow!("debugger shutdown unexpectedly")))
.ok();
});
}
Err(e) => {
pending_requests.lock().drain().for_each(|(_, request)| {
request.send(Err(e.cloned())).ok();
});
pending_requests
.lock()
.take()
.into_iter()
.flatten()
.for_each(|(_, request)| {
request.send(Err(e.cloned())).ok();
});
}
}
}));
@@ -188,15 +193,6 @@ impl TransportDelegate {
self.transport.lock().tcp_arguments()
}
pub(crate) fn add_pending_request(
&self,
sequence_id: u64,
request: oneshot::Sender<Result<Response>>,
) {
let mut pending_requests = self.pending_requests.lock();
pending_requests.insert(sequence_id, request);
}
pub(crate) async fn send_message(&self, message: Message) -> Result<()> {
if let Some(server_tx) = self.server_tx.lock().await.as_ref() {
server_tx.send(message).await.context("sending message")
@@ -290,7 +286,7 @@ impl TransportDelegate {
async fn recv_from_server<Stdout>(
server_stdout: Stdout,
mut message_handler: DapMessageHandler,
pending_requests: Requests,
pending_requests: Arc<Mutex<Option<HashMap<u64, oneshot::Sender<Result<Response>>>>>>,
log_handlers: Option<LogHandlers>,
) -> Result<()>
where
@@ -300,16 +296,21 @@ impl TransportDelegate {
let mut reader = BufReader::new(server_stdout);
let result = loop {
match Self::receive_server_message(&mut reader, &mut recv_buffer, log_handlers.as_ref())
.await
{
let result =
Self::receive_server_message(&mut reader, &mut recv_buffer, log_handlers.as_ref())
.await;
match result {
ConnectionResult::Timeout => anyhow::bail!("Timed out when connecting to debugger"),
ConnectionResult::ConnectionReset => {
log::info!("Debugger closed the connection");
return Ok(());
break Ok(());
}
ConnectionResult::Result(Ok(Message::Response(res))) => {
let tx = pending_requests.lock().remove(&res.request_seq);
let tx = pending_requests
.lock()
.as_mut()
.context("client is closed")?
.remove(&res.request_seq);
if let Some(tx) = tx {
if let Err(e) = tx.send(Self::process_response(res)) {
log::trace!("Did not send response `{:?}` for a cancelled", e);

View File

@@ -2,7 +2,6 @@ mod codelldb;
mod gdb;
mod go;
mod javascript;
mod php;
mod python;
use std::sync::Arc;
@@ -22,7 +21,6 @@ use gdb::GdbDebugAdapter;
use go::GoDebugAdapter;
use gpui::{App, BorrowAppContext};
use javascript::JsDebugAdapter;
use php::PhpDebugAdapter;
use python::PythonDebugAdapter;
use serde_json::json;
use task::{DebugScenario, ZedDebugConfig};
@@ -31,7 +29,6 @@ pub fn init(cx: &mut App) {
cx.update_default_global(|registry: &mut DapRegistry, _cx| {
registry.add_adapter(Arc::from(CodeLldbDebugAdapter::default()));
registry.add_adapter(Arc::from(PythonDebugAdapter::default()));
registry.add_adapter(Arc::from(PhpDebugAdapter::default()));
registry.add_adapter(Arc::from(JsDebugAdapter::default()));
registry.add_adapter(Arc::from(GoDebugAdapter::default()));
registry.add_adapter(Arc::from(GdbDebugAdapter));

View File

@@ -1,9 +1,10 @@
use adapters::latest_github_release;
use anyhow::Context as _;
use collections::HashMap;
use dap::{StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
use gpui::AsyncApp;
use serde_json::Value;
use std::{collections::HashMap, path::PathBuf, sync::OnceLock};
use std::{path::PathBuf, sync::OnceLock};
use task::DebugRequest;
use util::{ResultExt, maybe};
@@ -70,6 +71,8 @@ impl JsDebugAdapter {
let tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default();
let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
let mut envs = HashMap::default();
let mut configuration = task_definition.config.clone();
if let Some(configuration) = configuration.as_object_mut() {
maybe!({
@@ -110,6 +113,12 @@ impl JsDebugAdapter {
}
}
if let Some(env) = configuration.get("env").cloned() {
if let Ok(env) = serde_json::from_value(env) {
envs = env;
}
}
configuration
.entry("cwd")
.or_insert(delegate.worktree_root_path().to_string_lossy().into());
@@ -158,7 +167,7 @@ impl JsDebugAdapter {
),
arguments,
cwd: Some(delegate.worktree_root_path().to_path_buf()),
envs: HashMap::default(),
envs,
connection: Some(adapters::TcpArguments {
host,
port,
@@ -245,7 +254,7 @@ impl DebugAdapter for JsDebugAdapter {
"properties": {
"type": {
"type": "string",
"enum": ["pwa-node", "node", "chrome", "pwa-chrome", "msedge", "pwa-msedge"],
"enum": ["pwa-node", "node", "chrome", "pwa-chrome", "msedge", "pwa-msedge", "node-terminal"],
"description": "The type of debug session",
"default": "pwa-node"
},
@@ -379,10 +388,6 @@ impl DebugAdapter for JsDebugAdapter {
}
}
},
"oneOf": [
{ "required": ["program"] },
{ "required": ["url"] }
]
}
]
},

View File

@@ -1,368 +0,0 @@
use adapters::latest_github_release;
use anyhow::Context as _;
use anyhow::bail;
use dap::StartDebuggingRequestArguments;
use dap::StartDebuggingRequestArgumentsRequest;
use dap::adapters::{DebugTaskDefinition, TcpArguments};
use gpui::{AsyncApp, SharedString};
use language::LanguageName;
use std::{collections::HashMap, path::PathBuf, sync::OnceLock};
use util::ResultExt;
use crate::*;
#[derive(Default)]
pub(crate) struct PhpDebugAdapter {
checked: OnceLock<()>,
}
impl PhpDebugAdapter {
const ADAPTER_NAME: &'static str = "PHP";
const ADAPTER_PACKAGE_NAME: &'static str = "vscode-php-debug";
const ADAPTER_PATH: &'static str = "extension/out/phpDebug.js";
async fn fetch_latest_adapter_version(
&self,
delegate: &Arc<dyn DapDelegate>,
) -> Result<AdapterVersion> {
let release = latest_github_release(
&format!("{}/{}", "xdebug", Self::ADAPTER_PACKAGE_NAME),
true,
false,
delegate.http_client(),
)
.await?;
let asset_name = format!("php-debug-{}.vsix", release.tag_name.replace("v", ""));
Ok(AdapterVersion {
tag_name: release.tag_name,
url: release
.assets
.iter()
.find(|asset| asset.name == asset_name)
.with_context(|| format!("no asset found matching {asset_name:?}"))?
.browser_download_url
.clone(),
})
}
async fn get_installed_binary(
&self,
delegate: &Arc<dyn DapDelegate>,
task_definition: &DebugTaskDefinition,
user_installed_path: Option<PathBuf>,
user_args: Option<Vec<String>>,
_: &mut AsyncApp,
) -> Result<DebugAdapterBinary> {
let adapter_path = if let Some(user_installed_path) = user_installed_path {
user_installed_path
} else {
let adapter_path = paths::debug_adapters_dir().join(self.name().as_ref());
let file_name_prefix = format!("{}_", self.name());
util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| {
file_name.starts_with(&file_name_prefix)
})
.await
.context("Couldn't find PHP dap directory")?
};
let tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default();
let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
let mut configuration = task_definition.config.clone();
if let Some(obj) = configuration.as_object_mut() {
obj.entry("cwd")
.or_insert_with(|| delegate.worktree_root_path().to_string_lossy().into());
}
let arguments = if let Some(mut args) = user_args {
args.insert(
0,
adapter_path
.join(Self::ADAPTER_PATH)
.to_string_lossy()
.to_string(),
);
args
} else {
vec![
adapter_path
.join(Self::ADAPTER_PATH)
.to_string_lossy()
.to_string(),
format!("--server={}", port),
]
};
Ok(DebugAdapterBinary {
command: Some(
delegate
.node_runtime()
.binary_path()
.await?
.to_string_lossy()
.into_owned(),
),
arguments,
connection: Some(TcpArguments {
port,
host,
timeout,
}),
cwd: Some(delegate.worktree_root_path().to_path_buf()),
envs: HashMap::default(),
request_args: StartDebuggingRequestArguments {
configuration,
request: <Self as DebugAdapter>::request_kind(self, &task_definition.config)
.await?,
},
})
}
}
#[async_trait(?Send)]
impl DebugAdapter for PhpDebugAdapter {
fn dap_schema(&self) -> serde_json::Value {
json!({
"properties": {
"request": {
"type": "string",
"enum": ["launch"],
"description": "The request type for the PHP debug adapter, always \"launch\"",
"default": "launch"
},
"hostname": {
"type": "string",
"description": "The address to bind to when listening for Xdebug (default: all IPv6 connections if available, else all IPv4 connections) or Unix Domain socket (prefix with unix://) or Windows Pipe (\\\\?\\pipe\\name) - cannot be combined with port"
},
"port": {
"type": "integer",
"description": "The port on which to listen for Xdebug (default: 9003). If port is set to 0 a random port is chosen by the system and a placeholder ${port} is replaced with the chosen port in env and runtimeArgs.",
"default": 9003
},
"program": {
"type": "string",
"description": "The PHP script to debug (typically a path to a file)",
"default": "${file}"
},
"cwd": {
"type": "string",
"description": "Working directory for the debugged program"
},
"args": {
"type": "array",
"items": {
"type": "string"
},
"description": "Command line arguments to pass to the program"
},
"env": {
"type": "object",
"description": "Environment variables to pass to the program",
"additionalProperties": {
"type": "string"
}
},
"stopOnEntry": {
"type": "boolean",
"description": "Whether to break at the beginning of the script",
"default": false
},
"pathMappings": {
"type": "object",
"description": "A mapping of server paths to local paths.",
},
"log": {
"type": "boolean",
"description": "Whether to log all communication between editor and the adapter to the debug console",
"default": false
},
"ignore": {
"type": "array",
"description": "An array of glob patterns that errors should be ignored from (for example **/vendor/**/*.php)",
"items": {
"type": "string"
}
},
"ignoreExceptions": {
"type": "array",
"description": "An array of exception class names that should be ignored (for example BaseException, \\NS1\\Exception, \\*\\Exception or \\**\\Exception*)",
"items": {
"type": "string"
}
},
"skipFiles": {
"type": "array",
"description": "An array of glob patterns to skip when debugging. Star patterns and negations are allowed.",
"items": {
"type": "string"
}
},
"skipEntryPaths": {
"type": "array",
"description": "An array of glob patterns to immediately detach from and ignore for debugging if the entry script matches",
"items": {
"type": "string"
}
},
"maxConnections": {
"type": "integer",
"description": "Accept only this number of parallel debugging sessions. Additional connections will be dropped.",
"default": 1
},
"proxy": {
"type": "object",
"description": "DBGp Proxy settings",
"properties": {
"enable": {
"type": "boolean",
"description": "To enable proxy registration",
"default": false
},
"host": {
"type": "string",
"description": "The address of the proxy. Supports host name, IP address, or Unix domain socket.",
"default": "127.0.0.1"
},
"port": {
"type": "integer",
"description": "The port where the adapter will register with the proxy",
"default": 9001
},
"key": {
"type": "string",
"description": "A unique key that allows the proxy to match requests to your editor",
"default": "vsc"
},
"timeout": {
"type": "integer",
"description": "The number of milliseconds to wait before giving up on the connection to proxy",
"default": 3000
},
"allowMultipleSessions": {
"type": "boolean",
"description": "If the proxy should forward multiple sessions/connections at the same time or not",
"default": true
}
}
},
"xdebugSettings": {
"type": "object",
"description": "Allows you to override Xdebug's remote debugging settings to fine tune Xdebug to your needs",
"properties": {
"max_children": {
"type": "integer",
"description": "Max number of array or object children to initially retrieve"
},
"max_data": {
"type": "integer",
"description": "Max amount of variable data to initially retrieve"
},
"max_depth": {
"type": "integer",
"description": "Maximum depth that the debugger engine may return when sending arrays, hashes or object structures to the IDE"
},
"show_hidden": {
"type": "integer",
"description": "Whether to show detailed internal information on properties (e.g. private members of classes). Zero means hidden members are not shown.",
"enum": [0, 1]
},
"breakpoint_include_return_value": {
"type": "boolean",
"description": "Determines whether to enable an additional \"return from function\" debugging step, allowing inspection of the return value when a function call returns"
}
}
},
"xdebugCloudToken": {
"type": "string",
"description": "Instead of listening locally, open a connection and register with Xdebug Cloud and accept debugging sessions on that connection"
},
"stream": {
"type": "object",
"description": "Allows to influence DBGp streams. Xdebug only supports stdout",
"properties": {
"stdout": {
"type": "integer",
"description": "Redirect stdout stream: 0 (disable), 1 (copy), 2 (redirect)",
"enum": [0, 1, 2],
"default": 0
}
}
}
},
"required": ["request", "program"]
})
}
fn name(&self) -> DebugAdapterName {
DebugAdapterName(Self::ADAPTER_NAME.into())
}
fn adapter_language_name(&self) -> Option<LanguageName> {
Some(SharedString::new_static("PHP").into())
}
async fn request_kind(
&self,
_: &serde_json::Value,
) -> Result<StartDebuggingRequestArgumentsRequest> {
Ok(StartDebuggingRequestArgumentsRequest::Launch)
}
async fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
let obj = match &zed_scenario.request {
dap::DebugRequest::Attach(_) => {
bail!("Php adapter doesn't support attaching")
}
dap::DebugRequest::Launch(launch_config) => json!({
"program": launch_config.program,
"cwd": launch_config.cwd,
"args": launch_config.args,
"env": launch_config.env_json(),
"stopOnEntry": zed_scenario.stop_on_entry.unwrap_or_default(),
}),
};
Ok(DebugScenario {
adapter: zed_scenario.adapter,
label: zed_scenario.label,
build: None,
config: obj,
tcp_connection: None,
})
}
async fn get_binary(
&self,
delegate: &Arc<dyn DapDelegate>,
task_definition: &DebugTaskDefinition,
user_installed_path: Option<PathBuf>,
user_args: Option<Vec<String>>,
cx: &mut AsyncApp,
) -> Result<DebugAdapterBinary> {
if self.checked.set(()).is_ok() {
delegate.output_to_console(format!("Checking latest version of {}...", self.name()));
if let Some(version) = self.fetch_latest_adapter_version(delegate).await.log_err() {
adapters::download_adapter_from_github(
self.name(),
version,
adapters::DownloadedFileType::Vsix,
delegate.as_ref(),
)
.await?;
}
}
self.get_installed_binary(
delegate,
&task_definition,
user_installed_path,
user_args,
cx,
)
.await
}
}

View File

@@ -44,7 +44,9 @@ impl DapLocator for ExtensionLocatorAdapter {
.flatten()
}
async fn run(&self, _build_config: SpawnInTerminal) -> Result<DebugRequest> {
Err(anyhow::anyhow!("Not implemented"))
async fn run(&self, build_config: SpawnInTerminal) -> Result<DebugRequest> {
self.extension
.run_dap_locator(self.locator_name.as_ref().to_owned(), build_config)
.await
}
}

View File

@@ -40,6 +40,7 @@ file_icons.workspace = true
futures.workspace = true
fuzzy.workspace = true
gpui.workspace = true
indoc.workspace = true
itertools.workspace = true
language.workspace = true
log.workspace = true
@@ -60,6 +61,7 @@ task.workspace = true
tasks_ui.workspace = true
telemetry.workspace = true
terminal_view.workspace = true
text.workspace = true
theme.workspace = true
tree-sitter.workspace = true
tree-sitter-json.workspace = true

View File

@@ -206,7 +206,7 @@ impl PickerDelegate for AttachModalDelegate {
})
}
fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
let candidate = self
.matches
.get(self.selected_index())
@@ -229,30 +229,44 @@ impl PickerDelegate for AttachModalDelegate {
}
}
let workspace = self.workspace.clone();
let Some(panel) = workspace
.update(cx, |workspace, cx| workspace.panel::<DebugPanel>(cx))
.ok()
.flatten()
else {
return;
};
if secondary {
// let Some(id) = worktree_id else { return };
// cx.spawn_in(window, async move |_, cx| {
// panel
// .update_in(cx, |debug_panel, window, cx| {
// debug_panel.save_scenario(&debug_scenario, id, window, cx)
// })?
// .await?;
// anyhow::Ok(())
// })
// .detach_and_log_err(cx);
}
let Some(adapter) = cx.read_global::<DapRegistry, _>(|registry, _| {
registry.adapter(&self.definition.adapter)
}) else {
return;
};
let workspace = self.workspace.clone();
let definition = self.definition.clone();
cx.spawn_in(window, async move |this, cx| {
let Ok(scenario) = adapter.config_from_zed_format(definition).await else {
return;
};
let panel = workspace
.update(cx, |workspace, cx| workspace.panel::<DebugPanel>(cx))
.ok()
.flatten();
if let Some(panel) = panel {
panel
.update_in(cx, |panel, window, cx| {
panel.start_session(scenario, Default::default(), None, None, window, cx);
})
.ok();
}
panel
.update_in(cx, |panel, window, cx| {
panel.start_session(scenario, Default::default(), None, None, window, cx);
})
.ok();
this.update(cx, |_, cx| {
cx.emit(DismissEvent);
})

View File

@@ -16,16 +16,18 @@ use dap::{
client::SessionId, debugger_settings::DebuggerSettings,
};
use dap::{DapRegistry, StartDebuggingRequestArguments};
use editor::Editor;
use gpui::{
Action, App, AsyncWindowContext, ClipboardItem, Context, DismissEvent, Entity, EntityId,
EventEmitter, FocusHandle, Focusable, MouseButton, MouseDownEvent, Point, Subscription, Task,
WeakEntity, anchored, deferred,
};
use text::ToPoint as _;
use itertools::Itertools as _;
use language::Buffer;
use project::debugger::session::{Session, SessionStateEvent};
use project::{DebugScenarioContext, Fs, ProjectPath, WorktreeId};
use project::{DebugScenarioContext, Fs, ProjectPath, TaskSourceKind, WorktreeId};
use project::{Project, debugger::session::ThreadStatus};
use rpc::proto::{self};
use settings::Settings;
@@ -33,10 +35,11 @@ use std::sync::{Arc, LazyLock};
use task::{DebugScenario, TaskContext};
use tree_sitter::{Query, StreamingIterator as _};
use ui::{ContextMenu, Divider, PopoverMenuHandle, Tooltip, prelude::*};
use util::maybe;
use util::{ResultExt, maybe};
use workspace::SplitDirection;
use workspace::item::SaveOptions;
use workspace::{
Pane, Workspace,
Item, Pane, Workspace,
dock::{DockPosition, Panel, PanelEvent},
};
use zed_actions::ToggleFocus;
@@ -363,11 +366,17 @@ impl DebugPanel {
let label = curr_session.read(cx).label().clone();
let adapter = curr_session.read(cx).adapter().clone();
let binary = curr_session.read(cx).binary().cloned().unwrap();
let task = curr_session.update(cx, |session, cx| session.shutdown(cx));
let task_context = curr_session.read(cx).task_context().clone();
let curr_session_id = curr_session.read(cx).session_id();
self.sessions
.retain(|session| session.read(cx).session_id(cx) != curr_session_id);
let task = dap_store_handle.update(cx, |dap_store, cx| {
dap_store.shutdown_session(curr_session_id, cx)
});
cx.spawn_in(window, async move |this, cx| {
task.await;
task.await.log_err();
let (session, task) = dap_store_handle.update(cx, |dap_store, cx| {
let session = dap_store.new_session(label, adapter, task_context, None, cx);
@@ -982,13 +991,90 @@ impl DebugPanel {
cx.notify();
}
pub(crate) fn save_scenario(
pub(crate) fn go_to_scenario_definition(
&self,
scenario: &DebugScenario,
kind: TaskSourceKind,
scenario: DebugScenario,
worktree_id: WorktreeId,
window: &mut Window,
cx: &mut App,
) -> Task<Result<ProjectPath>> {
cx: &mut Context<Self>,
) -> Task<Result<()>> {
let Some(workspace) = self.workspace.upgrade() else {
return Task::ready(Ok(()));
};
let project_path = match kind {
TaskSourceKind::AbsPath { abs_path, .. } => {
let Some(project_path) = workspace
.read(cx)
.project()
.read(cx)
.project_path_for_absolute_path(&abs_path, cx)
else {
return Task::ready(Err(anyhow!("no abs path")));
};
project_path
}
TaskSourceKind::Worktree {
id,
directory_in_worktree: dir,
..
} => {
let relative_path = if dir.ends_with(".vscode") {
dir.join("launch.json")
} else {
dir.join("debug.json")
};
ProjectPath {
worktree_id: id,
path: Arc::from(relative_path),
}
}
_ => return self.save_scenario(scenario, worktree_id, window, cx),
};
let editor = workspace.update(cx, |workspace, cx| {
workspace.open_path(project_path, None, true, window, cx)
});
cx.spawn_in(window, async move |_, cx| {
let editor = editor.await?;
let editor = cx
.update(|_, cx| editor.act_as::<Editor>(cx))?
.context("expected editor")?;
// unfortunately debug tasks don't have an easy way to globally
// identify them. to jump to the one that you just created or an
// old one that you're choosing to edit we use a heuristic of searching for a line with `label: <your label>` from the end rather than the start so we bias towards more renctly
editor.update_in(cx, |editor, window, cx| {
let row = editor.text(cx).lines().enumerate().find_map(|(row, text)| {
if text.contains(scenario.label.as_ref()) && text.contains("\"label\": ") {
Some(row)
} else {
None
}
});
if let Some(row) = row {
editor.go_to_singleton_buffer_point(
text::Point::new(row as u32, 4),
window,
cx,
);
}
})?;
Ok(())
})
}
pub(crate) fn save_scenario(
&self,
scenario: DebugScenario,
worktree_id: WorktreeId,
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
let this = cx.weak_entity();
let project = self.project.clone();
self.workspace
.update(cx, |workspace, cx| {
let Some(mut path) = workspace.absolute_path_of_worktree(worktree_id, cx) else {
@@ -1021,47 +1107,7 @@ impl DebugPanel {
)
.await?;
}
let mut content = fs.load(path).await?;
let new_scenario = serde_json_lenient::to_string_pretty(&serialized_scenario)?
.lines()
.map(|l| format!(" {l}"))
.join("\n");
static ARRAY_QUERY: LazyLock<Query> = LazyLock::new(|| {
Query::new(
&tree_sitter_json::LANGUAGE.into(),
"(document (array (object) @object))", // TODO: use "." anchor to only match last object
)
.expect("Failed to create ARRAY_QUERY")
});
let mut parser = tree_sitter::Parser::new();
parser
.set_language(&tree_sitter_json::LANGUAGE.into())
.unwrap();
let mut cursor = tree_sitter::QueryCursor::new();
let syntax_tree = parser.parse(&content, None).unwrap();
let mut matches =
cursor.matches(&ARRAY_QUERY, syntax_tree.root_node(), content.as_bytes());
// we don't have `.last()` since it's a lending iterator, so loop over
// the whole thing to find the last one
let mut last_offset = None;
while let Some(mat) = matches.next() {
if let Some(pos) = mat.captures.first().map(|m| m.node.byte_range().end) {
last_offset = Some(pos)
}
}
if let Some(pos) = last_offset {
content.insert_str(pos, &new_scenario);
content.insert_str(pos, ",\n");
}
fs.write(path, content.as_bytes()).await?;
workspace.update(cx, |workspace, cx| {
let project_path = workspace.update(cx, |workspace, cx| {
workspace
.project()
.read(cx)
@@ -1069,12 +1115,113 @@ impl DebugPanel {
.context(
"Couldn't get project path for .zed/debug.json in active worktree",
)
})?
})??;
let editor = this
.update_in(cx, |this, window, cx| {
this.workspace.update(cx, |workspace, cx| {
workspace.open_path(project_path, None, true, window, cx)
})
})??
.await?;
let editor = cx
.update(|_, cx| editor.act_as::<Editor>(cx))?
.context("expected editor")?;
let new_scenario = serde_json_lenient::to_string_pretty(&serialized_scenario)?
.lines()
.map(|l| format!(" {l}"))
.join("\n");
editor
.update_in(cx, |editor, window, cx| {
Self::insert_task_into_editor(editor, new_scenario, project, window, cx)
})??
.await
})
})
.unwrap_or_else(|err| Task::ready(Err(err)))
}
pub fn insert_task_into_editor(
editor: &mut Editor,
new_scenario: String,
project: Entity<Project>,
window: &mut Window,
cx: &mut Context<Editor>,
) -> Result<Task<Result<()>>> {
static LAST_ITEM_QUERY: LazyLock<Query> = LazyLock::new(|| {
Query::new(
&tree_sitter_json::LANGUAGE.into(),
"(document (array (object) @object))", // TODO: use "." anchor to only match last object
)
.expect("Failed to create LAST_ITEM_QUERY")
});
static EMPTY_ARRAY_QUERY: LazyLock<Query> = LazyLock::new(|| {
Query::new(
&tree_sitter_json::LANGUAGE.into(),
"(document (array) @array)",
)
.expect("Failed to create EMPTY_ARRAY_QUERY")
});
let content = editor.text(cx);
let mut parser = tree_sitter::Parser::new();
parser.set_language(&tree_sitter_json::LANGUAGE.into())?;
let mut cursor = tree_sitter::QueryCursor::new();
let syntax_tree = parser
.parse(&content, None)
.context("could not parse debug.json")?;
let mut matches = cursor.matches(
&LAST_ITEM_QUERY,
syntax_tree.root_node(),
content.as_bytes(),
);
let mut last_offset = None;
while let Some(mat) = matches.next() {
if let Some(pos) = mat.captures.first().map(|m| m.node.byte_range().end) {
last_offset = Some(pos)
}
}
let mut edits = Vec::new();
let mut cursor_position = 0;
if let Some(pos) = last_offset {
edits.push((pos..pos, format!(",\n{new_scenario}")));
cursor_position = pos + ",\n ".len();
} else {
let mut matches = cursor.matches(
&EMPTY_ARRAY_QUERY,
syntax_tree.root_node(),
content.as_bytes(),
);
if let Some(mat) = matches.next() {
if let Some(pos) = mat.captures.first().map(|m| m.node.byte_range().end - 1) {
edits.push((pos..pos, format!("\n{new_scenario}\n")));
cursor_position = pos + "\n ".len();
}
} else {
edits.push((0..0, format!("[\n{}\n]", new_scenario)));
cursor_position = "[\n ".len();
}
}
editor.transact(window, cx, |editor, window, cx| {
editor.edit(edits, cx);
let snapshot = editor
.buffer()
.read(cx)
.as_singleton()
.unwrap()
.read(cx)
.snapshot();
let point = cursor_position.to_point(&snapshot);
editor.go_to_singleton_buffer_point(point, window, cx);
});
Ok(editor.save(SaveOptions::default(), project, window, cx))
}
pub(crate) fn toggle_thread_picker(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.thread_picker_menu_handle.toggle(window, cx);
}
@@ -1298,9 +1445,7 @@ impl Panel for DebugPanel {
impl Render for DebugPanel {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let has_sessions = self.sessions.len() > 0;
let this = cx.weak_entity();
debug_assert_eq!(has_sessions, self.active_session.is_some());
if self
.active_session
@@ -1487,8 +1632,8 @@ impl Render for DebugPanel {
}))
})
.map(|this| {
if has_sessions {
this.children(self.active_session.clone())
if let Some(active_session) = self.active_session.clone() {
this.child(active_session)
} else {
let docked_to_bottom = self.position(window, cx) == DockPosition::Bottom;
let welcome_experience = v_flex()

View File

@@ -1,12 +1,10 @@
use anyhow::bail;
use anyhow::{Context as _, bail};
use collections::{FxHashMap, HashMap};
use language::LanguageRegistry;
use paths::local_debug_file_relative_path;
use std::{
borrow::Cow,
path::{Path, PathBuf},
sync::Arc,
time::Duration,
usize,
};
use tasks_ui::{TaskOverrides, TasksModal};
@@ -18,35 +16,27 @@ use editor::{Editor, EditorElement, EditorStyle};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
Action, App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
HighlightStyle, InteractiveText, KeyContext, PromptButton, PromptLevel, Render, StyledText,
Subscription, Task, TextStyle, UnderlineStyle, WeakEntity,
KeyContext, Render, Subscription, Task, TextStyle, WeakEntity,
};
use itertools::Itertools as _;
use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch};
use project::{
DebugScenarioContext, ProjectPath, TaskContexts, TaskSourceKind, task_store::TaskStore,
};
use settings::{Settings, initial_local_debug_tasks_content};
use project::{DebugScenarioContext, TaskContexts, TaskSourceKind, task_store::TaskStore};
use settings::Settings;
use task::{DebugScenario, RevealTarget, ZedDebugConfig};
use theme::ThemeSettings;
use ui::{
ActiveTheme, CheckboxWithLabel, Clickable, Context, ContextMenu, Disableable, DropdownMenu,
FluentBuilder, IconWithIndicator, Indicator, IntoElement, KeyBinding, ListItem,
ListItemSpacing, ParentElement, StyledExt, ToggleButton, ToggleState, Toggleable, Tooltip,
Window, div, prelude::*, px, relative, rems,
ActiveTheme, Button, ButtonCommon, ButtonSize, CheckboxWithLabel, Clickable, Color, Context,
ContextMenu, Disableable, DropdownMenu, FluentBuilder, Icon, IconName, IconSize,
IconWithIndicator, Indicator, InteractiveElement, IntoElement, KeyBinding, Label,
LabelCommon as _, LabelSize, ListItem, ListItemSpacing, ParentElement, RenderOnce,
SharedString, Styled, StyledExt, ToggleButton, ToggleState, Toggleable, Tooltip, Window, div,
h_flex, relative, rems, v_flex,
};
use util::ResultExt;
use workspace::{ModalView, Workspace, pane};
use workspace::{ModalView, Workspace, notifications::DetachAndPromptErr, pane};
use crate::{attach_modal::AttachModal, debugger_panel::DebugPanel};
#[allow(unused)]
enum SaveScenarioState {
Saving,
Saved((ProjectPath, SharedString)),
Failed(SharedString),
}
pub(super) struct NewProcessModal {
workspace: WeakEntity<Workspace>,
debug_panel: WeakEntity<DebugPanel>,
@@ -56,7 +46,6 @@ pub(super) struct NewProcessModal {
configure_mode: Entity<ConfigureMode>,
task_mode: TaskMode,
debugger: Option<DebugAdapterName>,
save_scenario_state: Option<SaveScenarioState>,
_subscriptions: [Subscription; 3],
}
@@ -268,7 +257,6 @@ impl NewProcessModal {
mode,
debug_panel: debug_panel.downgrade(),
workspace: workspace_handle,
save_scenario_state: None,
_subscriptions,
}
});
@@ -420,63 +408,29 @@ impl NewProcessModal {
self.debug_picker.read(cx).delegate.task_contexts.clone()
}
fn save_debug_scenario(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let task_contents = self.task_contexts(cx);
pub fn save_debug_scenario(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let task_contexts = self.task_contexts(cx);
let Some(adapter) = self.debugger.as_ref() else {
return;
};
let scenario = self.debug_scenario(&adapter, cx);
self.save_scenario_state = Some(SaveScenarioState::Saving);
cx.spawn_in(window, async move |this, cx| {
let Some((scenario, worktree_id)) = scenario
.await
.zip(task_contents.and_then(|tcx| tcx.worktree()))
else {
this.update(cx, |this, _| {
this.save_scenario_state = Some(SaveScenarioState::Failed(
"Couldn't get scenario or task contents".into(),
))
let scenario = scenario.await.context("no scenario to save")?;
let worktree_id = task_contexts
.context("no task contexts")?
.worktree()
.context("no active worktree")?;
this.update_in(cx, |this, window, cx| {
this.debug_panel.update(cx, |panel, cx| {
panel.save_scenario(scenario, worktree_id, window, cx)
})
.ok();
return;
};
let Some(save_scenario) = this
.update_in(cx, |this, window, cx| {
this.debug_panel
.update(cx, |panel, cx| {
panel.save_scenario(&scenario, worktree_id, window, cx)
})
.ok()
})
.ok()
.flatten()
else {
return;
};
let res = save_scenario.await;
this.update(cx, |this, _| match res {
Ok(saved_file) => {
this.save_scenario_state = Some(SaveScenarioState::Saved((
saved_file,
scenario.label.clone(),
)))
}
Err(error) => {
this.save_scenario_state =
Some(SaveScenarioState::Failed(error.to_string().into()))
}
})??
.await?;
this.update_in(cx, |_, _, cx| {
cx.emit(DismissEvent);
})
.ok();
cx.background_executor().timer(Duration::from_secs(3)).await;
this.update(cx, |this, _| this.save_scenario_state.take())
.ok();
})
.detach();
.detach_and_prompt_err("Failed to edit debug.json", window, cx, |_, _, _| None);
}
fn adapter_drop_down_menu(
@@ -544,70 +498,6 @@ impl NewProcessModal {
}),
)
}
fn open_debug_json(&self, window: &mut Window, cx: &mut Context<NewProcessModal>) {
let this = cx.entity();
window
.spawn(cx, async move |cx| {
let worktree_id = this.update(cx, |this, cx| {
let tcx = this.task_contexts(cx);
tcx?.worktree()
})?;
let Some(worktree_id) = worktree_id else {
let _ = cx.prompt(
PromptLevel::Critical,
"Cannot open debug.json",
Some("You must have at least one project open"),
&[PromptButton::ok("Ok")],
);
return Ok(());
};
let editor = this
.update_in(cx, |this, window, cx| {
this.workspace.update(cx, |workspace, cx| {
workspace.open_path(
ProjectPath {
worktree_id,
path: local_debug_file_relative_path().into(),
},
None,
true,
window,
cx,
)
})
})??
.await?;
cx.update(|_window, cx| {
if let Some(editor) = editor.act_as::<Editor>(cx) {
editor.update(cx, |editor, cx| {
editor.buffer().update(cx, |buffer, cx| {
if let Some(singleton) = buffer.as_singleton() {
singleton.update(cx, |buffer, cx| {
if buffer.is_empty() {
buffer.edit(
[(0..0, initial_local_debug_tasks_content())],
None,
cx,
);
}
})
}
})
});
}
})
.ok();
this.update(cx, |_, cx| cx.emit(DismissEvent)).ok();
anyhow::Ok(())
})
.detach();
}
}
static SELECT_DEBUGGER_LABEL: SharedString = SharedString::new_static("Select Debugger");
@@ -812,39 +702,21 @@ impl Render for NewProcessModal {
NewProcessMode::Launch => el.child(
container
.child(
h_flex()
.text_ui_sm(cx)
.text_color(Color::Muted.color(cx))
.child(
InteractiveText::new(
"open-debug-json",
StyledText::new(
"Open .zed/debug.json for advanced configuration.",
)
.with_highlights([(
5..20,
HighlightStyle {
underline: Some(UnderlineStyle {
thickness: px(1.0),
color: None,
wavy: false,
}),
..Default::default()
},
)]),
)
.on_click(
vec![5..20],
{
let this = cx.entity();
move |_, window, cx| {
this.update(cx, |this, cx| {
this.open_debug_json(window, cx);
})
}
},
h_flex().child(
Button::new("edit-custom-debug", "Edit in debug.json")
.on_click(cx.listener(|this, _, window, cx| {
this.save_debug_scenario(window, cx);
}))
.disabled(
self.debugger.is_none()
|| self
.configure_mode
.read(cx)
.program
.read(cx)
.is_empty(cx),
),
),
),
)
.child(
Button::new("debugger-spawn", "Start")
@@ -862,29 +734,48 @@ impl Render for NewProcessModal {
),
),
),
NewProcessMode::Attach => el.child(
NewProcessMode::Attach => el.child({
let disabled = self.debugger.is_none()
|| self
.attach_mode
.read(cx)
.attach_picker
.read(cx)
.picker
.read(cx)
.delegate
.match_count()
== 0;
let secondary_action = menu::SecondaryConfirm.boxed_clone();
container
.child(div().child(self.adapter_drop_down_menu(window, cx)))
.child(div().children(
KeyBinding::for_action(&*secondary_action, window, cx).map(
|keybind| {
Button::new("edit-attach-task", "Edit in debug.json")
.label_size(LabelSize::Small)
.key_binding(keybind)
.on_click(move |_, window, cx| {
window.dispatch_action(
secondary_action.boxed_clone(),
cx,
)
})
.disabled(disabled)
},
),
))
.child(
Button::new("debugger-spawn", "Start")
.on_click(cx.listener(|this, _, window, cx| {
this.start_new_session(window, cx)
}))
.disabled(
self.debugger.is_none()
|| self
.attach_mode
.read(cx)
.attach_picker
.read(cx)
.picker
.read(cx)
.delegate
.match_count()
== 0,
h_flex()
.child(div().child(self.adapter_drop_down_menu(window, cx)))
.child(
Button::new("debugger-spawn", "Start")
.on_click(cx.listener(|this, _, window, cx| {
this.start_new_session(window, cx)
}))
.disabled(disabled),
),
),
),
)
}),
NewProcessMode::Debug => el,
NewProcessMode::Task => el,
}
@@ -1048,25 +939,6 @@ impl ConfigureMode {
)
.checkbox_position(ui::IconPosition::End),
)
.child(
CheckboxWithLabel::new(
"debugger-save-to-debug-json",
Label::new("Save to debug.json")
.size(LabelSize::Small)
.color(Color::Muted),
self.save_to_debug_json,
{
let this = cx.weak_entity();
move |state, _, cx| {
this.update(cx, |this, _| {
this.save_to_debug_json = *state;
})
.ok();
}
},
)
.checkbox_position(ui::IconPosition::End),
)
}
}
@@ -1329,12 +1201,7 @@ impl PickerDelegate for DebugDelegate {
}
}
fn confirm_input(
&mut self,
_secondary: bool,
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) {
fn confirm_input(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
let text = self.prompt.clone();
let (task_context, worktree_id) = self
.task_contexts
@@ -1364,7 +1231,7 @@ impl PickerDelegate for DebugDelegate {
let args = args.collect::<Vec<_>>();
let task = task::TaskTemplate {
label: "one-off".to_owned(),
label: "one-off".to_owned(), // TODO: rename using command as label
env,
command: program,
args,
@@ -1405,7 +1272,11 @@ impl PickerDelegate for DebugDelegate {
.background_spawn(async move {
for locator in locators {
if let Some(scenario) =
locator.1.create_scenario(&task, "one-off", &adapter).await
// TODO: use a more informative label than "one-off"
locator
.1
.create_scenario(&task, &task.label, &adapter)
.await
{
return Some(scenario);
}
@@ -1439,13 +1310,18 @@ impl PickerDelegate for DebugDelegate {
.detach();
}
fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<picker::Picker<Self>>) {
fn confirm(
&mut self,
secondary: 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.get(match_candidate.candidate_id).cloned());
let Some((_, debug_scenario, context)) = debug_scenario else {
let Some((kind, debug_scenario, context)) = debug_scenario else {
return;
};
@@ -1463,24 +1339,38 @@ impl PickerDelegate for DebugDelegate {
});
let DebugScenarioContext {
task_context,
active_buffer,
active_buffer: _,
worktree_id,
} = context;
let active_buffer = active_buffer.and_then(|buffer| buffer.upgrade());
send_telemetry(&debug_scenario, TelemetrySpawnLocation::ScenarioList, cx);
self.debug_panel
.update(cx, |panel, cx| {
panel.start_session(
debug_scenario,
task_context,
active_buffer,
worktree_id,
window,
cx,
);
if secondary {
let Some(kind) = kind else { return };
let Some(id) = worktree_id else { return };
let debug_panel = self.debug_panel.clone();
cx.spawn_in(window, async move |_, cx| {
debug_panel
.update_in(cx, |debug_panel, window, cx| {
debug_panel.go_to_scenario_definition(kind, debug_scenario, id, window, cx)
})?
.await?;
anyhow::Ok(())
})
.ok();
.detach();
} else {
send_telemetry(&debug_scenario, TelemetrySpawnLocation::ScenarioList, cx);
self.debug_panel
.update(cx, |panel, cx| {
panel.start_session(
debug_scenario,
task_context,
None,
worktree_id,
window,
cx,
);
})
.ok();
}
cx.emit(DismissEvent);
}
@@ -1498,19 +1388,23 @@ impl PickerDelegate for DebugDelegate {
let footer = h_flex()
.w_full()
.p_1p5()
.justify_end()
.justify_between()
.border_t_1()
.border_color(cx.theme().colors().border_variant)
// .child(
// // TODO: add button to open selected task in debug.json
// h_flex().into_any_element(),
// )
.children({
let action = menu::SecondaryConfirm.boxed_clone();
KeyBinding::for_action(&*action, window, cx).map(|keybind| {
Button::new("edit-debug-task", "Edit in debug.json")
.label_size(LabelSize::Small)
.key_binding(keybind)
.on_click(move |_, window, cx| {
window.dispatch_action(action.boxed_clone(), cx)
})
})
})
.map(|this| {
if (current_modifiers.alt || self.matches.is_empty()) && !self.prompt.is_empty() {
let action = picker::ConfirmInput {
secondary: current_modifiers.secondary(),
}
.boxed_clone();
let action = picker::ConfirmInput { secondary: false }.boxed_clone();
this.children(KeyBinding::for_action(&*action, window, cx).map(|keybind| {
Button::new("launch-custom", "Launch Custom")
.key_binding(keybind)
@@ -1607,3 +1501,35 @@ pub(crate) fn resolve_path(path: &mut String) {
);
};
}
#[cfg(test)]
impl NewProcessModal {
pub(crate) fn set_configure(
&mut self,
program: impl AsRef<str>,
cwd: impl AsRef<str>,
stop_on_entry: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.mode = NewProcessMode::Launch;
self.debugger = Some(dap::adapters::DebugAdapterName("fake-adapter".into()));
self.configure_mode.update(cx, |configure, cx| {
configure.program.update(cx, |editor, cx| {
editor.clear(window, cx);
editor.set_text(program.as_ref(), window, cx);
});
configure.cwd.update(cx, |editor, cx| {
editor.clear(window, cx);
editor.set_text(cwd.as_ref(), window, cx);
});
configure.stop_on_entry = match stop_on_entry {
true => ToggleState::Selected,
_ => ToggleState::Unselected,
}
})
}
}

View File

@@ -973,7 +973,7 @@ impl RunningState {
let task_with_shell = SpawnInTerminal {
command_label,
command,
command: Some(command),
args,
..task.resolved.clone()
};
@@ -1085,19 +1085,6 @@ impl RunningState {
.map(PathBuf::from)
.or_else(|| session.binary().unwrap().cwd.clone());
let mut args = request.args.clone();
// Handle special case for NodeJS debug adapter
// If only the Node binary path is provided, we set the command to None
// This prevents the NodeJS REPL from appearing, which is not the desired behavior
// The expected usage is for users to provide their own Node command, e.g., `node test.js`
// This allows the NodeJS debug client to attach correctly
let command = if args.len() > 1 {
Some(args.remove(0))
} else {
None
};
let mut envs: HashMap<String, String> =
self.session.read(cx).task_context().project_env.clone();
if let Some(Value::Object(env)) = &request.env {
@@ -1111,32 +1098,58 @@ impl RunningState {
}
}
let shell = project.read(cx).terminal_settings(&cwd, cx).shell.clone();
let kind = if let Some(command) = command {
let title = request.title.clone().unwrap_or(command.clone());
TerminalKind::Task(task::SpawnInTerminal {
id: task::TaskId("debug".to_string()),
full_label: title.clone(),
label: title.clone(),
command: command.clone(),
args,
command_label: title.clone(),
cwd,
env: envs,
use_new_terminal: true,
allow_concurrent_runs: true,
reveal: task::RevealStrategy::NoFocus,
reveal_target: task::RevealTarget::Dock,
hide: task::HideStrategy::Never,
shell,
show_summary: false,
show_command: false,
show_rerun: false,
})
let mut args = request.args.clone();
let command = if envs.contains_key("VSCODE_INSPECTOR_OPTIONS") {
// Handle special case for NodeJS debug adapter
// If the Node binary path is provided (possibly with arguments like --experimental-network-inspection),
// we set the command to None
// This prevents the NodeJS REPL from appearing, which is not the desired behavior
// The expected usage is for users to provide their own Node command, e.g., `node test.js`
// This allows the NodeJS debug client to attach correctly
if args
.iter()
.filter(|arg| !arg.starts_with("--"))
.collect::<Vec<_>>()
.len()
> 1
{
Some(args.remove(0))
} else {
None
}
} else if args.len() > 0 {
Some(args.remove(0))
} else {
TerminalKind::Shell(cwd.map(|c| c.to_path_buf()))
None
};
let shell = project.read(cx).terminal_settings(&cwd, cx).shell.clone();
let title = request
.title
.clone()
.filter(|title| !title.is_empty())
.or_else(|| command.clone())
.unwrap_or_else(|| "Debug terminal".to_string());
let kind = TerminalKind::Task(task::SpawnInTerminal {
id: task::TaskId("debug".to_string()),
full_label: title.clone(),
label: title.clone(),
command: command.clone(),
args,
command_label: title.clone(),
cwd,
env: envs,
use_new_terminal: true,
allow_concurrent_runs: true,
reveal: task::RevealStrategy::NoFocus,
reveal_target: task::RevealTarget::Dock,
hide: task::HideStrategy::Never,
shell,
show_summary: false,
show_command: false,
show_rerun: false,
});
let workspace = self.workspace.clone();
let weak_project = project.downgrade();

View File

@@ -5,7 +5,8 @@ use std::{
time::Duration,
};
use dap::{Capabilities, ExceptionBreakpointsFilter};
use dap::{Capabilities, ExceptionBreakpointsFilter, adapters::DebugAdapterName};
use db::kvp::KEY_VALUE_STORE;
use editor::Editor;
use gpui::{
Action, AppContext, ClickEvent, Entity, FocusHandle, Focusable, MouseButton, ScrollStrategy,
@@ -16,6 +17,7 @@ use project::{
Project,
debugger::{
breakpoint_store::{BreakpointEditAction, BreakpointStore, SourceBreakpoint},
dap_store::{DapStore, PersistedAdapterOptions},
session::Session,
},
worktree_store::WorktreeStore,
@@ -48,6 +50,7 @@ pub(crate) enum SelectedBreakpointKind {
pub(crate) struct BreakpointList {
workspace: WeakEntity<Workspace>,
breakpoint_store: Entity<BreakpointStore>,
dap_store: Entity<DapStore>,
worktree_store: Entity<WorktreeStore>,
scrollbar_state: ScrollbarState,
breakpoints: Vec<BreakpointEntry>,
@@ -59,6 +62,7 @@ pub(crate) struct BreakpointList {
selected_ix: Option<usize>,
input: Entity<Editor>,
strip_mode: Option<ActiveBreakpointStripMode>,
serialize_exception_breakpoints_task: Option<Task<anyhow::Result<()>>>,
}
impl Focusable for BreakpointList {
@@ -85,24 +89,34 @@ impl BreakpointList {
let project = project.read(cx);
let breakpoint_store = project.breakpoint_store();
let worktree_store = project.worktree_store();
let dap_store = project.dap_store();
let focus_handle = cx.focus_handle();
let scroll_handle = UniformListScrollHandle::new();
let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
cx.new(|cx| Self {
breakpoint_store,
worktree_store,
scrollbar_state,
breakpoints: Default::default(),
hide_scrollbar_task: None,
show_scrollbar: false,
workspace,
session,
focus_handle,
scroll_handle,
selected_ix: None,
input: cx.new(|cx| Editor::single_line(window, cx)),
strip_mode: None,
let adapter_name = session.as_ref().map(|session| session.read(cx).adapter());
cx.new(|cx| {
let this = Self {
breakpoint_store,
dap_store,
worktree_store,
scrollbar_state,
breakpoints: Default::default(),
hide_scrollbar_task: None,
show_scrollbar: false,
workspace,
session,
focus_handle,
scroll_handle,
selected_ix: None,
input: cx.new(|cx| Editor::single_line(window, cx)),
strip_mode: None,
serialize_exception_breakpoints_task: None,
};
if let Some(name) = adapter_name {
_ = this.deserialize_exception_breakpoints(name, cx);
}
this
})
}
@@ -404,12 +418,8 @@ impl BreakpointList {
self.edit_line_breakpoint(path, row, BreakpointEditAction::InvertState, cx);
}
BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => {
if let Some(session) = &self.session {
let id = exception_breakpoint.id.clone();
session.update(cx, |session, cx| {
session.toggle_exception_breakpoint(&id, cx);
});
}
let id = exception_breakpoint.id.clone();
self.toggle_exception_breakpoint(&id, cx);
}
}
cx.notify();
@@ -480,6 +490,64 @@ impl BreakpointList {
cx.notify();
}
fn toggle_exception_breakpoint(&mut self, id: &str, cx: &mut Context<Self>) {
if let Some(session) = &self.session {
session.update(cx, |this, cx| {
this.toggle_exception_breakpoint(&id, cx);
});
cx.notify();
const EXCEPTION_SERIALIZATION_INTERVAL: Duration = Duration::from_secs(1);
self.serialize_exception_breakpoints_task = Some(cx.spawn(async move |this, cx| {
cx.background_executor()
.timer(EXCEPTION_SERIALIZATION_INTERVAL)
.await;
this.update(cx, |this, cx| this.serialize_exception_breakpoints(cx))?
.await?;
Ok(())
}));
}
}
fn kvp_key(adapter_name: &str) -> String {
format!("debug_adapter_`{adapter_name}`_persistence")
}
fn serialize_exception_breakpoints(
&mut self,
cx: &mut Context<Self>,
) -> Task<anyhow::Result<()>> {
if let Some(session) = self.session.as_ref() {
let key = {
let session = session.read(cx);
let name = session.adapter().0;
Self::kvp_key(&name)
};
let settings = self.dap_store.update(cx, |this, cx| {
this.sync_adapter_options(session, cx);
});
let value = serde_json::to_string(&settings);
cx.background_executor()
.spawn(async move { KEY_VALUE_STORE.write_kvp(key, value?).await })
} else {
return Task::ready(Result::Ok(()));
}
}
fn deserialize_exception_breakpoints(
&self,
adapter_name: DebugAdapterName,
cx: &mut Context<Self>,
) -> anyhow::Result<()> {
let Some(val) = KEY_VALUE_STORE.read_kvp(&Self::kvp_key(&adapter_name))? else {
return Ok(());
};
let value: PersistedAdapterOptions = serde_json::from_str(&val)?;
self.dap_store
.update(cx, |this, _| this.set_adapter_options(adapter_name, value));
Ok(())
}
fn hide_scrollbar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |panel, cx| {
@@ -988,12 +1056,7 @@ impl ExceptionBreakpoint {
let list = list.clone();
move |_, _, cx| {
list.update(cx, |this, cx| {
if let Some(session) = &this.session {
session.update(cx, |this, cx| {
this.toggle_exception_breakpoint(&id, cx);
});
cx.notify();
}
this.toggle_exception_breakpoint(&id, cx);
})
.ok();
}

View File

@@ -5,7 +5,7 @@ use super::{
use alacritty_terminal::vte::ansi;
use anyhow::Result;
use collections::HashMap;
use dap::OutputEvent;
use dap::{CompletionItem, CompletionItemType, OutputEvent};
use editor::{Bias, CompletionProvider, Editor, EditorElement, EditorStyle, ExcerptId};
use fuzzy::StringMatchCandidate;
use gpui::{
@@ -17,6 +17,7 @@ use menu::{Confirm, SelectNext, SelectPrevious};
use project::{
Completion, CompletionResponse,
debugger::session::{CompletionsQuery, OutputToken, Session},
lsp_store::CompletionDocumentation,
search_history::{SearchHistory, SearchHistoryCursor},
};
use settings::Settings;
@@ -555,15 +556,27 @@ impl CompletionProvider for ConsoleQueryBarCompletionProvider {
buffer: &Entity<Buffer>,
position: language::Anchor,
text: &str,
_trigger_in_words: bool,
trigger_in_words: bool,
menu_is_open: bool,
cx: &mut Context<Editor>,
) -> bool {
let mut chars = text.chars();
let char = if let Some(char) = chars.next() {
char
} else {
return false;
};
let snapshot = buffer.read(cx).snapshot();
if !menu_is_open && !snapshot.settings_at(position, cx).show_completions_on_input {
return false;
}
let classifier = snapshot.char_classifier_at(position).for_completion(true);
if trigger_in_words && classifier.is_word(char) {
return true;
}
self.0
.read_with(cx, |console, cx| {
console
@@ -596,21 +609,28 @@ impl ConsoleQueryBarCompletionProvider {
variable_list.completion_variables(cx)
}) {
if let Some(evaluate_name) = &variable.evaluate_name {
variables.insert(evaluate_name.clone(), variable.value.clone());
string_matches.push(StringMatchCandidate {
id: 0,
string: evaluate_name.clone(),
char_bag: evaluate_name.chars().collect(),
});
if variables
.insert(evaluate_name.clone(), variable.value.clone())
.is_none()
{
string_matches.push(StringMatchCandidate {
id: 0,
string: evaluate_name.clone(),
char_bag: evaluate_name.chars().collect(),
});
}
}
variables.insert(variable.name.clone(), variable.value.clone());
string_matches.push(StringMatchCandidate {
id: 0,
string: variable.name.clone(),
char_bag: variable.name.chars().collect(),
});
if variables
.insert(variable.name.clone(), variable.value.clone())
.is_none()
{
string_matches.push(StringMatchCandidate {
id: 0,
string: variable.name.clone(),
char_bag: variable.name.chars().collect(),
});
}
}
(variables, string_matches)
@@ -656,11 +676,13 @@ impl ConsoleQueryBarCompletionProvider {
new_text: string_match.string.clone(),
label: CodeLabel {
filter_range: 0..string_match.string.len(),
text: format!("{} {}", string_match.string, variable_value),
text: string_match.string.clone(),
runs: Vec::new(),
},
icon_path: None,
documentation: None,
documentation: Some(CompletionDocumentation::MultiLineMarkdown(
variable_value.into(),
)),
confirm: None,
source: project::CompletionSource::Custom,
insert_text_mode: None,
@@ -675,6 +697,32 @@ impl ConsoleQueryBarCompletionProvider {
})
}
const fn completion_type_score(completion_type: CompletionItemType) -> usize {
match completion_type {
CompletionItemType::Field | CompletionItemType::Property => 0,
CompletionItemType::Variable | CompletionItemType::Value => 1,
CompletionItemType::Method
| CompletionItemType::Function
| CompletionItemType::Constructor => 2,
CompletionItemType::Class
| CompletionItemType::Interface
| CompletionItemType::Module => 3,
_ => 4,
}
}
fn completion_item_sort_text(completion_item: &CompletionItem) -> String {
completion_item.sort_text.clone().unwrap_or_else(|| {
format!(
"{:03}_{}",
Self::completion_type_score(
completion_item.type_.unwrap_or(CompletionItemType::Text)
),
completion_item.label.to_ascii_lowercase()
)
})
}
fn client_completions(
&self,
console: &Entity<Console>,
@@ -699,6 +747,7 @@ impl ConsoleQueryBarCompletionProvider {
let completions = completions
.into_iter()
.map(|completion| {
let sort_text = Self::completion_item_sort_text(&completion);
let new_text = completion
.text
.as_ref()
@@ -731,12 +780,11 @@ impl ConsoleQueryBarCompletionProvider {
runs: Vec::new(),
},
icon_path: None,
documentation: None,
documentation: completion.detail.map(|detail| {
CompletionDocumentation::MultiLineMarkdown(detail.into())
}),
confirm: None,
source: project::CompletionSource::BufferWord {
word_range: buffer_position..language::Anchor::MAX,
resolved: false,
},
source: project::CompletionSource::Dap { sort_text },
insert_text_mode: None,
}
})

View File

@@ -27,7 +27,7 @@ async fn test_direct_attach_to_process(executor: BackgroundExecutor, cx: &mut Te
let workspace = init_test_workspace(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let session = start_debug_session_with(
let _session = start_debug_session_with(
&workspace,
cx,
DebugTaskDefinition {
@@ -59,14 +59,6 @@ async fn test_direct_attach_to_process(executor: BackgroundExecutor, cx: &mut Te
assert!(workspace.active_modal::<AttachModal>(cx).is_none());
})
.unwrap();
let shutdown_session = project.update(cx, |project, cx| {
project.dap_store().update(cx, |dap_store, cx| {
dap_store.shutdown_session(session.read(cx).session_id(), cx)
})
});
shutdown_session.await.unwrap();
}
#[gpui::test]

View File

@@ -1,13 +1,15 @@
use dap::DapRegistry;
use editor::Editor;
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
use project::{FakeFs, Project};
use project::{FakeFs, Fs as _, Project};
use serde_json::json;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use task::{DebugRequest, DebugScenario, LaunchRequest, TaskContext, VariableName, ZedDebugConfig};
use text::Point;
use util::path;
// use crate::new_process_modal::NewProcessMode;
use crate::NewProcessMode;
use crate::tests::{init_test, init_test_workspace};
#[gpui::test]
@@ -159,111 +161,127 @@ async fn test_debug_session_substitutes_variables_and_relativizes_paths(
}
}
// #[gpui::test]
// async fn test_save_debug_scenario_to_file(executor: BackgroundExecutor, cx: &mut TestAppContext) {
// init_test(cx);
#[gpui::test]
async fn test_save_debug_scenario_to_file(executor: BackgroundExecutor, cx: &mut TestAppContext) {
init_test(cx);
// let fs = FakeFs::new(executor.clone());
// fs.insert_tree(
// path!("/project"),
// json!({
// "main.rs": "fn main() {}"
// }),
// )
// .await;
let fs = FakeFs::new(executor.clone());
fs.insert_tree(
path!("/project"),
json!({
"main.rs": "fn main() {}"
}),
)
.await;
// let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
// let workspace = init_test_workspace(&project, cx).await;
// let cx = &mut VisualTestContext::from_window(*workspace, cx);
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
let workspace = init_test_workspace(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx);
// workspace
// .update(cx, |workspace, window, cx| {
// crate::new_process_modal::NewProcessModal::show(
// workspace,
// window,
// NewProcessMode::Debug,
// None,
// cx,
// );
// })
// .unwrap();
workspace
.update(cx, |workspace, window, cx| {
crate::new_process_modal::NewProcessModal::show(
workspace,
window,
NewProcessMode::Debug,
None,
cx,
);
})
.unwrap();
// cx.run_until_parked();
cx.run_until_parked();
// let modal = workspace
// .update(cx, |workspace, _, cx| {
// workspace.active_modal::<crate::new_process_modal::NewProcessModal>(cx)
// })
// .unwrap()
// .expect("Modal should be active");
let modal = workspace
.update(cx, |workspace, _, cx| {
workspace.active_modal::<crate::new_process_modal::NewProcessModal>(cx)
})
.unwrap()
.expect("Modal should be active");
// modal.update_in(cx, |modal, window, cx| {
// modal.set_configure("/project/main", "/project", false, window, cx);
// modal.save_scenario(window, cx);
// });
modal.update_in(cx, |modal, window, cx| {
modal.set_configure("/project/main", "/project", false, window, cx);
modal.save_debug_scenario(window, cx);
});
// cx.executor().run_until_parked();
cx.executor().run_until_parked();
// let debug_json_content = fs
// .load(path!("/project/.zed/debug.json").as_ref())
// .await
// .expect("debug.json should exist");
let editor = workspace
.update(cx, |workspace, _window, cx| {
workspace.active_item_as::<Editor>(cx).unwrap()
})
.unwrap();
// let expected_content = vec![
// "[",
// " {",
// r#" "adapter": "fake-adapter","#,
// r#" "label": "main (fake-adapter)","#,
// r#" "request": "launch","#,
// r#" "program": "/project/main","#,
// r#" "cwd": "/project","#,
// r#" "args": [],"#,
// r#" "env": {}"#,
// " }",
// "]",
// ];
let debug_json_content = fs
.load(path!("/project/.zed/debug.json").as_ref())
.await
.expect("debug.json should exist")
.lines()
.filter(|line| !line.starts_with("//"))
.collect::<Vec<_>>()
.join("\n");
// let actual_lines: Vec<&str> = debug_json_content.lines().collect();
// pretty_assertions::assert_eq!(expected_content, actual_lines);
let expected_content = indoc::indoc! {r#"
[
{
"adapter": "fake-adapter",
"label": "main (fake-adapter)",
"request": "launch",
"program": "/project/main",
"cwd": "/project",
"args": [],
"env": {}
}
]"#};
// modal.update_in(cx, |modal, window, cx| {
// modal.set_configure("/project/other", "/project", true, window, cx);
// modal.save_scenario(window, cx);
// });
pretty_assertions::assert_eq!(expected_content, debug_json_content);
// cx.executor().run_until_parked();
editor.update(cx, |editor, cx| {
assert_eq!(
editor.selections.newest::<Point>(cx).head(),
Point::new(5, 2)
)
});
// let debug_json_content = fs
// .load(path!("/project/.zed/debug.json").as_ref())
// .await
// .expect("debug.json should exist after second save");
modal.update_in(cx, |modal, window, cx| {
modal.set_configure("/project/other", "/project", true, window, cx);
modal.save_debug_scenario(window, cx);
});
// let expected_content = vec![
// "[",
// " {",
// r#" "adapter": "fake-adapter","#,
// r#" "label": "main (fake-adapter)","#,
// r#" "request": "launch","#,
// r#" "program": "/project/main","#,
// r#" "cwd": "/project","#,
// r#" "args": [],"#,
// r#" "env": {}"#,
// " },",
// " {",
// r#" "adapter": "fake-adapter","#,
// r#" "label": "other (fake-adapter)","#,
// r#" "request": "launch","#,
// r#" "program": "/project/other","#,
// r#" "cwd": "/project","#,
// r#" "args": [],"#,
// r#" "env": {}"#,
// " }",
// "]",
// ];
cx.executor().run_until_parked();
// let actual_lines: Vec<&str> = debug_json_content.lines().collect();
// pretty_assertions::assert_eq!(expected_content, actual_lines);
// }
let expected_content = indoc::indoc! {r#"
[
{
"adapter": "fake-adapter",
"label": "main (fake-adapter)",
"request": "launch",
"program": "/project/main",
"cwd": "/project",
"args": [],
"env": {}
},
{
"adapter": "fake-adapter",
"label": "other (fake-adapter)",
"request": "launch",
"program": "/project/other",
"cwd": "/project",
"args": [],
"env": {}
}
]"#};
let debug_json_content = fs
.load(path!("/project/.zed/debug.json").as_ref())
.await
.expect("debug.json should exist")
.lines()
.filter(|line| !line.starts_with("//"))
.collect::<Vec<_>>()
.join("\n");
pretty_assertions::assert_eq!(expected_content, debug_json_content);
}
#[gpui::test]
async fn test_dap_adapter_config_conversion_and_validation(cx: &mut TestAppContext) {
@@ -272,7 +290,6 @@ async fn test_dap_adapter_config_conversion_and_validation(cx: &mut TestAppConte
let mut expected_adapters = vec![
"CodeLLDB",
"Debugpy",
"PHP",
"JavaScript",
"Delve",
"GDB",

View File

@@ -635,6 +635,8 @@ actions!(
SignatureHelpNext,
/// Navigates to the previous signature in the signature help popup.
SignatureHelpPrevious,
/// Sorts selected lines by length.
SortLinesByLength,
/// Sorts selected lines case-insensitively.
SortLinesCaseInsensitive,
/// Sorts selected lines case-sensitively.

View File

@@ -1083,11 +1083,10 @@ impl CompletionsMenu {
if lsp_completion.kind == Some(CompletionItemKind::SNIPPET)
);
let sort_text = if let CompletionSource::Lsp { lsp_completion, .. } = &completion.source
{
lsp_completion.sort_text.as_deref()
} else {
None
let sort_text = match &completion.source {
CompletionSource::Lsp { lsp_completion, .. } => lsp_completion.sort_text.as_deref(),
CompletionSource::Dap { sort_text } => Some(sort_text.as_str()),
_ => None,
};
let (sort_kind, sort_label) = completion.sort_key();

View File

@@ -1066,7 +1066,7 @@ impl DisplaySnapshot {
}
let font_size = editor_style.text.font_size.to_pixels(*rem_size);
text_system.layout_line(&line, font_size, &runs)
text_system.layout_line(&line, font_size, &runs, None)
}
pub fn x_for_display_point(

View File

@@ -571,7 +571,7 @@ impl Default for EditorStyle {
// HACK: Status colors don't have a real default.
// We should look into removing the status colors from the editor
// style and retrieve them directly from the theme.
status: StatusColors::dark(),
status: StatusColors::default(),
inlay_hints_style: HighlightStyle::default(),
inline_completion_styles: InlineCompletionStyles {
insertion: HighlightStyle::default(),
@@ -10204,6 +10204,17 @@ impl Editor {
self.manipulate_immutable_lines(window, cx, |lines| lines.sort())
}
pub fn sort_lines_by_length(
&mut self,
_: &SortLinesByLength,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.manipulate_immutable_lines(window, cx, |lines| {
lines.sort_by_key(|&line| line.chars().count())
})
}
pub fn sort_lines_case_insensitive(
&mut self,
_: &SortLinesCaseInsensitive,

View File

@@ -4075,6 +4075,29 @@ async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppC
Zˇ»
"});
// Test sort_lines_by_length()
//
// Demonstrates:
// - ∞ is 3 bytes UTF-8, but sorted by its char count (1)
// - sort is stable
cx.set_state(indoc! {"
«123
æ
12
1
æˇ»
"});
cx.update_editor(|e, window, cx| e.sort_lines_by_length(&SortLinesByLength, window, cx));
cx.assert_editor_state(indoc! {"
«æ
1
æ
12
123ˇ»
"});
// Test reverse_lines()
cx.set_state(indoc! {"
«5
@@ -22325,6 +22348,19 @@ async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
def f() -> list[str]:
"});
// test does not outdent on typing : after case keyword
cx.set_state(indoc! {"
match 1:
caseˇ
"});
cx.update_editor(|editor, window, cx| {
editor.handle_input(":", window, cx);
});
cx.assert_editor_state(indoc! {"
match 1:
case:ˇ
"});
}
#[gpui::test]

View File

@@ -225,6 +225,7 @@ impl EditorElement {
register_action(editor, window, Editor::autoindent);
register_action(editor, window, Editor::delete_line);
register_action(editor, window, Editor::join_lines);
register_action(editor, window, Editor::sort_lines_by_length);
register_action(editor, window, Editor::sort_lines_case_sensitive);
register_action(editor, window, Editor::sort_lines_case_insensitive);
register_action(editor, window, Editor::reverse_lines);
@@ -1610,6 +1611,7 @@ impl EditorElement {
strikethrough: None,
underline: None,
}],
None,
)
})
} else {
@@ -2828,6 +2830,7 @@ impl EditorElement {
) -> Vec<AnyElement> {
self.editor.update(cx, |editor, cx| {
let active_task_indicator_row =
// TODO: add edit button on the right side of each row in the context menu
if let Some(crate::CodeContextMenu::CodeActions(CodeActionsMenu {
deployed_from,
actions,
@@ -3261,10 +3264,12 @@ impl EditorElement {
underline: None,
strikethrough: None,
};
let line =
window
.text_system()
.shape_line(line.to_string().into(), font_size, &[run]);
let line = window.text_system().shape_line(
line.to_string().into(),
font_size,
&[run],
None,
);
LineWithInvisibles {
width: line.width,
len: line.len,
@@ -6886,6 +6891,7 @@ impl EditorElement {
underline: None,
strikethrough: None,
}],
None,
);
layout.width
@@ -6914,6 +6920,7 @@ impl EditorElement {
text,
self.style.text.font_size.to_pixels(window.rem_size()),
&[run],
None,
)
}
@@ -7182,10 +7189,12 @@ impl LineWithInvisibles {
}]) {
if let Some(replacement) = highlighted_chunk.replacement {
if !line.is_empty() {
let shaped_line =
window
.text_system()
.shape_line(line.clone().into(), font_size, &styles);
let shaped_line = window.text_system().shape_line(
line.clone().into(),
font_size,
&styles,
None,
);
width += shaped_line.width;
len += shaped_line.len;
fragments.push(LineFragment::Text(shaped_line));
@@ -7205,6 +7214,7 @@ impl LineWithInvisibles {
chunk,
font_size,
&[text_style.to_run(highlighted_chunk.text.len())],
None,
);
AvailableSpace::Definite(shaped_line.width)
} else {
@@ -7249,7 +7259,7 @@ impl LineWithInvisibles {
};
let line_layout = window
.text_system()
.shape_line(x, font_size, &[run])
.shape_line(x, font_size, &[run], None)
.with_len(highlighted_chunk.text.len());
width += line_layout.width;
@@ -7264,6 +7274,7 @@ impl LineWithInvisibles {
line.clone().into(),
font_size,
&styles,
None,
);
width += shaped_line.width;
len += shaped_line.len;
@@ -7933,6 +7944,7 @@ impl Element for EditorElement {
editor.last_bounds = Some(bounds);
editor.gutter_dimensions = gutter_dimensions;
editor.set_visible_line_count(bounds.size.height / line_height, window, cx);
editor.set_visible_column_count(editor_content_width / em_advance);
if matches!(
editor.mode,
@@ -8438,6 +8450,7 @@ impl Element for EditorElement {
scroll_width,
em_advance,
&line_layouts,
window,
cx,
)
} else {
@@ -8592,6 +8605,7 @@ impl Element for EditorElement {
scroll_width,
em_advance,
&line_layouts,
window,
cx,
)
} else {
@@ -8829,6 +8843,7 @@ impl Element for EditorElement {
underline: None,
strikethrough: None,
}],
None
);
let space_invisible = window.text_system().shape_line(
"".into(),
@@ -8841,6 +8856,7 @@ impl Element for EditorElement {
underline: None,
strikethrough: None,
}],
None
);
let mode = snapshot.mode.clone();

View File

@@ -381,10 +381,14 @@ fn show_hover(
.anchor_after(local_diagnostic.range.end),
};
let scroll_handle = ScrollHandle::new();
Some(DiagnosticPopover {
local_diagnostic,
markdown,
border_color,
scrollbar_state: ScrollbarState::new(scroll_handle.clone()),
scroll_handle,
background_color,
keyboard_grace: Rc::new(RefCell::new(ignore_timeout)),
anchor,
@@ -955,6 +959,8 @@ pub struct DiagnosticPopover {
pub keyboard_grace: Rc<RefCell<bool>>,
pub anchor: Anchor,
_subscription: Subscription,
pub scroll_handle: ScrollHandle,
pub scrollbar_state: ScrollbarState,
}
impl DiagnosticPopover {
@@ -968,10 +974,7 @@ impl DiagnosticPopover {
let this = cx.entity().downgrade();
div()
.id("diagnostic")
.block()
.max_h(max_size.height)
.overflow_y_scroll()
.max_w(max_size.width)
.occlude()
.elevation_2_borderless(cx)
// Don't draw the background color if the theme
// allows transparent surfaces.
@@ -992,27 +995,72 @@ impl DiagnosticPopover {
div()
.py_1()
.px_2()
.child(
MarkdownElement::new(
self.markdown.clone(),
diagnostics_markdown_style(window, cx),
)
.on_url_click(move |link, window, cx| {
if let Some(renderer) = GlobalDiagnosticRenderer::global(cx) {
this.update(cx, |this, cx| {
renderer.as_ref().open_link(this, link, window, cx);
})
.ok();
}
}),
)
.bg(self.background_color)
.border_1()
.border_color(self.border_color)
.rounded_lg(),
.rounded_lg()
.child(
div()
.id("diagnostic-content-container")
.overflow_y_scroll()
.max_w(max_size.width)
.max_h(max_size.height)
.track_scroll(&self.scroll_handle)
.child(
MarkdownElement::new(
self.markdown.clone(),
diagnostics_markdown_style(window, cx),
)
.on_url_click(
move |link, window, cx| {
if let Some(renderer) = GlobalDiagnosticRenderer::global(cx)
{
this.update(cx, |this, cx| {
renderer.as_ref().open_link(this, link, window, cx);
})
.ok();
}
},
),
),
)
.child(self.render_vertical_scrollbar(cx)),
)
.into_any_element()
}
fn render_vertical_scrollbar(&self, cx: &mut Context<Editor>) -> Stateful<Div> {
div()
.occlude()
.id("diagnostic-popover-vertical-scroll")
.on_mouse_move(cx.listener(|_, _, _, cx| {
cx.notify();
cx.stop_propagation()
}))
.on_hover(|_, _, cx| {
cx.stop_propagation();
})
.on_any_mouse_down(|_, _, cx| {
cx.stop_propagation();
})
.on_mouse_up(
MouseButton::Left,
cx.listener(|_, _, _, cx| {
cx.stop_propagation();
}),
)
.on_scroll_wheel(cx.listener(|_, _, _, cx| {
cx.notify();
}))
.h_full()
.absolute()
.right_1()
.top_1()
.bottom_0()
.w(px(12.))
.cursor_default()
.children(Scrollbar::vertical(self.scrollbar_state.clone()))
}
}
#[cfg(test)]

View File

@@ -13,6 +13,7 @@ use crate::{
pub use autoscroll::{Autoscroll, AutoscrollStrategy};
use core::fmt::Debug;
use gpui::{App, Axis, Context, Global, Pixels, Task, Window, point, px};
use language::language_settings::{AllLanguageSettings, SoftWrap};
use language::{Bias, Point};
pub use scroll_amount::ScrollAmount;
use settings::Settings;
@@ -151,12 +152,16 @@ pub struct ScrollManager {
pub(crate) vertical_scroll_margin: f32,
anchor: ScrollAnchor,
ongoing: OngoingScroll,
/// The second element indicates whether the autoscroll request is local
/// (true) or remote (false). Local requests are initiated by user actions,
/// while remote requests come from external sources.
autoscroll_request: Option<(Autoscroll, bool)>,
last_autoscroll: Option<(gpui::Point<f32>, f32, f32, AutoscrollStrategy)>,
show_scrollbars: bool,
hide_scrollbar_task: Option<Task<()>>,
active_scrollbar: Option<ActiveScrollbarState>,
visible_line_count: Option<f32>,
visible_column_count: Option<f32>,
forbid_vertical_scroll: bool,
minimap_thumb_state: Option<ScrollbarThumbState>,
}
@@ -173,6 +178,7 @@ impl ScrollManager {
active_scrollbar: None,
last_autoscroll: None,
visible_line_count: None,
visible_column_count: None,
forbid_vertical_scroll: false,
minimap_thumb_state: None,
}
@@ -210,7 +216,7 @@ impl ScrollManager {
window: &mut Window,
cx: &mut Context<Editor>,
) {
let (new_anchor, top_row) = if scroll_position.y <= 0. {
let (new_anchor, top_row) = if scroll_position.y <= 0. && scroll_position.x <= 0. {
(
ScrollAnchor {
anchor: Anchor::min(),
@@ -218,6 +224,22 @@ impl ScrollManager {
},
0,
)
} else if scroll_position.y <= 0. {
let buffer_point = map
.clip_point(
DisplayPoint::new(DisplayRow(0), scroll_position.x as u32),
Bias::Left,
)
.to_point(map);
let anchor = map.buffer_snapshot.anchor_at(buffer_point, Bias::Right);
(
ScrollAnchor {
anchor: anchor,
offset: scroll_position.max(&gpui::Point::default()),
},
0,
)
} else {
let scroll_top = scroll_position.y;
let scroll_top = match EditorSettings::get_global(cx).scroll_beyond_last_line {
@@ -242,8 +264,13 @@ impl ScrollManager {
}
};
let scroll_top_buffer_point =
DisplayPoint::new(DisplayRow(scroll_top as u32), 0).to_point(map);
let scroll_top_row = DisplayRow(scroll_top as u32);
let scroll_top_buffer_point = map
.clip_point(
DisplayPoint::new(scroll_top_row, scroll_position.x as u32),
Bias::Left,
)
.to_point(map);
let top_anchor = map
.buffer_snapshot
.anchor_at(scroll_top_buffer_point, Bias::Right);
@@ -476,6 +503,10 @@ impl Editor {
.map(|line_count| line_count as u32 - 1)
}
pub fn visible_column_count(&self) -> Option<f32> {
self.scroll_manager.visible_column_count
}
pub(crate) fn set_visible_line_count(
&mut self,
lines: f32,
@@ -497,6 +528,10 @@ impl Editor {
}
}
pub(crate) fn set_visible_column_count(&mut self, columns: f32) {
self.scroll_manager.visible_column_count = Some(columns);
}
pub fn apply_scroll_delta(
&mut self,
scroll_delta: gpui::Point<f32>,
@@ -675,25 +710,48 @@ impl Editor {
let Some(visible_line_count) = self.visible_line_count() else {
return;
};
let Some(mut visible_column_count) = self.visible_column_count() else {
return;
};
// If the user has a preferred line length, and has the editor
// configured to wrap at the preferred line length, or bounded to it,
// use that value over the visible column count. This was mostly done so
// that tests could actually be written for vim's `z l`, `z h`, `z
// shift-l` and `z shift-h` commands, as there wasn't a good way to
// configure the editor to only display a certain number of columns. If
// that ever happens, this could probably be removed.
let settings = AllLanguageSettings::get_global(cx);
if matches!(
settings.defaults.soft_wrap,
SoftWrap::PreferredLineLength | SoftWrap::Bounded
) {
if (settings.defaults.preferred_line_length as f32) < visible_column_count {
visible_column_count = settings.defaults.preferred_line_length as f32;
}
}
// If the scroll position is currently at the left edge of the document
// (x == 0.0) and the intent is to scroll right, the gutter's margin
// should first be added to the current position, otherwise the cursor
// will end at the column position minus the margin, which looks off.
if current_position.x == 0.0 && amount.columns() > 0. {
if current_position.x == 0.0 && amount.columns(visible_column_count) > 0. {
if let Some(last_position_map) = &self.last_position_map {
current_position.x += self.gutter_dimensions.margin / last_position_map.em_advance;
}
}
let new_position =
current_position + point(amount.columns(), amount.lines(visible_line_count));
let new_position = current_position
+ point(
amount.columns(visible_column_count),
amount.lines(visible_line_count),
);
self.set_scroll_position(new_position, window, cx);
}
/// Returns an ordering. The newest selection is:
/// Ordering::Equal => on screen
/// Ordering::Less => above the screen
/// Ordering::Greater => below the screen
/// Ordering::Less => above or to the left of the screen
/// Ordering::Greater => below or to the right of the screen
pub fn newest_selection_on_screen(&self, cx: &mut App) -> Ordering {
let snapshot = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let newest_head = self
@@ -711,8 +769,12 @@ impl Editor {
return Ordering::Less;
}
if let Some(visible_lines) = self.visible_line_count() {
if newest_head.row() <= DisplayRow(screen_top.row().0 + visible_lines as u32) {
if let (Some(visible_lines), Some(visible_columns)) =
(self.visible_line_count(), self.visible_column_count())
{
if newest_head.row() <= DisplayRow(screen_top.row().0 + visible_lines as u32)
&& newest_head.column() <= screen_top.column() + visible_columns as u32
{
return Ordering::Equal;
}
}

View File

@@ -274,12 +274,14 @@ impl Editor {
start_row: DisplayRow,
viewport_width: Pixels,
scroll_width: Pixels,
max_glyph_width: Pixels,
em_advance: Pixels,
layouts: &[LineWithInvisibles],
window: &mut Window,
cx: &mut Context<Self>,
) -> bool {
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let selections = self.selections.all::<Point>(cx);
let mut scroll_position = self.scroll_manager.scroll_position(&display_map);
let mut target_left;
let mut target_right;
@@ -295,16 +297,17 @@ impl Editor {
if head.row() >= start_row
&& head.row() < DisplayRow(start_row.0 + layouts.len() as u32)
{
let start_column = head.column().saturating_sub(3);
let end_column = cmp::min(display_map.line_len(head.row()), head.column() + 3);
let start_column = head.column();
let end_column = cmp::min(display_map.line_len(head.row()), head.column());
target_left = target_left.min(
layouts[head.row().minus(start_row) as usize]
.x_for_index(start_column as usize),
.x_for_index(start_column as usize)
+ self.gutter_dimensions.margin,
);
target_right = target_right.max(
layouts[head.row().minus(start_row) as usize]
.x_for_index(end_column as usize)
+ max_glyph_width,
+ em_advance,
);
}
}
@@ -319,14 +322,16 @@ impl Editor {
return false;
}
let scroll_left = self.scroll_manager.anchor.offset.x * max_glyph_width;
let scroll_left = self.scroll_manager.anchor.offset.x * em_advance;
let scroll_right = scroll_left + viewport_width;
if target_left < scroll_left {
self.scroll_manager.anchor.offset.x = target_left / max_glyph_width;
scroll_position.x = target_left / em_advance;
self.set_scroll_position_internal(scroll_position, true, true, window, cx);
true
} else if target_right > scroll_right {
self.scroll_manager.anchor.offset.x = (target_right - viewport_width) / max_glyph_width;
scroll_position.x = (target_right - viewport_width) / em_advance;
self.set_scroll_position_internal(scroll_position, true, true, window, cx);
true
} else {
false

View File

@@ -23,6 +23,8 @@ pub enum ScrollAmount {
Page(f32),
// Scroll N columns (positive is towards the right of the document)
Column(f32),
// Scroll N page width (positive is towards the right of the document)
PageWidth(f32),
}
impl ScrollAmount {
@@ -37,14 +39,16 @@ impl ScrollAmount {
(visible_line_count * count).trunc()
}
Self::Column(_count) => 0.0,
Self::PageWidth(_count) => 0.0,
}
}
pub fn columns(&self) -> f32 {
pub fn columns(&self, visible_column_count: f32) -> f32 {
match self {
Self::Line(_count) => 0.0,
Self::Page(_count) => 0.0,
Self::Column(count) => *count,
Self::PageWidth(count) => (visible_column_count * count).trunc(),
}
}
@@ -58,6 +62,7 @@ impl ScrollAmount {
// so I'm leaving this at 0.0 for now to try and make it clear that
// this should not have an impact on that?
ScrollAmount::Column(_) => px(0.0),
ScrollAmount::PageWidth(_) => px(0.0),
}
}

View File

@@ -14,7 +14,7 @@ impl Example for FileChangeNotificationExample {
url: "https://github.com/octocat/hello-world".to_string(),
revision: "7fd1a60b01f91b314f59955a4e4d4e80d8edf11d".to_string(),
language_server: None,
max_assertions: Some(1),
max_assertions: None,
profile_id: AgentProfileId::default(),
existing_thread_json: None,
max_turns: Some(3),

View File

@@ -1,5 +1,6 @@
use crate::{
ExtensionLibraryKind, ExtensionManifest, GrammarManifestEntry, parse_wasm_extension_version,
ExtensionLibraryKind, ExtensionManifest, GrammarManifestEntry, build_debug_adapter_schema_path,
parse_wasm_extension_version,
};
use anyhow::{Context as _, Result, bail};
use async_compression::futures::bufread::GzipDecoder;
@@ -99,12 +100,8 @@ impl ExtensionBuilder {
}
for (debug_adapter_name, meta) in &mut extension_manifest.debug_adapters {
let debug_adapter_relative_schema_path =
meta.schema_path.clone().unwrap_or_else(|| {
Path::new("debug_adapter_schemas")
.join(Path::new(debug_adapter_name.as_ref()).with_extension("json"))
});
let debug_adapter_schema_path = extension_dir.join(debug_adapter_relative_schema_path);
let debug_adapter_schema_path =
extension_dir.join(build_debug_adapter_schema_path(debug_adapter_name, meta));
let debug_adapter_schema = fs::read_to_string(&debug_adapter_schema_path)
.with_context(|| {

View File

@@ -130,6 +130,22 @@ impl ExtensionManifest {
Ok(())
}
pub fn allow_remote_load(&self) -> bool {
!self.language_servers.is_empty()
|| !self.debug_adapters.is_empty()
|| !self.debug_locators.is_empty()
}
}
pub fn build_debug_adapter_schema_path(
adapter_name: &Arc<str>,
meta: &DebugAdapterManifestEntry,
) -> PathBuf {
meta.schema_path.clone().unwrap_or_else(|| {
Path::new("debug_adapter_schemas")
.join(Path::new(adapter_name.as_ref()).with_extension("json"))
})
}
/// A capability for an extension.
@@ -320,6 +336,29 @@ mod tests {
}
}
#[test]
fn test_build_adapter_schema_path_with_schema_path() {
let adapter_name = Arc::from("my_adapter");
let entry = DebugAdapterManifestEntry {
schema_path: Some(PathBuf::from("foo/bar")),
};
let path = build_debug_adapter_schema_path(&adapter_name, &entry);
assert_eq!(path, PathBuf::from("foo/bar"));
}
#[test]
fn test_build_adapter_schema_path_without_schema_path() {
let adapter_name = Arc::from("my_adapter");
let entry = DebugAdapterManifestEntry { schema_path: None };
let path = build_debug_adapter_schema_path(&adapter_name, &entry);
assert_eq!(
path,
PathBuf::from("debug_adapter_schemas").join("my_adapter.json")
);
}
#[test]
fn test_allow_exact_match() {
let manifest = ExtensionManifest {

View File

@@ -54,7 +54,7 @@ use std::{
time::{Duration, Instant},
};
use url::Url;
use util::ResultExt;
use util::{ResultExt, paths::RemotePathBuf};
use wasm_host::{
WasmExtension, WasmHost,
wit::{is_supported_wasm_api_version, wasm_api_version_range},
@@ -1639,6 +1639,23 @@ impl ExtensionStore {
}
}
for (adapter_name, meta) in loaded_extension.manifest.debug_adapters.iter() {
let schema_path = &extension::build_debug_adapter_schema_path(adapter_name, meta);
if fs.is_file(&src_dir.join(schema_path)).await {
match schema_path.parent() {
Some(parent) => fs.create_dir(&tmp_dir.join(parent)).await?,
None => {}
}
fs.copy_file(
&src_dir.join(schema_path),
&tmp_dir.join(schema_path),
fs::CopyOptions::default(),
)
.await?
}
}
Ok(())
})
}
@@ -1653,7 +1670,7 @@ impl ExtensionStore {
.extensions
.iter()
.filter_map(|(id, entry)| {
if entry.manifest.language_servers.is_empty() {
if !entry.manifest.allow_remote_load() {
return None;
}
Some(proto::Extension {
@@ -1672,6 +1689,7 @@ impl ExtensionStore {
.request(proto::SyncExtensions { extensions })
})?
.await?;
let path_style = client.read_with(cx, |client, _| client.path_style())?;
for missing_extension in response.missing_extensions.into_iter() {
let tmp_dir = tempfile::tempdir()?;
@@ -1684,7 +1702,10 @@ impl ExtensionStore {
)
})?
.await?;
let dest_dir = PathBuf::from(&response.tmp_dir).join(missing_extension.clone().id);
let dest_dir = RemotePathBuf::new(
PathBuf::from(&response.tmp_dir).join(missing_extension.clone().id),
path_style,
);
log::info!("Uploading extension {}", missing_extension.clone().id);
client
@@ -1701,7 +1722,7 @@ impl ExtensionStore {
client
.update(cx, |client, _cx| {
client.proto_client().request(proto::InstallExtension {
tmp_dir: dest_dir.to_string_lossy().to_string(),
tmp_dir: dest_dir.to_proto(),
extension: Some(missing_extension),
})
})?

View File

@@ -1,11 +1,14 @@
use std::{path::PathBuf, sync::Arc};
use anyhow::{Context as _, Result};
use client::{TypedEnvelope, proto};
use client::{
TypedEnvelope,
proto::{self, FromProto},
};
use collections::{HashMap, HashSet};
use extension::{
Extension, ExtensionHostProxy, ExtensionLanguageProxy, ExtensionLanguageServerProxy,
ExtensionManifest,
Extension, ExtensionDebugAdapterProviderProxy, ExtensionHostProxy, ExtensionLanguageProxy,
ExtensionLanguageServerProxy, ExtensionManifest,
};
use fs::{Fs, RemoveOptions, RenameOptions};
use gpui::{App, AppContext as _, AsyncApp, Context, Entity, Task, WeakEntity};
@@ -125,7 +128,7 @@ impl HeadlessExtensionStore {
let manifest = Arc::new(ExtensionManifest::load(fs.clone(), &extension_dir).await?);
debug_assert!(!manifest.languages.is_empty() || !manifest.language_servers.is_empty());
debug_assert!(!manifest.languages.is_empty() || manifest.allow_remote_load());
if manifest.version.as_ref() != extension.version.as_str() {
anyhow::bail!(
@@ -165,12 +168,13 @@ impl HeadlessExtensionStore {
})?;
}
if manifest.language_servers.is_empty() {
if !manifest.allow_remote_load() {
return Ok(());
}
let wasm_extension: Arc<dyn Extension> =
Arc::new(WasmExtension::load(extension_dir, &manifest, wasm_host.clone(), &cx).await?);
let wasm_extension: Arc<dyn Extension> = Arc::new(
WasmExtension::load(extension_dir.clone(), &manifest, wasm_host.clone(), &cx).await?,
);
for (language_server_id, language_server_config) in &manifest.language_servers {
for language in language_server_config.languages() {
@@ -186,6 +190,28 @@ impl HeadlessExtensionStore {
);
})?;
}
log::info!("Loaded language server: {}", language_server_id);
}
for (debug_adapter, meta) in &manifest.debug_adapters {
let schema_path = extension::build_debug_adapter_schema_path(debug_adapter, meta);
this.update(cx, |this, _cx| {
this.proxy.register_debug_adapter(
wasm_extension.clone(),
debug_adapter.clone(),
&extension_dir.join(schema_path),
);
})?;
log::info!("Loaded debug adapter: {}", debug_adapter);
}
for debug_locator in manifest.debug_locators.keys() {
this.update(cx, |this, _cx| {
this.proxy
.register_debug_locator(wasm_extension.clone(), debug_locator.clone());
})?;
log::info!("Loaded debug locator: {}", debug_locator);
}
Ok(())
@@ -305,7 +331,7 @@ impl HeadlessExtensionStore {
version: extension.version,
dev: extension.dev,
},
PathBuf::from(envelope.payload.tmp_dir),
PathBuf::from_proto(envelope.payload.tmp_dir),
cx,
)
})?

View File

@@ -999,7 +999,7 @@ impl Extension {
) -> Result<Result<DebugRequest, String>> {
match self {
Extension::V0_6_0(ext) => {
let build_config_template = resolved_build_task.into();
let build_config_template = resolved_build_task.try_into()?;
let dap_request = ext
.call_run_dap_locator(store, &locator_name, &build_config_template)
.await?

View File

@@ -299,15 +299,17 @@ impl From<extension::DebugScenario> for DebugScenario {
}
}
impl From<SpawnInTerminal> for ResolvedTask {
fn from(value: SpawnInTerminal) -> Self {
Self {
impl TryFrom<SpawnInTerminal> for ResolvedTask {
type Error = anyhow::Error;
fn try_from(value: SpawnInTerminal) -> Result<Self, Self::Error> {
Ok(Self {
label: value.label,
command: value.command,
command: value.command.context("missing command")?,
args: value.args,
env: value.env.into_iter().collect(),
cwd: value.cwd.map(|s| s.to_string_lossy().into_owned()),
}
})
}
}

View File

@@ -92,6 +92,17 @@ impl FeatureFlag for JjUiFeatureFlag {
const NAME: &'static str = "jj-ui";
}
pub struct ZedCloudFeatureFlag {}
impl FeatureFlag for ZedCloudFeatureFlag {
const NAME: &'static str = "zed-cloud";
fn enabled_for_staff() -> bool {
// Require individual opt-in, for now.
false
}
}
pub trait FeatureFlagViewExt<V: 'static> {
fn observe_flag<T: FeatureFlag, F>(&mut self, window: &Window, callback: F) -> Subscription
where

View File

@@ -15,16 +15,14 @@ use std::{
};
use ui::{Context, LabelLike, ListItem, Window};
use ui::{HighlightedLabel, ListItemSpacing, prelude::*};
use util::{maybe, paths::compare_paths};
use util::{
maybe,
paths::{PathStyle, compare_paths},
};
use workspace::Workspace;
pub(crate) struct OpenPathPrompt;
#[cfg(target_os = "windows")]
const PROMPT_ROOT: &str = "C:\\";
#[cfg(not(target_os = "windows"))]
const PROMPT_ROOT: &str = "/";
#[derive(Debug)]
pub struct OpenPathDelegate {
tx: Option<oneshot::Sender<Option<Vec<PathBuf>>>>,
@@ -34,6 +32,8 @@ pub struct OpenPathDelegate {
string_matches: Vec<StringMatch>,
cancel_flag: Arc<AtomicBool>,
should_dismiss: bool,
prompt_root: String,
path_style: PathStyle,
replace_prompt: Task<()>,
}
@@ -42,6 +42,7 @@ impl OpenPathDelegate {
tx: oneshot::Sender<Option<Vec<PathBuf>>>,
lister: DirectoryLister,
creating_path: bool,
path_style: PathStyle,
) -> Self {
Self {
tx: Some(tx),
@@ -53,6 +54,11 @@ impl OpenPathDelegate {
string_matches: Vec::new(),
cancel_flag: Arc::new(AtomicBool::new(false)),
should_dismiss: true,
prompt_root: match path_style {
PathStyle::Posix => "/".to_string(),
PathStyle::Windows => "C:\\".to_string(),
},
path_style,
replace_prompt: Task::ready(()),
}
}
@@ -185,7 +191,8 @@ impl OpenPathPrompt {
cx: &mut Context<Workspace>,
) {
workspace.toggle_modal(window, cx, |window, cx| {
let delegate = OpenPathDelegate::new(tx, lister.clone(), creating_path);
let delegate =
OpenPathDelegate::new(tx, lister.clone(), creating_path, PathStyle::current());
let picker = Picker::uniform_list(delegate, window, cx).width(rems(34.));
let query = lister.default_query(cx);
picker.set_query(query, window, cx);
@@ -226,18 +233,7 @@ impl PickerDelegate for OpenPathDelegate {
cx: &mut Context<Picker<Self>>,
) -> Task<()> {
let lister = &self.lister;
let last_item = Path::new(&query)
.file_name()
.unwrap_or_default()
.to_string_lossy();
let (mut dir, suffix) = if let Some(dir) = query.strip_suffix(last_item.as_ref()) {
(dir.to_string(), last_item.into_owned())
} else {
(query, String::new())
};
if dir == "" {
dir = PROMPT_ROOT.to_string();
}
let (dir, suffix) = get_dir_and_suffix(query, self.path_style);
let query = match &self.directory_state {
DirectoryState::List { parent_path, .. } => {
@@ -266,6 +262,7 @@ impl PickerDelegate for OpenPathDelegate {
self.cancel_flag = Arc::new(AtomicBool::new(false));
let cancel_flag = self.cancel_flag.clone();
let parent_path_is_root = self.prompt_root == dir;
cx.spawn_in(window, async move |this, cx| {
if let Some(query) = query {
let paths = query.await;
@@ -279,7 +276,7 @@ impl PickerDelegate for OpenPathDelegate {
DirectoryState::None { create: false }
| DirectoryState::List { .. } => match paths {
Ok(paths) => DirectoryState::List {
entries: path_candidates(&dir, paths),
entries: path_candidates(parent_path_is_root, paths),
parent_path: dir.clone(),
error: None,
},
@@ -292,7 +289,7 @@ impl PickerDelegate for OpenPathDelegate {
DirectoryState::None { create: true }
| DirectoryState::Create { .. } => match paths {
Ok(paths) => {
let mut entries = path_candidates(&dir, paths);
let mut entries = path_candidates(parent_path_is_root, paths);
let mut exists = false;
let mut is_dir = false;
let mut new_id = None;
@@ -488,6 +485,7 @@ impl PickerDelegate for OpenPathDelegate {
_: &mut Context<Picker<Self>>,
) -> Option<String> {
let candidate = self.get_entry(self.selected_index)?;
let path_style = self.path_style;
Some(
maybe!({
match &self.directory_state {
@@ -496,7 +494,7 @@ impl PickerDelegate for OpenPathDelegate {
parent_path,
candidate.path.string,
if candidate.is_dir {
MAIN_SEPARATOR_STR
path_style.separator()
} else {
""
}
@@ -506,7 +504,7 @@ impl PickerDelegate for OpenPathDelegate {
parent_path,
candidate.path.string,
if candidate.is_dir {
MAIN_SEPARATOR_STR
path_style.separator()
} else {
""
}
@@ -527,8 +525,8 @@ impl PickerDelegate for OpenPathDelegate {
DirectoryState::None { .. } => return,
DirectoryState::List { parent_path, .. } => {
let confirmed_path =
if parent_path == PROMPT_ROOT && candidate.path.string.is_empty() {
PathBuf::from(PROMPT_ROOT)
if parent_path == &self.prompt_root && candidate.path.string.is_empty() {
PathBuf::from(&self.prompt_root)
} else {
Path::new(self.lister.resolve_tilde(parent_path, cx).as_ref())
.join(&candidate.path.string)
@@ -548,8 +546,8 @@ impl PickerDelegate for OpenPathDelegate {
return;
}
let prompted_path =
if parent_path == PROMPT_ROOT && user_input.file.string.is_empty() {
PathBuf::from(PROMPT_ROOT)
if parent_path == &self.prompt_root && user_input.file.string.is_empty() {
PathBuf::from(&self.prompt_root)
} else {
Path::new(self.lister.resolve_tilde(parent_path, cx).as_ref())
.join(&user_input.file.string)
@@ -652,8 +650,8 @@ impl PickerDelegate for OpenPathDelegate {
.inset(true)
.toggle_state(selected)
.child(HighlightedLabel::new(
if parent_path == PROMPT_ROOT {
format!("{}{}", PROMPT_ROOT, candidate.path.string)
if parent_path == &self.prompt_root {
format!("{}{}", self.prompt_root, candidate.path.string)
} else {
candidate.path.string.clone()
},
@@ -665,10 +663,10 @@ impl PickerDelegate for OpenPathDelegate {
user_input,
..
} => {
let (label, delta) = if parent_path == PROMPT_ROOT {
let (label, delta) = if parent_path == &self.prompt_root {
(
format!("{}{}", PROMPT_ROOT, candidate.path.string),
PROMPT_ROOT.len(),
format!("{}{}", self.prompt_root, candidate.path.string),
self.prompt_root.len(),
)
} else {
(candidate.path.string.clone(), 0)
@@ -751,8 +749,11 @@ impl PickerDelegate for OpenPathDelegate {
}
}
fn path_candidates(parent_path: &String, mut children: Vec<DirectoryItem>) -> Vec<CandidateInfo> {
if *parent_path == PROMPT_ROOT {
fn path_candidates(
parent_path_is_root: bool,
mut children: Vec<DirectoryItem>,
) -> Vec<CandidateInfo> {
if parent_path_is_root {
children.push(DirectoryItem {
is_dir: true,
path: PathBuf::default(),
@@ -769,3 +770,128 @@ fn path_candidates(parent_path: &String, mut children: Vec<DirectoryItem>) -> Ve
})
.collect()
}
#[cfg(target_os = "windows")]
fn get_dir_and_suffix(query: String, path_style: PathStyle) -> (String, String) {
let last_item = Path::new(&query)
.file_name()
.unwrap_or_default()
.to_string_lossy();
let (mut dir, suffix) = if let Some(dir) = query.strip_suffix(last_item.as_ref()) {
(dir.to_string(), last_item.into_owned())
} else {
(query.to_string(), String::new())
};
match path_style {
PathStyle::Posix => {
if dir.is_empty() {
dir = "/".to_string();
}
}
PathStyle::Windows => {
if dir.len() < 3 {
dir = "C:\\".to_string();
}
}
}
(dir, suffix)
}
#[cfg(not(target_os = "windows"))]
fn get_dir_and_suffix(query: String, path_style: PathStyle) -> (String, String) {
match path_style {
PathStyle::Posix => {
let (mut dir, suffix) = if let Some(index) = query.rfind('/') {
(query[..index].to_string(), query[index + 1..].to_string())
} else {
(query, String::new())
};
if !dir.ends_with('/') {
dir.push('/');
}
(dir, suffix)
}
PathStyle::Windows => {
let (mut dir, suffix) = if let Some(index) = query.rfind('\\') {
(query[..index].to_string(), query[index + 1..].to_string())
} else {
(query, String::new())
};
if dir.len() < 3 {
dir = "C:\\".to_string();
}
if !dir.ends_with('\\') {
dir.push('\\');
}
(dir, suffix)
}
}
}
#[cfg(test)]
mod tests {
use util::paths::PathStyle;
use crate::open_path_prompt::get_dir_and_suffix;
#[test]
fn test_get_dir_and_suffix_with_windows_style() {
let (dir, suffix) = get_dir_and_suffix("".into(), PathStyle::Windows);
assert_eq!(dir, "C:\\");
assert_eq!(suffix, "");
let (dir, suffix) = get_dir_and_suffix("C:".into(), PathStyle::Windows);
assert_eq!(dir, "C:\\");
assert_eq!(suffix, "");
let (dir, suffix) = get_dir_and_suffix("C:\\".into(), PathStyle::Windows);
assert_eq!(dir, "C:\\");
assert_eq!(suffix, "");
let (dir, suffix) = get_dir_and_suffix("C:\\Use".into(), PathStyle::Windows);
assert_eq!(dir, "C:\\");
assert_eq!(suffix, "Use");
let (dir, suffix) =
get_dir_and_suffix("C:\\Users\\Junkui\\Docum".into(), PathStyle::Windows);
assert_eq!(dir, "C:\\Users\\Junkui\\");
assert_eq!(suffix, "Docum");
let (dir, suffix) =
get_dir_and_suffix("C:\\Users\\Junkui\\Documents".into(), PathStyle::Windows);
assert_eq!(dir, "C:\\Users\\Junkui\\");
assert_eq!(suffix, "Documents");
let (dir, suffix) =
get_dir_and_suffix("C:\\Users\\Junkui\\Documents\\".into(), PathStyle::Windows);
assert_eq!(dir, "C:\\Users\\Junkui\\Documents\\");
assert_eq!(suffix, "");
}
#[test]
fn test_get_dir_and_suffix_with_posix_style() {
let (dir, suffix) = get_dir_and_suffix("".into(), PathStyle::Posix);
assert_eq!(dir, "/");
assert_eq!(suffix, "");
let (dir, suffix) = get_dir_and_suffix("/".into(), PathStyle::Posix);
assert_eq!(dir, "/");
assert_eq!(suffix, "");
let (dir, suffix) = get_dir_and_suffix("/Use".into(), PathStyle::Posix);
assert_eq!(dir, "/");
assert_eq!(suffix, "Use");
let (dir, suffix) = get_dir_and_suffix("/Users/Junkui/Docum".into(), PathStyle::Posix);
assert_eq!(dir, "/Users/Junkui/");
assert_eq!(suffix, "Docum");
let (dir, suffix) = get_dir_and_suffix("/Users/Junkui/Documents".into(), PathStyle::Posix);
assert_eq!(dir, "/Users/Junkui/");
assert_eq!(suffix, "Documents");
let (dir, suffix) = get_dir_and_suffix("/Users/Junkui/Documents/".into(), PathStyle::Posix);
assert_eq!(dir, "/Users/Junkui/Documents/");
assert_eq!(suffix, "");
}
}

View File

@@ -5,7 +5,7 @@ use picker::{Picker, PickerDelegate};
use project::Project;
use serde_json::json;
use ui::rems;
use util::path;
use util::{path, paths::PathStyle};
use workspace::{AppState, Workspace};
use crate::OpenPathDelegate;
@@ -37,7 +37,7 @@ async fn test_open_path_prompt(cx: &mut TestAppContext) {
let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
let (picker, cx) = build_open_path_prompt(project, false, cx);
let (picker, cx) = build_open_path_prompt(project, false, PathStyle::current(), cx);
let query = path!("/root");
insert_query(query, &picker, cx).await;
@@ -111,7 +111,7 @@ async fn test_open_path_prompt_completion(cx: &mut TestAppContext) {
let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
let (picker, cx) = build_open_path_prompt(project, false, cx);
let (picker, cx) = build_open_path_prompt(project, false, PathStyle::current(), cx);
// Confirm completion for the query "/root", since it's a directory, it should add a trailing slash.
let query = path!("/root");
@@ -186,7 +186,7 @@ async fn test_open_path_prompt_completion(cx: &mut TestAppContext) {
}
#[gpui::test]
#[cfg(target_os = "windows")]
#[cfg_attr(not(target_os = "windows"), ignore)]
async fn test_open_path_prompt_on_windows(cx: &mut TestAppContext) {
let app_state = init_test(cx);
app_state
@@ -204,7 +204,7 @@ async fn test_open_path_prompt_on_windows(cx: &mut TestAppContext) {
let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
let (picker, cx) = build_open_path_prompt(project, false, cx);
let (picker, cx) = build_open_path_prompt(project, false, PathStyle::current(), cx);
// Support both forward and backward slashes.
let query = "C:/root/";
@@ -251,6 +251,47 @@ async fn test_open_path_prompt_on_windows(cx: &mut TestAppContext) {
);
}
#[gpui::test]
#[cfg_attr(not(target_os = "windows"), ignore)]
async fn test_open_path_prompt_on_windows_with_remote(cx: &mut TestAppContext) {
let app_state = init_test(cx);
app_state
.fs
.as_fake()
.insert_tree(
"/root",
json!({
"a": "A",
"dir1": {},
"dir2": {}
}),
)
.await;
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
let (picker, cx) = build_open_path_prompt(project, false, PathStyle::Posix, cx);
let query = "/root/";
insert_query(query, &picker, cx).await;
assert_eq!(
collect_match_candidates(&picker, cx),
vec!["a", "dir1", "dir2"]
);
assert_eq!(confirm_completion(query, 0, &picker, cx), "/root/a");
// Confirm completion for the query "/root/d", selecting the second candidate "dir2", since it's a directory, it should add a trailing slash.
let query = "/root/d";
insert_query(query, &picker, cx).await;
assert_eq!(collect_match_candidates(&picker, cx), vec!["dir1", "dir2"]);
assert_eq!(confirm_completion(query, 1, &picker, cx), "/root/dir2/");
let query = "/root/d";
insert_query(query, &picker, cx).await;
assert_eq!(collect_match_candidates(&picker, cx), vec!["dir1", "dir2"]);
assert_eq!(confirm_completion(query, 0, &picker, cx), "/root/dir1/");
}
#[gpui::test]
async fn test_new_path_prompt(cx: &mut TestAppContext) {
let app_state = init_test(cx);
@@ -278,7 +319,7 @@ async fn test_new_path_prompt(cx: &mut TestAppContext) {
let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
let (picker, cx) = build_open_path_prompt(project, true, cx);
let (picker, cx) = build_open_path_prompt(project, true, PathStyle::current(), cx);
insert_query(path!("/root"), &picker, cx).await;
assert_eq!(collect_match_candidates(&picker, cx), vec!["root"]);
@@ -315,11 +356,12 @@ fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
fn build_open_path_prompt(
project: Entity<Project>,
creating_path: bool,
path_style: PathStyle,
cx: &mut TestAppContext,
) -> (Entity<Picker<OpenPathDelegate>>, &mut VisualTestContext) {
let (tx, _) = futures::channel::oneshot::channel();
let lister = project::DirectoryLister::Project(project.clone());
let delegate = OpenPathDelegate::new(tx, lister.clone(), creating_path);
let delegate = OpenPathDelegate::new(tx, lister.clone(), creating_path, path_style);
let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
(

View File

@@ -2844,7 +2844,7 @@ impl GitPanel {
PopoverMenu::new(id.into())
.trigger(
IconButton::new("overflow-menu-trigger", IconName::EllipsisVertical)
IconButton::new("overflow-menu-trigger", IconName::Ellipsis)
.icon_size(IconSize::Small)
.icon_color(Color::Muted),
)
@@ -2965,15 +2965,20 @@ impl GitPanel {
&self,
id: impl Into<ElementId>,
keybinding_target: Option<FocusHandle>,
cx: &mut Context<Self>,
) -> impl IntoElement {
PopoverMenu::new(id.into())
.trigger(
ui::ButtonLike::new_rounded_right("commit-split-button-right")
.layer(ui::ElevationIndex::ModalSurface)
.size(ui::ButtonSize::None)
.size(ButtonSize::None)
.child(
div()
h_flex()
.px_1()
.h_full()
.justify_center()
.border_l_1()
.border_color(cx.theme().colors().border)
.child(Icon::new(IconName::ChevronDownSmall).size(IconSize::XSmall)),
),
)
@@ -3066,6 +3071,7 @@ impl GitPanel {
Some(
self.panel_header_container(window, cx)
.px_2()
.justify_between()
.child(
panel_button(change_string)
.color(Color::Muted)
@@ -3080,23 +3086,25 @@ impl GitPanel {
})
}),
)
.child(div().flex_grow()) // spacer
.child(self.render_overflow_menu("overflow_menu"))
.child(div().w_2()) // another spacer
.child(
panel_filled_button(text)
.tooltip(Tooltip::for_action_title_in(
tooltip,
action.as_ref(),
&self.focus_handle,
))
.disabled(self.entry_count == 0)
.on_click(move |_, _, cx| {
let action = action.boxed_clone();
cx.defer(move |cx| {
cx.dispatch_action(action.as_ref());
})
}),
h_flex()
.gap_1()
.child(self.render_overflow_menu("overflow_menu"))
.child(
panel_filled_button(text)
.tooltip(Tooltip::for_action_title_in(
tooltip,
action.as_ref(),
&self.focus_handle,
))
.disabled(self.entry_count == 0)
.on_click(move |_, _, cx| {
let action = action.boxed_clone();
cx.defer(move |cx| {
cx.dispatch_action(action.as_ref());
})
}),
),
),
)
}
@@ -3174,7 +3182,7 @@ impl GitPanel {
.w_full()
.h(max_height + footer_size)
.border_t_1()
.border_color(cx.theme().colors().border_variant)
.border_color(cx.theme().colors().border)
.cursor_text()
.on_click(cx.listener(move |this, _: &ClickEvent, window, cx| {
window.focus(&this.commit_editor.focus_handle(cx));
@@ -3259,6 +3267,7 @@ impl GitPanel {
let (can_commit, tooltip) = self.configure_commit_button(cx);
let title = self.commit_button_title();
let commit_tooltip_focus_handle = self.commit_editor.focus_handle(cx);
div()
.id("commit-wrapper")
.on_hover(cx.listener(move |this, hovered, _, cx| {
@@ -3371,6 +3380,7 @@ impl GitPanel {
self.render_git_commit_menu(
ElementId::Name(format!("split-button-right-{}", title).into()),
Some(commit_tooltip_focus_handle.clone()),
cx,
)
.into_any_element(),
))
@@ -3415,8 +3425,8 @@ impl GitPanel {
fn render_pending_amend(&self, cx: &mut Context<Self>) -> impl IntoElement {
div()
.py_2()
.px(px(8.))
.p_2()
.border_t_1()
.border_color(cx.theme().colors().border)
.child(
Label::new(
@@ -3431,22 +3441,21 @@ impl GitPanel {
let branch = active_repository.read(cx).branch.as_ref()?;
let commit = branch.most_recent_commit.as_ref()?.clone();
let workspace = self.workspace.clone();
let this = cx.entity();
Some(
h_flex()
.items_center()
.py_2()
.px(px(8.))
.border_color(cx.theme().colors().border)
.py_1p5()
.px_2()
.gap_1p5()
.justify_between()
.border_t_1()
.border_color(cx.theme().colors().border.opacity(0.8))
.child(
div()
.flex_grow()
.overflow_hidden()
.items_center()
.max_w(relative(0.85))
.h_full()
.child(
Label::new(commit.subject.clone())
.size(LabelSize::Small)
@@ -3480,12 +3489,11 @@ impl GitPanel {
}
}),
)
.child(div().flex_1())
.when(commit.has_parent, |this| {
let has_unstaged = self.has_unstaged_changes();
this.child(
panel_icon_button("undo", IconName::Undo)
.icon_size(IconSize::Small)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.tooltip(move |window, cx| {
Tooltip::with_meta(
@@ -3507,43 +3515,38 @@ impl GitPanel {
}
fn render_empty_state(&self, cx: &mut Context<Self>) -> impl IntoElement {
h_flex()
.h_full()
.flex_grow()
.justify_center()
.items_center()
.child(
v_flex()
.gap_2()
.child(h_flex().w_full().justify_around().child(
if self.active_repository.is_some() {
"No changes to commit"
} else {
"No Git repositories"
},
))
.children({
let worktree_count = self.project.read(cx).visible_worktrees(cx).count();
(worktree_count > 0 && self.active_repository.is_none()).then(|| {
h_flex().w_full().justify_around().child(
panel_filled_button("Initialize Repository")
.tooltip(Tooltip::for_action_title_in(
"git init",
&git::Init,
&self.focus_handle,
))
.on_click(move |_, _, cx| {
cx.defer(move |cx| {
cx.dispatch_action(&git::Init);
})
}),
)
})
h_flex().h_full().flex_grow().justify_center().child(
v_flex()
.gap_2()
.child(h_flex().w_full().justify_around().child(
if self.active_repository.is_some() {
"No changes to commit"
} else {
"No Git repositories"
},
))
.children({
let worktree_count = self.project.read(cx).visible_worktrees(cx).count();
(worktree_count > 0 && self.active_repository.is_none()).then(|| {
h_flex().w_full().justify_around().child(
panel_filled_button("Initialize Repository")
.tooltip(Tooltip::for_action_title_in(
"git init",
&git::Init,
&self.focus_handle,
))
.on_click(move |_, _, cx| {
cx.defer(move |cx| {
cx.dispatch_action(&git::Init);
})
}),
)
})
.text_ui_sm(cx)
.mx_auto()
.text_color(Color::Placeholder.color(cx)),
)
})
.text_ui_sm(cx)
.mx_auto()
.text_color(Color::Placeholder.color(cx)),
)
}
fn render_vertical_scrollbar(
@@ -4621,7 +4624,7 @@ impl RenderOnce for PanelRepoFooter {
})
.trigger_with_tooltip(
repo_selector_trigger.disabled(single_repo).truncate(true),
Tooltip::text("Switch active repository"),
Tooltip::text("Switch Active Repository"),
)
.anchor(Corner::BottomLeft)
.into_any_element();

View File

@@ -487,7 +487,7 @@ impl Element for TextElement {
let font_size = style.font_size.to_pixels(window.rem_size());
let line = window
.text_system()
.shape_line(display_text, font_size, &runs);
.shape_line(display_text, font_size, &runs, None);
let cursor_pos = line.x_for_index(cursor);
let (selection, cursor) = if selected_range.is_empty() {

View File

@@ -7,8 +7,8 @@
use crate::{
AnyElement, App, AvailableSpace, Bounds, ContentMask, Element, ElementId, GlobalElementId,
Hitbox, InspectorElementId, InteractiveElement, Interactivity, IntoElement, IsZero, LayoutId,
ListSizingBehavior, Overflow, Pixels, ScrollHandle, Size, StyleRefinement, Styled, Window,
point, size,
ListSizingBehavior, Overflow, Pixels, Point, ScrollHandle, Size, StyleRefinement, Styled,
Window, point, size,
};
use smallvec::SmallVec;
use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
@@ -88,6 +88,8 @@ pub enum ScrollStrategy {
/// May not be possible if there's not enough list items above the item scrolled to:
/// in this case, the element will be placed at the closest possible position.
Center,
/// Scrolls the element to be at the given item index from the top of the viewport.
ToPosition(usize),
}
#[derive(Clone, Debug, Default)]
@@ -140,6 +142,15 @@ impl UniformListScrollHandle {
.map(|(ix, _)| ix)
.unwrap_or_else(|| this.base_handle.logical_scroll_top().0)
}
/// Checks if the list can be scrolled vertically.
pub fn is_scrollable(&self) -> bool {
if let Some(size) = self.0.borrow().last_item_size {
size.contents.height > size.item.height
} else {
false
}
}
}
impl Styled for UniformList {
@@ -345,6 +356,15 @@ impl Element for UniformList {
}
}
}
ScrollStrategy::ToPosition(sticky_index) => {
let target_y_in_viewport = item_height * sticky_index;
let target_scroll_top = item_top - target_y_in_viewport;
let max_scroll_top =
(content_height - list_height).max(Pixels::ZERO);
let new_scroll_top =
target_scroll_top.clamp(Pixels::ZERO, max_scroll_top);
updated_scroll_offset.y = -new_scroll_top;
}
}
scroll_offset = *updated_scroll_offset
}
@@ -354,6 +374,7 @@ impl Element for UniformList {
let last_visible_element_ix = ((-scroll_offset.y + padded_bounds.size.height)
/ item_height)
.ceil() as usize;
let visible_range = first_visible_element_ix
..cmp::min(last_visible_element_ix, self.item_count);
@@ -409,6 +430,7 @@ impl Element for UniformList {
let mut decoration = decoration.as_ref().compute(
visible_range.clone(),
bounds,
scroll_offset,
item_height,
self.item_count,
window,
@@ -476,6 +498,7 @@ pub trait UniformListDecoration {
&self,
visible_range: Range<usize>,
bounds: Bounds<Pixels>,
scroll_offset: Point<Pixels>,
item_height: Pixels,
item_count: usize,
window: &mut Window,
@@ -483,6 +506,35 @@ pub trait UniformListDecoration {
) -> AnyElement;
}
/// A trait for implementing top slots in a [`UniformList`].
/// Top slots are elements that appear at the top of the list and can adjust
/// the visible range of list items.
pub trait UniformListTopSlot {
/// Returns elements to render at the top slot for the given visible range.
fn compute(
&mut self,
visible_range: Range<usize>,
window: &mut Window,
cx: &mut App,
) -> SmallVec<[AnyElement; 8]>;
/// Layout and prepaint the top slot elements.
fn prepaint(
&self,
elements: &mut SmallVec<[AnyElement; 8]>,
bounds: Bounds<Pixels>,
item_height: Pixels,
scroll_offset: Point<Pixels>,
padding: crate::Edges<Pixels>,
can_scroll_horizontally: bool,
window: &mut Window,
cx: &mut App,
);
/// Paint the top slot elements.
fn paint(&self, elements: &mut SmallVec<[AnyElement; 8]>, window: &mut Window, cx: &mut App);
}
impl UniformList {
/// Selects a specific list item for measurement.
pub fn with_width_from_item(mut self, item_index: Option<usize>) -> Self {

View File

@@ -55,7 +55,7 @@ impl Keystroke {
///
/// This method assumes that `self` was typed and `target' is in the keymap, and checks
/// both possibilities for self against the target.
pub(crate) fn should_match(&self, target: &Keystroke) -> bool {
pub fn should_match(&self, target: &Keystroke) -> bool {
#[cfg(not(target_os = "windows"))]
if let Some(key_char) = self
.key_char

View File

@@ -200,8 +200,8 @@ impl<P: LinuxClient + 'static> Platform for P {
app_path = app_path.display()
);
// execute the script using /bin/bash
let restart_process = Command::new("/bin/bash")
let restart_process = Command::new("/usr/bin/env")
.arg("bash")
.arg("-c")
.arg(script)
.process_group(0)

View File

@@ -466,12 +466,7 @@ fn handle_keyup_msg(
}
fn handle_char_msg(wparam: WPARAM, state_ptr: Rc<WindowsWindowStatePtr>) -> Option<isize> {
let Some(input) = char::from_u32(wparam.0 as u32)
.filter(|c| !c.is_control())
.map(String::from)
else {
return Some(1);
};
let input = parse_char_message(wparam, &state_ptr)?;
with_input_handler(&state_ptr, |input_handler| {
input_handler.replace_text_in_range(None, &input);
});
@@ -1228,6 +1223,36 @@ fn handle_input_language_changed(
Some(0)
}
#[inline]
fn parse_char_message(wparam: WPARAM, state_ptr: &Rc<WindowsWindowStatePtr>) -> Option<String> {
let code_point = wparam.loword();
let mut lock = state_ptr.state.borrow_mut();
// https://www.unicode.org/versions/Unicode16.0.0/core-spec/chapter-3/#G2630
match code_point {
0xD800..=0xDBFF => {
// High surrogate, wait for low surrogate
lock.pending_surrogate = Some(code_point);
None
}
0xDC00..=0xDFFF => {
if let Some(high_surrogate) = lock.pending_surrogate.take() {
// Low surrogate, combine with pending high surrogate
String::from_utf16(&[high_surrogate, code_point]).ok()
} else {
// Invalid low surrogate without a preceding high surrogate
log::warn!(
"Received low surrogate without a preceding high surrogate: {code_point:x}"
);
None
}
}
_ => {
lock.pending_surrogate = None;
String::from_utf16(&[code_point]).ok()
}
}
}
#[inline]
fn translate_message(handle: HWND, wparam: WPARAM, lparam: LPARAM) {
let msg = MSG {
@@ -1270,6 +1295,10 @@ where
capslock: current_capslock(),
}))
}
VK_PACKET => {
translate_message(handle, wparam, lparam);
None
}
VK_CAPITAL => {
let capslock = current_capslock();
if state

View File

@@ -43,6 +43,7 @@ pub struct WindowsWindowState {
pub callbacks: Callbacks,
pub input_handler: Option<PlatformInputHandler>,
pub pending_surrogate: Option<u16>,
pub last_reported_modifiers: Option<Modifiers>,
pub last_reported_capslock: Option<Capslock>,
pub system_key_handled: bool,
@@ -105,6 +106,7 @@ impl WindowsWindowState {
let renderer = windows_renderer::init(gpu_context, hwnd, transparent)?;
let callbacks = Callbacks::default();
let input_handler = None;
let pending_surrogate = None;
let last_reported_modifiers = None;
let last_reported_capslock = None;
let system_key_handled = false;
@@ -126,6 +128,7 @@ impl WindowsWindowState {
min_size,
callbacks,
input_handler,
pending_surrogate,
last_reported_modifiers,
last_reported_capslock,
system_key_handled,

View File

@@ -357,6 +357,7 @@ impl WindowTextSystem {
text: SharedString,
font_size: Pixels,
runs: &[TextRun],
force_width: Option<Pixels>,
) -> ShapedLine {
debug_assert!(
text.find('\n').is_none(),
@@ -384,7 +385,7 @@ impl WindowTextSystem {
});
}
let layout = self.layout_line(&text, font_size, runs);
let layout = self.layout_line(&text, font_size, runs, force_width);
ShapedLine {
layout,
@@ -524,6 +525,7 @@ impl WindowTextSystem {
text: Text,
font_size: Pixels,
runs: &[TextRun],
force_width: Option<Pixels>,
) -> Arc<LineLayout>
where
Text: AsRef<str>,
@@ -544,9 +546,9 @@ impl WindowTextSystem {
});
}
let layout = self
.line_layout_cache
.layout_line(text, font_size, &font_runs);
let layout =
self.line_layout_cache
.layout_line_internal(text, font_size, &font_runs, force_width);
font_runs.clear();
self.font_runs_pool.lock().push(font_runs);

View File

@@ -482,6 +482,7 @@ impl LineLayoutCache {
font_size,
runs,
wrap_width,
force_width: None,
} as &dyn AsCacheKeyRef;
let current_frame = self.current_frame.upgradable_read();
@@ -516,6 +517,7 @@ impl LineLayoutCache {
font_size,
runs: SmallVec::from(runs),
wrap_width,
force_width: None,
});
let mut current_frame = self.current_frame.write();
@@ -534,6 +536,20 @@ impl LineLayoutCache {
font_size: Pixels,
runs: &[FontRun],
) -> Arc<LineLayout>
where
Text: AsRef<str>,
SharedString: From<Text>,
{
self.layout_line_internal(text, font_size, runs, None)
}
pub fn layout_line_internal<Text>(
&self,
text: Text,
font_size: Pixels,
runs: &[FontRun],
force_width: Option<Pixels>,
) -> Arc<LineLayout>
where
Text: AsRef<str>,
SharedString: From<Text>,
@@ -543,6 +559,7 @@ impl LineLayoutCache {
font_size,
runs,
wrap_width: None,
force_width,
} as &dyn AsCacheKeyRef;
let current_frame = self.current_frame.upgradable_read();
@@ -557,16 +574,30 @@ impl LineLayoutCache {
layout
} else {
let text = SharedString::from(text);
let layout = Arc::new(
self.platform_text_system
.layout_line(&text, font_size, runs),
);
let mut layout = self
.platform_text_system
.layout_line(&text, font_size, runs);
if let Some(force_width) = force_width {
let mut glyph_pos = 0;
for run in layout.runs.iter_mut() {
for glyph in run.glyphs.iter_mut() {
if (glyph.position.x - glyph_pos * force_width).abs() > px(1.) {
glyph.position.x = glyph_pos * force_width;
}
glyph_pos += 1;
}
}
}
let key = Arc::new(CacheKey {
text,
font_size,
runs: SmallVec::from(runs),
wrap_width: None,
force_width,
});
let layout = Arc::new(layout);
current_frame.lines.insert(key.clone(), layout.clone());
current_frame.used_lines.push(key);
layout
@@ -591,6 +622,7 @@ struct CacheKey {
font_size: Pixels,
runs: SmallVec<[FontRun; 1]>,
wrap_width: Option<Pixels>,
force_width: Option<Pixels>,
}
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
@@ -599,6 +631,7 @@ struct CacheKeyRef<'a> {
font_size: Pixels,
runs: &'a [FontRun],
wrap_width: Option<Pixels>,
force_width: Option<Pixels>,
}
impl PartialEq for (dyn AsCacheKeyRef + '_) {
@@ -622,6 +655,7 @@ impl AsCacheKeyRef for CacheKey {
font_size: self.font_size,
runs: self.runs.as_slice(),
wrap_width: self.wrap_width,
force_width: self.force_width,
}
}
}

View File

@@ -226,10 +226,21 @@ impl HttpClientWithUrl {
}
/// Builds a Zed LLM URL using the given path.
pub fn build_zed_llm_url(&self, path: &str, query: &[(&str, &str)]) -> Result<Url> {
pub fn build_zed_llm_url(
&self,
path: &str,
query: &[(&str, &str)],
use_cloud: bool,
) -> Result<Url> {
let base_url = self.base_url();
let base_api_url = match base_url.as_ref() {
"https://zed.dev" => "https://llm.zed.dev",
"https://zed.dev" => {
if use_cloud {
"https://cloud.zed.dev"
} else {
"https://llm.zed.dev"
}
}
"https://staging.zed.dev" => "https://llm-staging.zed.dev",
"http://localhost:3000" => "http://localhost:8787",
other => other,

View File

@@ -835,10 +835,6 @@ impl InlineCompletionButton {
cx.notify();
}
pub fn toggle_menu(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.popover_menu_handle.toggle(window, cx);
}
}
impl StatusItemView for InlineCompletionButton {

View File

@@ -26,7 +26,7 @@ use std::time::Duration;
use std::{fmt, io};
use thiserror::Error;
use util::serde::is_default;
use zed_llm_client::CompletionRequestStatus;
use zed_llm_client::{CompletionMode, CompletionRequestStatus};
pub use crate::model::*;
pub use crate::rate_limiter::*;
@@ -462,6 +462,10 @@ pub trait LanguageModel: Send + Sync {
}
fn max_token_count(&self) -> u64;
/// Returns the maximum token count for this model in burn mode (If `supports_burn_mode` is `false` this returns `None`)
fn max_token_count_in_burn_mode(&self) -> Option<u64> {
None
}
fn max_output_tokens(&self) -> Option<u64> {
None
}
@@ -557,6 +561,18 @@ pub trait LanguageModel: Send + Sync {
}
}
pub trait LanguageModelExt: LanguageModel {
fn max_token_count_for_mode(&self, mode: CompletionMode) -> u64 {
match mode {
CompletionMode::Normal => self.max_token_count(),
CompletionMode::Max => self
.max_token_count_in_burn_mode()
.unwrap_or_else(|| self.max_token_count()),
}
}
}
impl LanguageModelExt for dyn LanguageModel {}
pub trait LanguageModelTool: 'static + DeserializeOwned + JsonSchema {
fn name() -> String;
fn description() -> String;

View File

@@ -28,6 +28,7 @@ credentials_provider.workspace = true
copilot.workspace = true
deepseek = { workspace = true, features = ["schemars"] }
editor.workspace = true
feature_flags.workspace = true
fs.workspace = true
futures.workspace = true
google_ai = { workspace = true, features = ["schemars"] }

View File

@@ -2,6 +2,7 @@ use anthropic::AnthropicModelMode;
use anyhow::{Context as _, Result, anyhow};
use chrono::{DateTime, Utc};
use client::{Client, ModelRequestUsage, UserStore, zed_urls};
use feature_flags::{FeatureFlagAppExt as _, ZedCloudFeatureFlag};
use futures::{
AsyncBufReadExt, FutureExt, Stream, StreamExt, future::BoxFuture, stream::BoxStream,
};
@@ -136,6 +137,7 @@ impl State {
cx: &mut Context<Self>,
) -> Self {
let refresh_llm_token_listener = RefreshLlmTokenListener::global(cx);
let use_cloud = cx.has_flag::<ZedCloudFeatureFlag>();
Self {
client: client.clone(),
@@ -163,7 +165,7 @@ impl State {
.await;
}
let response = Self::fetch_models(client, llm_api_token).await?;
let response = Self::fetch_models(client, llm_api_token, use_cloud).await?;
cx.update(|cx| {
this.update(cx, |this, cx| {
let mut models = Vec::new();
@@ -265,13 +267,18 @@ impl State {
async fn fetch_models(
client: Arc<Client>,
llm_api_token: LlmApiToken,
use_cloud: bool,
) -> Result<ListModelsResponse> {
let http_client = &client.http_client();
let token = llm_api_token.acquire(&client).await?;
let request = http_client::Request::builder()
.method(Method::GET)
.uri(http_client.build_zed_llm_url("/models", &[])?.as_ref())
.uri(
http_client
.build_zed_llm_url("/models", &[], use_cloud)?
.as_ref(),
)
.header("Authorization", format!("Bearer {token}"))
.body(AsyncBody::empty())?;
let mut response = http_client
@@ -535,6 +542,7 @@ impl CloudLanguageModel {
llm_api_token: LlmApiToken,
app_version: Option<SemanticVersion>,
body: CompletionBody,
use_cloud: bool,
) -> Result<PerformLlmCompletionResponse> {
let http_client = &client.http_client();
@@ -542,9 +550,11 @@ impl CloudLanguageModel {
let mut refreshed_token = false;
loop {
let request_builder = http_client::Request::builder()
.method(Method::POST)
.uri(http_client.build_zed_llm_url("/completions", &[])?.as_ref());
let request_builder = http_client::Request::builder().method(Method::POST).uri(
http_client
.build_zed_llm_url("/completions", &[], use_cloud)?
.as_ref(),
);
let request_builder = if let Some(app_version) = app_version {
request_builder.header(ZED_VERSION_HEADER_NAME, app_version.to_string())
} else {
@@ -730,6 +740,13 @@ impl LanguageModel for CloudLanguageModel {
self.model.max_token_count as u64
}
fn max_token_count_in_burn_mode(&self) -> Option<u64> {
self.model
.max_token_count_in_max_mode
.filter(|_| self.model.supports_max_mode)
.map(|max_token_count| max_token_count as u64)
}
fn cache_configuration(&self) -> Option<LanguageModelCacheConfiguration> {
match &self.model.provider {
zed_llm_client::LanguageModelProvider::Anthropic => {
@@ -764,6 +781,7 @@ impl LanguageModel for CloudLanguageModel {
let model_id = self.model.id.to_string();
let generate_content_request =
into_google(request, model_id.clone(), GoogleModelMode::Default);
let use_cloud = cx.has_flag::<ZedCloudFeatureFlag>();
async move {
let http_client = &client.http_client();
let token = llm_api_token.acquire(&client).await?;
@@ -779,7 +797,7 @@ impl LanguageModel for CloudLanguageModel {
.method(Method::POST)
.uri(
http_client
.build_zed_llm_url("/count_tokens", &[])?
.build_zed_llm_url("/count_tokens", &[], use_cloud)?
.as_ref(),
)
.header("Content-Type", "application/json")
@@ -828,6 +846,9 @@ impl LanguageModel for CloudLanguageModel {
let intent = request.intent;
let mode = request.mode;
let app_version = cx.update(|cx| AppVersion::global(cx)).ok();
let use_cloud = cx
.update(|cx| cx.has_flag::<ZedCloudFeatureFlag>())
.unwrap_or(false);
match self.model.provider {
zed_llm_client::LanguageModelProvider::Anthropic => {
let request = into_anthropic(
@@ -865,6 +886,7 @@ impl LanguageModel for CloudLanguageModel {
provider_request: serde_json::to_value(&request)
.map_err(|e| anyhow!(e))?,
},
use_cloud,
)
.await
.map_err(|err| match err.downcast::<ApiError>() {
@@ -917,6 +939,7 @@ impl LanguageModel for CloudLanguageModel {
provider_request: serde_json::to_value(&request)
.map_err(|e| anyhow!(e))?,
},
use_cloud,
)
.await?;
@@ -957,6 +980,7 @@ impl LanguageModel for CloudLanguageModel {
provider_request: serde_json::to_value(&request)
.map_err(|e| anyhow!(e))?,
},
use_cloud,
)
.await?;

View File

@@ -30,6 +30,7 @@ use settings::SettingsStore;
use std::time::Duration;
use ui::prelude::*;
use util::debug_panic;
use zed_llm_client::CompletionIntent;
use super::anthropic::count_anthropic_tokens;
use super::google::count_google_tokens;
@@ -268,6 +269,19 @@ impl LanguageModel for CopilotChatLanguageModel {
LanguageModelCompletionError,
>,
> {
let is_user_initiated = request.intent.is_none_or(|intent| match intent {
CompletionIntent::UserPrompt
| CompletionIntent::ThreadContextSummarization
| CompletionIntent::InlineAssist
| CompletionIntent::TerminalInlineAssist
| CompletionIntent::GenerateGitCommitMessage => true,
CompletionIntent::ToolResults
| CompletionIntent::ThreadSummarization
| CompletionIntent::CreateFile
| CompletionIntent::EditFile => false,
});
let copilot_request = match into_copilot_chat(&self.model, request) {
Ok(request) => request,
Err(err) => return futures::future::ready(Err(err.into())).boxed(),
@@ -276,7 +290,8 @@ impl LanguageModel for CopilotChatLanguageModel {
let request_limiter = self.request_limiter.clone();
let future = cx.spawn(async move |cx| {
let request = CopilotChat::stream_completion(copilot_request, cx.clone());
let request =
CopilotChat::stream_completion(copilot_request, is_user_initiated, cx.clone());
request_limiter
.stream(async move {
let response = request.await?;

View File

@@ -24,7 +24,6 @@ gpui.workspace = true
itertools.workspace = true
language.workspace = true
lsp.workspace = true
picker.workspace = true
project.workspace = true
serde_json.workspace = true
settings.workspace = true

File diff suppressed because it is too large Load Diff

View File

@@ -25,7 +25,7 @@
receiver: (parameter_list
"(" @context
(parameter_declaration
name: (_) @name
name: (_) @context
type: (_) @context)
")" @context)
name: (field_identifier) @name

View File

@@ -1,9 +1,8 @@
(comment) @comment.inclusive
[
(string)
(template_string)
] @string
(string) @string
(template_string (string_fragment) @string)
(jsx_element) @element

View File

@@ -34,5 +34,4 @@ decrease_indent_patterns = [
{ pattern = "^\\s*else\\b.*:", valid_after = ["if", "elif", "for", "while", "except"] },
{ pattern = "^\\s*except\\b.*:", valid_after = ["try", "except"] },
{ pattern = "^\\s*finally\\b.*:", valid_after = ["try", "except", "else"] },
{ pattern = "^\\s*case\\b.*:", valid_after = ["match", "case"] }
]

View File

@@ -14,4 +14,4 @@
(else_clause) @start.else
(except_clause) @start.except
(finally_clause) @start.finally
(case_pattern) @start.case
(case_clause) @start.case

View File

@@ -1,9 +1,8 @@
(comment) @comment.inclusive
[
(string)
(template_string)
] @string
(string) @string
(template_string (string_fragment) @string)
(jsx_element) @element

View File

@@ -1,6 +1,9 @@
(comment) @comment.inclusive
(string) @string
(template_string (string_fragment) @string)
(_ value: (call_expression
function: (identifier) @function_name_before_type_arguments
type_arguments: (type_arguments)))

25
crates/net/Cargo.toml Normal file
View File

@@ -0,0 +1,25 @@
[package]
name = "net"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/net.rs"
doctest = false
[dependencies]
smol.workspace = true
workspace-hack.workspace = true
[target.'cfg(target_os = "windows")'.dependencies]
anyhow.workspace = true
async-io = "2.4"
windows.workspace = true
[dev-dependencies]
tempfile.workspace = true

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

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

View File

@@ -0,0 +1,69 @@
#[cfg(not(target_os = "windows"))]
pub use smol::net::unix::{UnixListener, UnixStream};
#[cfg(target_os = "windows")]
pub use windows::{UnixListener, UnixStream};
#[cfg(target_os = "windows")]
pub mod windows {
use std::{
io::Result,
path::Path,
pin::Pin,
task::{Context, Poll},
};
use smol::{
Async,
io::{AsyncRead, AsyncWrite},
};
pub struct UnixListener(Async<crate::UnixListener>);
impl UnixListener {
pub fn bind<P: AsRef<Path>>(path: P) -> Result<Self> {
Ok(UnixListener(Async::new(crate::UnixListener::bind(path)?)?))
}
pub async fn accept(&self) -> Result<(UnixStream, ())> {
let (sock, _) = self.0.read_with(|listener| listener.accept()).await?;
Ok((UnixStream(Async::new(sock)?), ()))
}
}
pub struct UnixStream(Async<crate::UnixStream>);
impl UnixStream {
pub async fn connect<P: AsRef<Path>>(path: P) -> Result<Self> {
Ok(UnixStream(Async::new(crate::UnixStream::connect(path)?)?))
}
}
impl AsyncRead for UnixStream {
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut [u8],
) -> Poll<Result<usize>> {
Pin::new(&mut self.0).poll_read(cx, buf)
}
}
impl AsyncWrite for UnixStream {
fn poll_write(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<Result<usize>> {
Pin::new(&mut self.0).poll_write(cx, buf)
}
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<()>> {
Pin::new(&mut self.0).poll_flush(cx)
}
fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<()>> {
Pin::new(&mut self.0).poll_close(cx)
}
}
}

View File

@@ -0,0 +1,45 @@
use std::{
io::Result,
os::windows::io::{AsSocket, BorrowedSocket},
path::Path,
};
use windows::Win32::Networking::WinSock::{SOCKADDR_UN, SOMAXCONN, bind, listen};
use crate::{
socket::UnixSocket,
stream::UnixStream,
util::{init, map_ret, sockaddr_un},
};
pub struct UnixListener(UnixSocket);
impl UnixListener {
pub fn bind<P: AsRef<Path>>(path: P) -> Result<Self> {
init();
let socket = UnixSocket::new()?;
let (addr, len) = sockaddr_un(path)?;
unsafe {
map_ret(bind(
socket.as_raw(),
&addr as *const _ as *const _,
len as i32,
))?;
map_ret(listen(socket.as_raw(), SOMAXCONN as _))?;
}
Ok(Self(socket))
}
pub fn accept(&self) -> Result<(UnixStream, ())> {
let mut storage = SOCKADDR_UN::default();
let mut len = std::mem::size_of_val(&storage) as i32;
let raw = self.0.accept(&mut storage as *mut _ as *mut _, &mut len)?;
Ok((UnixStream::new(raw), ()))
}
}
impl AsSocket for UnixListener {
fn as_socket(&self) -> BorrowedSocket<'_> {
unsafe { BorrowedSocket::borrow_raw(self.0.as_raw().0 as _) }
}
}

107
crates/net/src/net.rs Normal file
View File

@@ -0,0 +1,107 @@
pub mod async_net;
#[cfg(target_os = "windows")]
pub mod listener;
#[cfg(target_os = "windows")]
pub mod socket;
#[cfg(target_os = "windows")]
pub mod stream;
#[cfg(target_os = "windows")]
mod util;
#[cfg(target_os = "windows")]
pub use listener::*;
#[cfg(target_os = "windows")]
pub use socket::*;
#[cfg(not(target_os = "windows"))]
pub use std::os::unix::net::{UnixListener, UnixStream};
#[cfg(target_os = "windows")]
pub use stream::*;
#[cfg(test)]
mod tests {
use std::io::{Read, Write};
use smol::io::{AsyncReadExt, AsyncWriteExt};
const SERVER_MESSAGE: &str = "Connection closed";
const CLIENT_MESSAGE: &str = "Hello, server!";
const BUFFER_SIZE: usize = 32;
#[test]
fn test_windows_listener() -> std::io::Result<()> {
use crate::{UnixListener, UnixStream};
let temp = tempfile::tempdir()?;
let socket = temp.path().join("socket.sock");
let listener = UnixListener::bind(&socket)?;
// Server
let server = std::thread::spawn(move || {
let (mut stream, _) = listener.accept().unwrap();
// Read data from the client
let mut buffer = [0; BUFFER_SIZE];
let bytes_read = stream.read(&mut buffer).unwrap();
let string = String::from_utf8_lossy(&buffer[..bytes_read]);
assert_eq!(string, CLIENT_MESSAGE);
// Send a message back to the client
stream.write_all(SERVER_MESSAGE.as_bytes()).unwrap();
});
// Client
let mut client = UnixStream::connect(&socket)?;
// Send data to the server
client.write_all(CLIENT_MESSAGE.as_bytes())?;
let mut buffer = [0; BUFFER_SIZE];
// Read the response from the server
let bytes_read = client.read(&mut buffer)?;
let string = String::from_utf8_lossy(&buffer[..bytes_read]);
assert_eq!(string, SERVER_MESSAGE);
client.flush()?;
server.join().unwrap();
Ok(())
}
#[test]
fn test_unix_listener() -> std::io::Result<()> {
use crate::async_net::{UnixListener, UnixStream};
smol::block_on(async {
let temp = tempfile::tempdir()?;
let socket = temp.path().join("socket.sock");
let listener = UnixListener::bind(&socket)?;
// Server
let server = smol::spawn(async move {
let (mut stream, _) = listener.accept().await.unwrap();
// Read data from the client
let mut buffer = [0; BUFFER_SIZE];
let bytes_read = stream.read(&mut buffer).await.unwrap();
let string = String::from_utf8_lossy(&buffer[..bytes_read]);
assert_eq!(string, CLIENT_MESSAGE);
// Send a message back to the client
stream.write_all(SERVER_MESSAGE.as_bytes()).await.unwrap();
});
// Client
let mut client = UnixStream::connect(&socket).await?;
client.write_all(CLIENT_MESSAGE.as_bytes()).await?;
// Read the response from the server
let mut buffer = [0; BUFFER_SIZE];
let bytes_read = client.read(&mut buffer).await?;
let string = String::from_utf8_lossy(&buffer[..bytes_read]);
assert_eq!(string, "Connection closed");
client.flush().await?;
server.await;
Ok(())
})
}
}

59
crates/net/src/socket.rs Normal file
View File

@@ -0,0 +1,59 @@
use std::io::{Error, ErrorKind, Result};
use windows::Win32::{
Foundation::{HANDLE, HANDLE_FLAG_INHERIT, HANDLE_FLAGS, SetHandleInformation},
Networking::WinSock::{
AF_UNIX, SEND_RECV_FLAGS, SOCK_STREAM, SOCKADDR, SOCKET, WSA_FLAG_OVERLAPPED,
WSAEWOULDBLOCK, WSASocketW, accept, closesocket, recv, send,
},
};
use crate::util::map_ret;
pub struct UnixSocket(SOCKET);
impl UnixSocket {
pub fn new() -> Result<Self> {
unsafe {
let raw = WSASocketW(AF_UNIX as _, SOCK_STREAM.0, 0, None, 0, WSA_FLAG_OVERLAPPED)?;
SetHandleInformation(
HANDLE(raw.0 as _),
HANDLE_FLAG_INHERIT.0,
HANDLE_FLAGS::default(),
)?;
Ok(Self(raw))
}
}
pub(crate) fn as_raw(&self) -> SOCKET {
self.0
}
pub fn accept(&self, storage: *mut SOCKADDR, len: &mut i32) -> Result<Self> {
match unsafe { accept(self.0, Some(storage), Some(len)) } {
Ok(sock) => Ok(Self(sock)),
Err(err) => {
let wsa_err = unsafe { windows::Win32::Networking::WinSock::WSAGetLastError().0 };
if wsa_err == WSAEWOULDBLOCK.0 {
Err(Error::new(ErrorKind::WouldBlock, "accept would block"))
} else {
Err(err.into())
}
}
}
}
pub(crate) fn recv(&self, buf: &mut [u8]) -> Result<usize> {
map_ret(unsafe { recv(self.0, buf, SEND_RECV_FLAGS::default()) })
}
pub(crate) fn send(&self, buf: &[u8]) -> Result<usize> {
map_ret(unsafe { send(self.0, buf, SEND_RECV_FLAGS::default()) })
}
}
impl Drop for UnixSocket {
fn drop(&mut self) {
unsafe { closesocket(self.0) };
}
}

60
crates/net/src/stream.rs Normal file
View File

@@ -0,0 +1,60 @@
use std::{
io::{Read, Result, Write},
os::windows::io::{AsSocket, BorrowedSocket},
path::Path,
};
use async_io::IoSafe;
use windows::Win32::Networking::WinSock::connect;
use crate::{
socket::UnixSocket,
util::{init, map_ret, sockaddr_un},
};
pub struct UnixStream(UnixSocket);
unsafe impl IoSafe for UnixStream {}
impl UnixStream {
pub fn new(socket: UnixSocket) -> Self {
Self(socket)
}
pub fn connect<P: AsRef<Path>>(path: P) -> Result<Self> {
init();
unsafe {
let inner = UnixSocket::new()?;
let (addr, len) = sockaddr_un(path)?;
map_ret(connect(
inner.as_raw(),
&addr as *const _ as *const _,
len as i32,
))?;
Ok(Self(inner))
}
}
}
impl Read for UnixStream {
fn read(&mut self, buf: &mut [u8]) -> Result<usize> {
self.0.recv(buf)
}
}
impl Write for UnixStream {
fn write(&mut self, buf: &[u8]) -> Result<usize> {
self.0.send(buf)
}
fn flush(&mut self) -> Result<()> {
Ok(())
}
}
impl AsSocket for UnixStream {
fn as_socket(&self) -> BorrowedSocket<'_> {
unsafe { BorrowedSocket::borrow_raw(self.0.as_raw().0 as _) }
}
}

76
crates/net/src/util.rs Normal file
View File

@@ -0,0 +1,76 @@
use std::{
io::{Error, ErrorKind, Result},
path::Path,
sync::Once,
};
use windows::Win32::Networking::WinSock::{
ADDRESS_FAMILY, AF_UNIX, SOCKADDR_UN, SOCKET_ERROR, WSAGetLastError, WSAStartup,
};
pub(crate) fn init() {
static ONCE: Once = Once::new();
ONCE.call_once(|| unsafe {
let mut wsa_data = std::mem::zeroed();
let result = WSAStartup(0x202, &mut wsa_data);
if result != 0 {
panic!("WSAStartup failed: {}", result);
}
});
}
// https://devblogs.microsoft.com/commandline/af_unix-comes-to-windows/
pub(crate) fn sockaddr_un<P: AsRef<Path>>(path: P) -> Result<(SOCKADDR_UN, usize)> {
let mut addr = SOCKADDR_UN::default();
addr.sun_family = ADDRESS_FAMILY(AF_UNIX);
let bytes = path
.as_ref()
.to_str()
.map(|s| s.as_bytes())
.ok_or(ErrorKind::InvalidInput)?;
if bytes.contains(&0) {
return Err(Error::new(
ErrorKind::InvalidInput,
"paths may not contain interior null bytes",
));
}
if bytes.len() >= addr.sun_path.len() {
return Err(Error::new(
ErrorKind::InvalidInput,
"path must be shorter than SUN_LEN",
));
}
unsafe {
std::ptr::copy_nonoverlapping(
bytes.as_ptr(),
addr.sun_path.as_mut_ptr().cast(),
bytes.len(),
);
}
let mut len = sun_path_offset(&addr) + bytes.len();
match bytes.first() {
Some(&0) | None => {}
Some(_) => len += 1,
}
Ok((addr, len))
}
pub(crate) fn map_ret(ret: i32) -> Result<usize> {
if ret == SOCKET_ERROR {
Err(Error::from_raw_os_error(unsafe { WSAGetLastError().0 }))
} else {
Ok(ret as usize)
}
}
fn sun_path_offset(addr: &SOCKADDR_UN) -> usize {
// Work with an actual instance of the type since using a null pointer is UB
let base = addr as *const _ as usize;
let path = &addr.sun_path as *const _ as usize;
path - base
}

View File

@@ -67,10 +67,10 @@ pub fn panel_filled_button(label: impl Into<SharedString>) -> ui::Button {
pub fn panel_icon_button(id: impl Into<SharedString>, icon: IconName) -> ui::IconButton {
let id = ElementId::Name(id.into());
ui::IconButton::new(id, icon)
IconButton::new(id, icon)
// TODO: Change this once we use on_surface_bg in button_like
.layer(ui::ElevationIndex::ModalSurface)
.size(ui::ButtonSize::Compact)
}
pub fn panel_filled_icon_button(id: impl Into<SharedString>, icon: IconName) -> ui::IconButton {

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