Compare commits

..

151 Commits

Author SHA1 Message Date
Lukas Wirth
85c2dc909d rope: Improve panic message for out of bounds anchor_at_offset (#40256)
Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-10-15 14:36:58 +00:00
Conrad Irwin
c814b99fcb Bump collab min version (#40198)
Release Notes:

- Prevent using Zed before the auto-update bug when collaborating.
2025-10-15 08:30:58 -06:00
localcc
07ccff217a Fix duplicate WSL entries (#40255)
Release Notes:

- N/A
2025-10-15 16:11:18 +02:00
Lukas Wirth
8ab52f3491 editor: Fix SelectionsCollection::disjoint not being ordered correctly (#40249)
We've been seeing the occasional `cannot seek backwards` panic within
`SelectionsCollection` without means to reproduce.

I believe the cause is one of the callers of
`MutableSelectionsCollection::select` not passing a well formed
`Selection` where `start > end`, so this PR enforces the invariant in
`select` by swapping the fields and setting `reversed` as required as
the other mutator functions already do that as well.

We could also just assert this instead, but it callers usually won't
care about this so its the less user facing annoyance to just fix this
invariant up internally.

Fixes ZED-253
Fixes ZED-ZJ
Fixes ZED-23S
Fixes ZED-222
Fixes ZED-1ZV
Fixes ZED-1SN
Fixes ZED-1Z0
Fixes ZED-10E
Fixes ZED-1X0
Fixes ZED-12M
Fixes ZED-1GR
Fixes ZED-1VE
Fixes ZED-13X
Fixes ZED-1G4

Release Notes:

- Fixed occasional panics when querying selections
2025-10-15 13:55:00 +00:00
localcc
ecf410e57d Improve musl libc detection (#40254)
Release Notes:

- N/A
2025-10-15 15:44:47 +02:00
Lukas Wirth
ec0eeaf69d rope: Assert utf8 boundary of start of Chunks::new range (#40253)
We seem to run into panics in related code, so better assert early

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-10-15 13:28:51 +00:00
Agus Zubiaga
376335496d zeta2: Numbered lines prompt format (#40218)
Adds a new `NumberedLines` format which is similar to `MarkedExcerpt`
but each line is prefixed with its line number.

Also fixes a bug where contagious snippets wouldn't get merged.

Release Notes:

- N/A

---------

Co-authored-by: Michael Sloan <mgsloan@gmail.com>
Co-authored-by: Michael <michael@zed.dev>
2025-10-15 09:35:39 -03:00
Ben Brandt
4f656cedfa acp: Fix /logout for agents that support it (#40248)
We were clearing the message editor too early. We only want to clear the
message editor if we are going to short circuit and return early before
submitting.
Otherwise, the agents that can handle this themselves won't have the
ability to do so.

Release Notes:

- acp: Fix /logout not working for some agents
2025-10-15 12:33:17 +00:00
Ben Brandt
0e9ee3cb55 docs: Add section for configuring Codex (#40250)
Release Notes:

- N/A
2025-10-15 14:29:01 +02:00
Lukas Wirth
bbe764794d agent_servers: Honor terminal settings provided shell when fetching shell env (#40243)
Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-10-15 10:32:03 +00:00
Lukas Wirth
3882323f79 language: Assert CodeLabel text ranges are correct (#40242)
Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-10-15 10:16:56 +00:00
Lukas Wirth
b0b83ef5aa markdown_preview: Fix markdown parser producing invalid link highlights (#40239)
Fixes ZED-1YC
Fixes ZED-1YK

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-10-15 08:54:39 +00:00
Ben Brandt
7beae757b8 acp: Allow updating default mode for Codex (#40238)
Release Notes:

- acp: Save default mode for codex
2025-10-15 08:46:47 +00:00
Lukas Wirth
a6e99c1c16 project: Always use shell env in LocalLspAdapterDelegate::which (#40237)
Windows not having a default shell does not matter here, we might still
have an environment from other means (by being spawned from the cli for
example).

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-10-15 08:06:37 +00:00
Julia Ryan
6d8d2e2989 Make help docs platform specific (#40194)
No need to clutter the `--help` docs with default directories for
platforms other than the current one.

Release Notes:

- N/A

Co-authored-by: David Kleingeld <davidsk@zed.dev>
2025-10-15 07:35:50 +00:00
Djordje
877790a105 docs: Remove duplicate Grok 4 Fast entry in models.md (#40232)
Release Notes:

- N/A
2025-10-15 07:32:01 +00:00
Conrad Irwin
0c08bbca05 Avoid gap between titlebar and body on linux (#40228)
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Co-authored-by: John Tur <john-tur@outlook.com>

Release Notes:

- N/A

Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Co-authored-by: John Tur <john-tur@outlook.com>
2025-10-15 04:44:00 +00:00
Cole Miller
ba0b68779d Fix triggers for debugger thread and session lists not rendering (#40227)
Release Notes:

- N/A
2025-10-15 04:36:04 +00:00
Cole Miller
45af5e4239 Fix a couple of bugs in remote browser debugging implementation (#40225)
Follow-up to #39248 

- Correctly forward ports over SSH, including the port from the debug
scenario's `url`
- Give the companion time to start up, instead of bailing if the first
connection attempt fails

Release Notes:

- Fixed not being able to launch a browser debugging session in an SSH
project.
2025-10-14 23:05:19 -04:00
Mikayla Maki
01f9b1e9b4 chore: VSCode -> VS Code (#40224)
Release Notes:

- N/A
2025-10-15 02:21:37 +00:00
Mikayla Maki
635b71c486 chore: Delete main.py (#40221)
Release Notes:

- N/A
2025-10-15 01:32:46 +00:00
Mikayla Maki
c4a7552a04 Bump Zed to v0.210 (#40219)
Release Notes:

- N/A
2025-10-15 01:10:56 +00:00
Mikayla Maki
918aee550c docs: Update releases.md (#40220)
Release Notes:

- N/A
2025-10-15 00:55:40 +00:00
Ben Kunkle
5c194f7cdc settings_ui: Last minute cleanup (#40217)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...

---------

Co-authored-by: Mikayla <mikayla@zed.dev>
2025-10-14 23:51:03 +00:00
Cole Miller
54df5812d9 windows: Add some trace-level logging to help dig into missing FS update bugs (#40200)
Related to https://github.com/zed-industries/zed/issues/38109

Release Notes:

- N/A
2025-10-14 23:12:30 +00:00
Mikayla Maki
0d84651f14 Implement 3+ file switcher (#40214)
Release Notes:

- N/A
2025-10-14 23:08:15 +00:00
Cole Miller
06af052e6d windows: Temporarily use preview release of Gemini CLI (#40212)
Workaround for disagreement about line endings that's fixed in the
v0.9.0 series

Release Notes:

- N/A
2025-10-14 18:07:51 -04:00
Jakub Konka
f1786b3b5f terminal: Simplify task_summary processing (#40201)
Release Notes:

- N/A
2025-10-14 21:32:07 +02:00
John Tur
f348240a8c Don't probe for local workspaces pointing to WSL filesystem on startup (#40142)
We automatically delete a local workspace if the folders comprising it
no longer exist.
If a local workspace points to folders in the WSL filesystem, checking
whether those folders exist will make us wait for the WSL VM and file
server to boot up. This can block Zed startup for many seconds.

Supported scenarios use remote workspaces, so delete these local
workspaces to ensure that we don't try to access their folders on the
startup path.

Release Notes:

- N/A
2025-10-14 14:24:03 -04:00
AidanV
762fa9b3c7 vim: Decrease max vim count (#40059)
Release Notes:

- Fixes bug were typing `9999999999999999999j` (19 9's) would go up
instead of down
- Max Vim count is now isize::MAX - 1
2025-10-14 12:07:26 -06:00
Agus Zubiaga
1bd34e0db0 zeta2 cli: Export retrieval stats data frame (#40145)
Retrieval stats will now use polars to build a big data frame for
references with the cartesian product of LSP declarations and retrieved
declaration candidates (with all their score components) and rebuilds
the stats summary on top of it.

This data frame is written to a `.parquet` file, which we can load into
advanced analytics tools (such as Metabase), so we can explore our
scoring distributions and find ways to improve retrieval, and then train
the decision tree.

Release Notes:

- N/A
2025-10-14 13:34:07 -03:00
Conrad Irwin
ce696c18ed Remove ping/unwrap from crash handler (#39870)
Release Notes:

- N/A

---------

Co-authored-by: Julia Ryan <juliaryan3.14@gmail.com>
2025-10-14 16:32:25 +00:00
Xiaobo Liu
9d23527663 util: Respect user-defined SHELL environment variable (#40181)
Fix issue where Zed would unconditionally override user's custom shell
with system default from passwd entry.

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

Release Notes:

- Fix issue where Zed would unconditionally override user's custom shell
with system default from passwd entry.

---------

Signed-off-by: Xiaobo Liu <cppcoffee@gmail.com>
2025-10-14 15:48:24 +00:00
Bennet Fenner
fc2b3b2e45 agent: Remove unused HistoryStore (#40187)
Release Notes:

- N/A
2025-10-14 15:23:47 +00:00
Yordis Prieto
8c7fb26af0 acp tools: Add button to copy all observed messages (#40076)
Added a "Copy All Messages" button to the ACP logs toolbar that copies
all messages in the watched stream to the clipboard as structured JSON.

## Motivation

When troubleshooting ACP protocol implementations, it's helpful to
provide the entire message thread to an LLM for analysis. Previously, I
had to copy individual messages one at a time, which was tedious and
time-consuming. This feature allows copying the entire conversation
history in a single click.

Release Notes:

- Added: Copy All Messages button to ACP logs view

---------

Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
Co-authored-by: Agus Zubiaga <agus@zed.dev>
2025-10-14 15:10:45 +00:00
Danilo Leal
867b5df070 settings_ui: Only allow to reset a setting to default in the file in which it was customized (#40182)
Plus some other tiny visual adjustments.

Release Notes:

- N/A
2025-10-14 14:14:16 +00:00
localcc
c5bbd556ea Add rust-analyzer support for musl linux (#40108)
Release Notes:

- Added rust-analyzer support for musl remotes
2025-10-14 15:48:05 +02:00
Delvin
4a84b78093 collab_ui: Make collaboration panel label responsive on resize (#40157)
Closes #40156

Release Notes:

- Fixed collaboration panel label responsive on resize
<img width="350" height="829" alt="Screenshot 2025-10-14 at 2 52 58 pm"
src="https://github.com/user-attachments/assets/94e21f1b-83a2-44f0-9f15-44a85155fda9"
/>
2025-10-14 13:38:27 +00:00
Abdelhakim Qbaich
fd63d432e9 Remove obsolete contents tool and add open to write profile (#40131)
`contents` doesn't exist anymore.
`open` was only set for `ask` and not `write`.

Release Notes:

- N/A
2025-10-14 10:18:52 -03:00
Bartosz Kaszubowski
ab70555a8a git_ui: Apply accented color to links in Blame tooltip (#40124)
# Why

Follow up to:
* #39905

# How

Apply accented color to links in message content inside Blame tooltip,
to match appearance in Markdown Preview panel.

Release Notes:

- Improved appearance of links in message content inside Blame tooltip.

# Preview

### Before

<img width="1186" height="798" alt="Screenshot 2025-10-13 at 19 33 37"
src="https://github.com/user-attachments/assets/33ab4fb5-7910-4d28-9152-c692d6ddeaa6"
/>

### After

<img width="1186" height="798" alt="Screenshot 2025-10-13 at 19 33 10"
src="https://github.com/user-attachments/assets/38082c5c-50d6-4fb3-90ca-410accff9aad"
/>

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-10-14 13:16:05 +00:00
Bartosz Kaszubowski
474eb8db77 git_ui: Layout/spacing tweaks for Blame tooltip (#40130)
# Why

Spotted that spacing of different Blame tooltip elements are spaced
uneven, also the fact that message content disappears on scroll before
reaching border felt a bit odd.

# How

Layout/spacing tweaks for Blame tooltip.

Release Notes:

- Improved appearance of Git Blame tooltip.

# Preview

### Before

<img width="1034" height="702" alt="Screenshot 2025-10-13 at 20 01 07"
src="https://github.com/user-attachments/assets/0c2715d5-d8fa-41dc-b891-a320a74d6fb0"
/>

<img width="1006" height="410" alt="Screenshot 2025-10-13 at 20 06 15"
src="https://github.com/user-attachments/assets/8c16f6dc-58e5-46cc-83fb-dd71a63e7557"
/>


### After

<img width="1034" height="672" alt="Screenshot 2025-10-13 at 20 00 33"
src="https://github.com/user-attachments/assets/e22e0e42-676e-411a-8773-2e57cdaaab17"
/>

<img width="1006" height="370" alt="Screenshot 2025-10-13 at 20 06 55"
src="https://github.com/user-attachments/assets/761995a9-153a-4e5d-923b-e7fbd73dc475"
/>

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-10-14 13:13:19 +00:00
Bennet Fenner
da5f25d9b0 acp: Hide completion menu when typing slash command argument (#40126)
Release Notes:

- acp: Fix an issue where the completion menu would still be active
after confirming a slash command
2025-10-14 13:02:03 +00:00
Cole Miller
83ba05eb32 windows: Revert "windows: Fix ascent/descent calculations (#40103)" (#40175)
This reverts commit f1db1f3a3c.

This seems to have affected the vertical positioning of text that
doesn't contain emojis in a way that was unintended.

Release Notes:

- N/A
2025-10-14 12:32:16 +00:00
Piotr Osiewicz
da583e5943 Revert "fs: Replace a bunch of uses of smol::fs with manual impls" (#40170)
Reverts zed-industries/zed#39906

This PR should not have landed prior to Wednesday.
2025-10-14 11:04:40 +00:00
Piotr Osiewicz
9ad6196150 fs: Replace a bunch of uses of smol::fs with manual impls (#39906)
smol::fs uses a separate threadpool, which is a bit yuck.

Release Notes:

- N/A
2025-10-14 10:38:26 +00:00
Smit Barmase
d4cc4f8ca7 editor: Fix highlight and selection overlap causing flicker while selecting (#40168)
Regressed in https://github.com/zed-industries/zed/pull/39857, only on
Nightly.

Release Notes:

- N/A
2025-10-14 16:05:21 +05:30
Ben Brandt
c61429e166 acp: Pass through experimental capability for terminal output (#40165)
Release Notes:

- N/A
2025-10-14 09:02:34 +00:00
Ben Brandt
4c70d55546 acp: Don't collapse tool calls by default (#40164)
Previously, if a tool call's output was just text, it would be collapsed
with no way to open it.

Now we track the collapsed cards instead of the expanded ones to allow
all tool calls to be expanded by default, and only collapse the ones
required by settings changes

Release Notes:

- acp: Fix tool call markdown output unintentionally being collapsed by
default
2025-10-14 08:55:34 +00:00
Lukas Wirth
025938b4a5 remote: Wrap uname invocation in sh for nu shell (#40084)
Closes https://github.com/zed-industries/zed/pull/39994

Release Notes:

- Fixed remoting not working when nushell is set as the default shell on
the remote target
2025-10-14 06:51:08 +00:00
Mikayla Maki
cc9af8d036 gpui 0.2.1 (#40158)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-10-14 05:05:55 +00:00
Mikayla Maki
ee60d5855c gpui: Update dependency package names (#40143)
This moves some of the changes made in
https://github.com/zed-industries/zed/pull/39543 to the `publish_gpui`
script.

This PR also updates that script to use `gpui_` instead of `zed-` (where
possible)

Release Notes:

- N/A
2025-10-14 04:43:28 +00:00
Cole Miller
97f398e677 windows: Prefer Git Bash for external agent terminals (#40150)
This applies the same change as #39466 to the terminal codepath for
external agents.

Release Notes:

- N/A

Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
2025-10-13 23:41:22 +00:00
Max Brunsfeld
6a2bad4e11 Load env vars from login shell in remote server (#40148)
Fixes a bug mentioned in
https://github.com/zed-industries/zed/issues/38891

Release Notes:

- Fixed a bug where environment variables like `NODE_EXTRA_CA_CERTS`
were not loaded from the user's shell initialization scripts in WSL or
SSH remote projects.

Co-authored-by: Cole Miller <cole@zed.dev>
2025-10-13 16:09:53 -07:00
Danilo Leal
ad4a53c71c agent: Fix review button not working while not focused in the message editor (#40144)
This PR fixes a bug where the review icon button wouldn't properly open
the review tab if you weren't focused in the agent panel's message
editor. The solution was to register the action also at the workspace
level.

Release Notes:

- agent: Fixed a bug where the review icon button wouldn't work to open
the review tab if focus weren't in the panel's message editor.
2025-10-13 18:55:31 -03:00
Ben Kunkle
160fca029c settings_ui: Move LSP & tool settings from Editor to Language page (#40140)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-10-13 17:40:44 -04:00
Cole Miller
6a1648825c windows: Detect when python3 is not usable and notify the user (#40070)
Fixes #39998

Debugpy and pylsp are installed in a Zed-global venv with pip. We need a
Python interpreter to create this venv when it doesn't exist and one of
these tools needs to be installed, and sometimes we attempt to use
`python3` from `$PATH`. This can cause issues on Windows, where out of
the box `python3` is a sort of shim that opens the Microsoft Store app.

This PR changes the debugpy installation path to create the Zed-global
venv using the Python interpreter from a venv in the project, and only
use python3 from `$PATH` if that fails. That matches how pylsp
installation already works. It also tightens up how we search for a
global Python installation by doing a basic sanity check (`python3 -c
'print(1 + 2)`) before accepting it, which should catch the Windows
shim.

Release Notes:

- windows: improved the behavior of Zed in situations where no global
Python installation exists.
2025-10-13 21:11:33 +00:00
Ben Kunkle
f0d097c66a settings_ui: Implement reset to default button (#40135)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-10-13 16:24:45 -04:00
0x2CA
a3bcf6fe21 windows: Fix shader rotation order for pattern rendering (#39993)
old

<img width="1076" height="1008" alt="image"
src="https://github.com/user-attachments/assets/e1cd8238-e869-4abb-98b4-4790467c59d1"
/>


new

<img width="989" height="1004" alt="image"
src="https://github.com/user-attachments/assets/42b7fd59-0038-4490-82a7-979983da5416"
/>


Release Notes:

- N/A
2025-10-13 22:12:58 +02:00
Danilo Leal
ac8e2f0576 Add .ZedSans as a possible fallback font (#40129)
Closes https://github.com/zed-industries/zed/issues/40121

Release Notes:

- Fixes a bug where users couldn't return the UI font family to the
default value through the UI.
2025-10-13 15:23:41 -03:00
Piotr Osiewicz
5c4649bd37 workspace: Fix auto-reveal-in-project-panel for Images, Notebooks and.. Terminals? (#40128)
This regressed in #39199

Release Notes:

- Fixed image files not getting auto-revealed in project panel.
2025-10-13 18:19:00 +00:00
Ben Kunkle
bd13c90acc settings_ui: Dynamic languages list (#40123)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-10-13 13:47:14 -04:00
Ben Kunkle
997f6c6a19 settings: Make "auto" and "language_server" valid format steps (#40113)
Follow up for: #39983 and
https://github.com/zed-industries/zed/pull/40040#issuecomment-3393902691

Previously it was possible to have formatting done using prettier or
language server using `"formatter": "auto"` and specify code actions to
apply on format using the `"code_actions_on_format"` setting. However,
post #39983 this is no longer possible due to the removal of the
`"code_actions_on_format"` setting. To rectify this regression, this PR
makes it so that the `"auto"` and `"language_server"` strings that were
previously only allowed as top level values on the `"formatter"` key,
are now allowed as format steps like so:
```json
{
      "formatter": ["auto", "language_server"]
}
```

Therefore to replicate the previous behavior using `"auto"` and
`"code_actions_on_format"` you can use the following configuration:

```json
{
      "formatter": [{"code_action": ...}, "auto"]
}
```

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-10-13 12:56:40 -04:00
Abdelhakim Qbaich
8dfbafd345 Fix inconsistent font size in toolbar code actions (#40120)
Release Notes:

- N/A

<img width="695" height="254" alt="before"
src="https://github.com/user-attachments/assets/7b180fb8-a6d3-409a-a0ee-1def447e8235"
/>
<img width="695" height="254" alt="after"
src="https://github.com/user-attachments/assets/3a035be0-3b74-433b-b0e7-5766b67bfcc1"
/>
2025-10-13 16:49:42 +00:00
John Tur
677d6acc9d Use DwmFlush unconditionally for Windows vsync (#39913)
Closes #36934

I'm still experiencing bugs with the
`DCompositionWaitForCompositorClock` API. Let's back out the support for
now until the fixes are identified and widely available.

`DwmFlush` does various things that aren't just waiting for VSync, so
it's not ideal, but it's not bad enough that it's worth a bigger
refactor right now.

Release Notes:

- N/A
2025-10-13 12:40:00 -04:00
Jakub Konka
96add6c9de remote: Check if remote can --exec, fall back to spawning shell otherwise (#40112)
Bonus: fix passing env vars to the proxy server in WSL setting.

Closes #39710
Supersedes #39893

Release Notes:

- N/A
2025-10-13 18:38:31 +02:00
Lukas Wirth
f76eecd758 terminal: Bump sysinfo crate (#39681)
Release Notes:

- N/A *or* Added/Fixed/Improved ...

Co-authored-by: dino <dinojoaocosta@gmail.com>
2025-10-13 16:31:00 +00:00
Bennet Fenner
bec2bfeb8b acp: Clear message editor after running /login (#40116)
Release Notes:

- N/A
2025-10-13 16:09:42 +00:00
Cole Miller
9edf1f8f04 Add a comment about the use of shell_kind in terminal.rs (#40114)
Release Notes:

- N/A

Co-authored-by: Jakub Konka <kubkon@jakubkonka.com>
2025-10-13 15:36:40 +00:00
Bennet Fenner
23fe74ebc5 acp: Fix slash command hint showing up after sending message (#40109)
Release Notes:

- N/A
2025-10-13 17:20:16 +02:00
CharlesChen0823
46fff9979d remove_server: Add function to delete wsl project (#40105)
As title say, could delete wsl project in `open remote` delegate.

Release Notes:

- Added ability to delete wsl projects from remote picker
2025-10-13 17:19:39 +02:00
Danilo Leal
e7b19ab0b1 settings_ui: Add some AI settings (#40111)
There's a lot of AI settings that will require custom UI for them to be
part of the settings window, but many don't (simple booleans and
dropdown) and can be moved right away. In consequence, the whole
"General Settings" section in the agent panel's settings view can be
removed given all of those items are now part of the settings window.

Release Notes:

- N/A
2025-10-13 12:19:17 -03:00
Danilo Leal
ce8d5e41a5 settings_ui: Make arrow keys up and down activate page content (#40106)
Release Notes:

- settings ui: Navigating the settings navbar with arrow keys up and
down now also activates the page, allowing users to more quickly see the
content for a given page before moving focus to the page itself.
2025-10-13 12:19:05 -03:00
Cole Miller
dac5725246 windows: Fix semantic merge conflict with ShellKind::new (#40107)
Release Notes:

- N/A
2025-10-13 14:30:40 +00:00
Cole Miller
f1db1f3a3c windows: Fix ascent/descent calculations (#40103)
This applies the same fix as #39886 for Windows.

Previously we were using `GetLineMetrics` to determine the ascent and
descent values for each line. It seems like this has the same behavior
as `GetTypographicBounds` on macOS, which is to return the minimum
ascent and descent for the current state of the `TextLayout` object.
This causes the ascent/descent to be unstable when adding or removing an
emoji because a font fallback is triggered when an emoji is present on
the line.

The issue is fixed by switching to `font.GetMetrics` to get the ascent
and descent, which should always return stable values for the main font,
instead of changing when there's a fallback. This also should support
situations where we have multiple explicit fonts on the same line,
although that probably can't be triggered in Zed right now.

Release Notes:

- windows: Fixed a vertical shift in text layout when inserting or
removing an emoji.

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
2025-10-13 09:49:07 -04:00
Marco Mihai Condrache
02bdba80a4 util: Fix shell kind in windows based on program path (#39696)
Closes #39614

The `ShellKind` struct is built on Windows' side, meaning that when
connecting to remotes, we fall back to PowerShell construction, even if
the shell program we are spawning is a unix program.

This broke tasks creation since we are using the shell kind to construct
args:


d04ac864b8/crates/project/src/terminals.rs (L149)

In normal terminals this only affected activation scripts (only place
where shell kind is used)

I don't have a Windows machine to test it, so I would appreciate any
help with testing!

Release Notes:

- Fixed an issue where tasks could not be executed in Windows WSL

---------

Signed-off-by: Marco Mihai Condrache <52580954+marcocondrache@users.noreply.github.com>
2025-10-13 15:45:46 +02:00
Lukas Wirth
af0cd30a9c editor: Fix delete line moving the cursor too far (#40102)
Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-10-13 13:40:45 +00:00
Lukas Wirth
3ea4b30e8d gpui: Do not render ligatures between different styled text runs (#39928)
This relands https://github.com/zed-industries/zed/pull/37175 as
https://github.com/zed-industries/zed/pull/39886 fixed the jiggling
issue.

Currently when we render text with differing styles adjacently we might
form a ligature between the text, causing the ligature forming
characters to take on one of the two styles. This can especially become
confusing when a ligature is formed between actual text and inlay hints.

Annoyingly, the only ways to prevent this with core text is to either
render each run separately, or to insert a zero-width non-joiner to
force core text to break the ligatures apart, as it otherwise will merge
subsequent font runs of the same fonts.

We currently do layouting on a per line basis and it is unlikely we want
to change that as it would incur a lot of complexity and annoyances to
merge things back into a line, so this goes with the other approach of
inserting ZWNJ characters instead.

Note that neither linux nor windows seem to currently render ligatures,
so this only concerns macOS rendering at the moment.

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

Release Notes:

- Fixed ligatures forming between real text and inlay hints on macOS
2025-10-13 15:35:28 +02:00
Ben Brandt
fdf801d90f acp: Add tooltips for auth methods with descriptions when available (#40098)
Release Notes:

- acp: Provide auth method descriptions in the UI when available

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
2025-10-13 12:35:03 +00:00
Piotr Osiewicz
ff50f48980 lsp: Handle dynamic registration of workspace diagnostic capabilities (#40095)
Workspace diagnostics in Zed have a dedicated background task that
handles querying the language server based on workspace diagnostics
refresh requests issued by both Zed and language server itself.
We only spawned that task when language server declared support for
workspace diagnostics on boot-up. This made workspace diagnostics
unavailable
when a language server (say, Ty) declared support via a capability
registration.
Originally reported in
https://github.com/zed-industries/zed/issues/39144#issuecomment-3370320004

Release Notes:

- python: Fixed workspace diagnostics not working with Ty.
2025-10-13 12:30:26 +00:00
Smit Barmase
af52cbacf9 settings_ui: Fix garbage value for terminal font size (#40093)
Closes #40086

Release Notes:

- Fixed garbage value shown for terminal font size in the settings UI
when no font size is defined in `settings.json`.
2025-10-13 11:54:48 +00:00
Smit Barmase
785cb41565 gpui: Make image auto sizing work with Rems too (#40089)
Closes #39981

Here both images of the left should be identical to the right, since
180px is the same as 11.25rem.

Before:

<img width="1457" height="847" alt="image"
src="https://github.com/user-attachments/assets/59f571d1-8d66-4f41-b9b0-e9826110cf0c"
/>

After:

<img width="1457" height="626" alt="image"
src="https://github.com/user-attachments/assets/a0c629a9-5916-453a-85a2-b3053ab2e613"
/>

Release Notes:

- N/A
2025-10-13 16:52:33 +05:30
Finn Evers
ce20e71abf theme_selector: Fix mouse clicks not updating the theme properly (#40090)
Closes https://github.com/zed-industries/zed/issues/40080

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

We were already doing this for icon themes, but not for normal themes. 

Issue here is that we would only update the `cx.theme()` on the next
frame. On mouse confirmation, we would override the theme and confirm it
on the same frame, yet the global would only be peropely updated on the
next frame and then instantly reset to the new settings file, which
would again be the old theme. This caused a flicker and the selection to
not persist.. Keyboard interactions worked still, because there would be
a rendered frame inbetween selection and confirmation.

Release Notes:

- N/A
2025-10-13 10:55:02 +00:00
Elliot Thomas
237474a889 Fix worktree ordering with PathList (#39944)
The recent introduction of PathList removed some of the ordering logic
resulting in paths always being alphabetised.

This change restores the previous logic for sorting worktrees in a
project using the newer PathList type.

Closes #39934

Release Notes:

- Fixed manual worktree reordering

<details>

<summary>Screen recording of it retaining the order</summary>


https://github.com/user-attachments/assets/0197d118-6ea7-4d2d-8fec-c917fcb9d277

</details>

---------

Co-authored-by: MrSubidubi <finn@zed.dev>
2025-10-13 12:41:42 +02:00
William Fleurant
f6630ed736 docs: Add Mesa GPU selection and XWayland fallback instructions (#39930)
Related #35948

Should document it.. re:
- Added documentation for Mesa GPU device selection using environment
variables
- Added instructions for XWayland fallback when using Wayland


Release Notes:

- N/A

---------

Co-authored-by: Finn Evers <finn.evers@outlook.de>
2025-10-13 09:58:36 +00:00
Finn Evers
81cd435e08 Improve loading times for extension themes (#40015)
This PR primarily does two things:
- replace `serde_json::from_reader` with `serde_json::from_slice`, as
the latter is much much faster, even with loading the file into memory
first.
- runs the initial loading of themes and icon themes coming from
extensions in parallel instead of sequential.

Measuring the `eager_load_active_theme_and_icon_theme` method, this
drastically improves the speed at which this happens (tested this method
primarily with debug builds on my MacBook Pro, but the `Before`
measurement was also confirmed against a `release-fast` build):
- Before: ~260ms on average (in one run, it even took 600ms)
- After: ~20ms on average

Which reduces the time this method takes to load these by around ~92%.

Given that we block on this during the initial app startup, this should
drastically improve Zeds initial startup loading time. Yet, it also
improves responsiveness when installing theme extensions and trying
these.

I also replaced all other `serde_json::from_reader` implementations with
`serde_json::from_slice` and added the former to `disallowed_methods`,
given
https://github.com/serde-rs/json/issues/160#issuecomment-253446892.

Release Notes:

- Improved Zed startup speed when using themes provided by extensions
2025-10-13 11:53:19 +02:00
Xiaobo Liu
47a66c938f editor: Optimize selection overlap checking with binary search (#39773)
Replace O(n²) linear search with O(log n) binary search for checking
selection overlaps when finding next selection range. Pre-sort selection
ranges and use binary search to significantly improve performance when
working with many selections.

Release Notes:

- N/A

---------

Signed-off-by: Xiaobo Liu <cppcoffee@gmail.com>
2025-10-13 10:54:53 +02:00
Finn Evers
1ca2f9871e Improve logging of extension manifest parsing errors (#40082)
Due to using anyhow here, we otherwise lose the relevant error and just
surface a fairly useless error message.

Intentionally not doing this for `extension.json` parsing since that is
deprecated.

Release Notes:

- N/A
2025-10-13 08:27:00 +00:00
Smit Barmase
52cc71e380 image_viewer: Make preview background checkered cover only the image size (#40078)
This makes it easier to see the image bounds for images with transparent
backgrounds.

<img width="2560" height="1377" alt="png"
src="https://github.com/user-attachments/assets/e1555576-39a2-4240-b9d3-67574df76f0d"
/>

Release Notes:

- Updated image preview background checkboxes to match the actual image
size, making it easier to see the bounds of images with transparent
backgrounds.
2025-10-13 13:13:25 +05:30
Tim Vermeulen
7a8a328d3c editor: Preserve the selection granularity when extending a selection (#39759)
Currently when extending a selection using shift-click, the selection
granularity (or `SelectMode`) is based on the click count when extending
the selection, not on the click count of the initial selection. For
example, selecting a word with double-click followed by shift-click uses
a character granularity:


https://github.com/user-attachments/assets/13c78bb9-9c31-45d4-97de-99c30c7425a7

This PR changes this behavior to be more in line with other editors that
I'm familiar with by preserving the granularity of the initial selection
(unless the extension has a higher click count, i.e. the behavior of a
single click selection by a shift-double-click extension is unchanged):


https://github.com/user-attachments/assets/92e69e95-7ea2-4f76-b0a4-e4b9efa1947b

Release Notes:

- Extending a selection using shift-click now preserves the
character/word/line granularity of the initial selection

---------

Co-authored-by: Lukas Wirth <lukas@zed.dev>
2025-10-13 07:23:37 +00:00
Finn Evers
eeaf0b5fec docs: Update basedpyright section (#40079)
Follows up the report #39794

Release Notes:

- N/A
2025-10-13 07:21:33 +00:00
versecafe
95780e5baf typescript: Runners support for bun:test & node:test (#39238)
Closes #21132

Release Notes:

- JavaScript/TypeScript: Added support for detecting `node:test` and `bun:test` test runners
2025-10-13 09:05:04 +02:00
Cole Miller
92e765b5d2 windows: Add support for fetching shell environment in remote projects (#39831)
Closes #39216

Note that this affects all platforms, I'm just using the prefix to make
auto-cherry-picking easier.

Release Notes:

- Fixed shell commands run by agents failing to find installed programs
in some cases.
2025-10-12 23:31:40 +00:00
Cole Miller
abc1e67221 Make ZED_BUILD_REMOTE_SERVER opt-out for dev builds (#39653)
Also removes the option to build with cross.

Release Notes:

- N/A
2025-10-12 19:25:50 -04:00
Ryan Hawkins
68bda24bc1 Allow viewing DAP logs in remote projects (#39744)
It looks like a `.is_local()` check got left in from the original
debugger implementation. I was able to view remote logs just fine after
removing it.

Release Notes:

- Fixed DAP logs being unviewable on remote projects.
2025-10-13 01:21:28 +02:00
Remco Smits
3f3d894c8b lsp colors: Reduce flickering while typing (#40055)
Closes #40019

Follow-up https://github.com/zed-industries/zed/pull/40025

This PR reduces/removes the flickering of inlay colors. This is done by
adding a debounce, and not detaching the task that fetches the new
colors.

**Result**


https://github.com/user-attachments/assets/5dae278b-b821-4e64-8adb-c4d8376ba1df

Release Notes:

- Lsp colors: Reduce flickering while typing.

---------

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2025-10-12 18:59:12 +00:00
Tim Vermeulen
83f0a36733 editor: Fix behavior of clickable line numbers navigation in multibuffer (#39447)
Repro:
- Open a multibuffer
- Click on a line number to jump to the corresponding file
- Click the back button
- Click the forward button, nothing happens
- Click the forward button again, now it works

Double clicking the code to jump to the file (with
`"double_click_in_multibuffer": "open"`) doesn't exhibit this bug, so I
just changed the logic when clicking on a line number in a multibuffer
to match that behavior.


https://github.com/user-attachments/assets/31c0d64d-fdb8-44d6-b0f3-a337ca53de30

Release Notes:

- Fixed bug that could cause navigation to break when clicking on a line
number in a multibuffer
2025-10-12 21:08:24 +03:00
Jakub Konka
bbb6783fb8 windows: Get more tests passing (#39984)
Still got one more test in `project_tests.rs` to investigate...

Release Notes:

- N/A

---------

Co-authored-by: John Tur <john-tur@outlook.com>
2025-10-12 18:13:40 +02:00
Smit Barmase
998fece3af project_panel: Add ability to hide hidden files (#39843)
Closes #5185

Release Notes:

- Added an option to hide hidden files in the project panel by setting
`hide_hidden` in the project panel settings.

---------

Co-authored-by: Gaauwe Rombouts <gromdroid@gmail.com>
Co-authored-by: Gaauwe Rombouts <mail@grombouts.nl>
2025-10-12 18:31:55 +05:30
Ben Kunkle
abe1fd5e16 docs: Validate JSON snippets (settings, keymap, tasks, etc) (#40043)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-10-12 00:19:57 -04:00
Ben Kunkle
deef58bef7 docs: Remove/fix mentions of code_actions_on_format post #39983 (#40040)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-10-11 23:55:57 +00:00
Katie Geer
e11e39f9b4 settings ui: Rearrange sections (#39978)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-10-11 19:40:51 -04:00
Danilo Leal
6dc3e643b4 onboarding: Add some UI improvements (#40016)
Includes improvements in button padding, ways we space elements out,
more consistent use of some components, and cleaning up redundant
buttons styles. Pretty much nothing changes in the design, though.

Release Notes:

- N/A
2025-10-11 13:32:20 +00:00
Finn Evers
d4b5bb9f17 ui: Change scrollbar hitbox insertion (#40008)
Closes #39974

Since the thumb hitboxes themselves do not propagate events, we need to
paint the normal parent hitbox on top of the other ones. This also
caused hover detection to fail, which caused the issue linked.

Release Notes:

- Fixed an issue where hovering scrollbars in hovers would dismiss
these.
2025-10-11 10:00:03 +00:00
Vitaly Slobodin
74d92fd733 ruby: Rename HTML/ERB to HTML+ERB (#40000)
Hi! In https://github.com/zed-extensions/ruby/issues/162 we renamed
embedded template languages:

- `HTML/ERB` to `HTML+ERB`
- `YAML/ERB` to `YAML+ERB`
- `JS/ERB` to `JS+ERB`

This pull request updates the Ruby extension documentation to reflect
that change. Thanks!

Release Notes:

- N/A
2025-10-11 11:17:20 +02:00
Kirill Bulatov
7d260bf4ef cargo update ammonia (#40003)
Deals with https://github.com/zed-industries/zed/security/dependabot/68
security warning

Release Notes:

- N/A
2025-10-11 08:55:30 +00:00
Ned Zimmerman
89bb2de450 docs: Fix link/reference in CSS language doc (#39952)
Looking at
5698636c92/crates/languages/src/css.rs (L19)
it appears that the vscode-css-languageservice is used so I think this
was a typo.

Release Notes:

- N/A
2025-10-11 07:38:13 +00:00
Martin Pool
3d4d8ef6a8 Remove unnecessary clone from Rope::append (#39960)
The previous code clones all the rope chunks, but the rope is passed by
value so the chunks are about to be dropped anyhow.

I thought this may slightly help performance but it has no very
noticeable effect, with a mix of small changes up and down probably
attributable to noise on my machine?

I wonder if the benchmarks might just not hit this path well? I'm
looking into that separately (see #39949, #39951), but this seemed clear
enough to be worth proposing by itself.

Incidentally it surprised me this did not generate a warning already,
but I think it's because we're taking only one field from the struct
that's about to be dropped:
https://github.com/rust-lang/rust-clippy/issues/7429.

<details>

```

     Running benches/rope_benchmark.rs (target/release/deps/rope_benchmark-4c5c71666e7c1729)
push/4096               time:   [362.58 µs 366.40 µs 370.69 µs]
                        thrpt:  [10.538 MiB/s 10.661 MiB/s 10.773 MiB/s]
                 change:
                        time:   [+0.0646% +1.2362% +2.4681%] (p = 0.04 < 0.05)
                        thrpt:  [-2.4086% -1.2211% -0.0646%]
                        Change within noise threshold.
Found 10 outliers among 100 measurements (10.00%)
  7 (7.00%) high mild
  3 (3.00%) high severe
Benchmarking push/65536: Warming up for 3.0000 s
Warning: Unable to complete 100 samples in 5.0s. You may wish to increase target time to 8.4s, enable flat sampling, or reduce sample count to 50.
push/65536              time:   [1.6185 ms 1.6353 ms 1.6557 ms]
                        thrpt:  [37.747 MiB/s 38.219 MiB/s 38.616 MiB/s]
                 change:
                        time:   [+1.9135% +2.9548% +3.9838%] (p = 0.00 < 0.05)
                        thrpt:  [-3.8312% -2.8700% -1.8776%]
                        Performance has regressed.
Found 6 outliers among 100 measurements (6.00%)
  5 (5.00%) high mild
  1 (1.00%) high severe

append/4096             time:   [1.1052 µs 1.1104 µs 1.1162 µs]
                        thrpt:  [3.4177 GiB/s 3.4354 GiB/s 3.4516 GiB/s]
                 change:
                        time:   [-2.5075% -0.3430% +1.5095%] (p = 0.76 > 0.05)
                        thrpt:  [-1.4871% +0.3441% +2.5720%]
                        No change in performance detected.
Found 8 outliers among 100 measurements (8.00%)
  7 (7.00%) high mild
  1 (1.00%) high severe
append/65536            time:   [12.404 µs 12.444 µs 12.487 µs]
                        thrpt:  [4.8881 GiB/s 4.9049 GiB/s 4.9204 GiB/s]
                 change:
                        time:   [-0.1408% +0.5573% +1.2016%] (p = 0.10 > 0.05)
                        thrpt:  [-1.1874% -0.5542% +0.1410%]
                        No change in performance detected.
Found 5 outliers among 100 measurements (5.00%)
  2 (2.00%) high mild
  3 (3.00%) high severe

slice/4096              time:   [32.963 µs 33.185 µs 33.466 µs]
                        thrpt:  [116.72 MiB/s 117.71 MiB/s 118.51 MiB/s]
                 change:
                        time:   [-6.4303% -5.1234% -3.6394%] (p = 0.00 < 0.05)
                        thrpt:  [+3.7769% +5.4000% +6.8722%]
                        Performance has improved.
Found 2 outliers among 100 measurements (2.00%)
  1 (1.00%) high mild
  1 (1.00%) high severe
slice/65536             time:   [668.67 µs 670.49 µs 672.65 µs]
                        thrpt:  [92.916 MiB/s 93.215 MiB/s 93.469 MiB/s]
                 change:
                        time:   [+0.0846% +0.5573% +1.0199%] (p = 0.02 < 0.05)
                        thrpt:  [-1.0096% -0.5542% -0.0845%]
                        Change within noise threshold.
Found 10 outliers among 100 measurements (10.00%)
  6 (6.00%) high mild
  4 (4.00%) high severe

bytes_in_range/4096     time:   [5.1513 µs 5.1594 µs 5.1674 µs]
                        thrpt:  [755.95 MiB/s 757.12 MiB/s 758.31 MiB/s]
                 change:
                        time:   [-4.9410% -4.2051% -3.3835%] (p = 0.00 < 0.05)
                        thrpt:  [+3.5020% +4.3897% +5.1978%]
                        Performance has improved.
Found 4 outliers among 100 measurements (4.00%)
  1 (1.00%) low mild
  3 (3.00%) high severe
bytes_in_range/65536    time:   [139.87 µs 140.17 µs 140.55 µs]
                        thrpt:  [444.67 MiB/s 445.89 MiB/s 446.85 MiB/s]
                 change:
                        time:   [-0.6267% -0.0474% +0.4635%] (p = 0.87 > 0.05)
                        thrpt:  [-0.4614% +0.0475% +0.6306%]
                        No change in performance detected.
Found 9 outliers among 100 measurements (9.00%)
  7 (7.00%) high mild
  2 (2.00%) high severe

chars/4096              time:   [1.0243 µs 1.0250 µs 1.0257 µs]
                        thrpt:  [3.7190 GiB/s 3.7217 GiB/s 3.7243 GiB/s]
                 change:
                        time:   [+4.0106% +4.5396% +5.3062%] (p = 0.00 < 0.05)
                        thrpt:  [-5.0388% -4.3425% -3.8559%]
                        Performance has regressed.
Found 10 outliers among 100 measurements (10.00%)
  2 (2.00%) high mild
  8 (8.00%) high severe
chars/65536             time:   [17.540 µs 17.576 µs 17.614 µs]
                        thrpt:  [3.4652 GiB/s 3.4727 GiB/s 3.4797 GiB/s]
                 change:
                        time:   [+2.5201% +3.3922% +4.1639%] (p = 0.00 < 0.05)
                        thrpt:  [-3.9974% -3.2809% -2.4581%]
                        Performance has regressed.
Found 7 outliers among 100 measurements (7.00%)
  4 (4.00%) high mild
  3 (3.00%) high severe

clip_point/4096         time:   [58.857 µs 59.162 µs 59.490 µs]
                        thrpt:  [65.662 MiB/s 66.026 MiB/s 66.368 MiB/s]
                 change:
                        time:   [+1.6900% +2.8088% +3.8521%] (p = 0.00 < 0.05)
                        thrpt:  [-3.7092% -2.7321% -1.6619%]
                        Performance has regressed.
Found 3 outliers among 100 measurements (3.00%)
  3 (3.00%) high mild
clip_point/65536        time:   [1.8609 ms 1.8633 ms 1.8660 ms]
                        thrpt:  [33.494 MiB/s 33.543 MiB/s 33.585 MiB/s]
                 change:
                        time:   [+0.0577% +0.2579% +0.4495%] (p = 0.01 < 0.05)
                        thrpt:  [-0.4474% -0.2572% -0.0577%]
                        Change within noise threshold.
Found 5 outliers among 100 measurements (5.00%)
  3 (3.00%) high mild
  2 (2.00%) high severe

point_to_offset/4096    time:   [19.246 µs 19.287 µs 19.331 µs]
                        thrpt:  [202.07 MiB/s 202.54 MiB/s 202.97 MiB/s]
                 change:
                        time:   [+1.1073% +2.9754% +5.3818%] (p = 0.00 < 0.05)
                        thrpt:  [-5.1069% -2.8894% -1.0951%]
                        Performance has regressed.
Found 13 outliers among 100 measurements (13.00%)
  5 (5.00%) high mild
  8 (8.00%) high severe
Benchmarking point_to_offset/65536: Warming up for 3.0000 s
Warning: Unable to complete 100 samples in 5.0s. You may wish to increase target time to 6.6s, enable flat sampling, or reduce sample count to 60.
point_to_offset/65536   time:   [741.87 µs 743.28 µs 744.74 µs]
                        thrpt:  [83.922 MiB/s 84.086 MiB/s 84.247 MiB/s]
                 change:
                        time:   [+5.0577% +5.6751% +6.3133%] (p = 0.00 < 0.05)
                        thrpt:  [-5.9384% -5.3703% -4.8142%]
                        Performance has regressed.
Found 7 outliers among 100 measurements (7.00%)
  4 (4.00%) high mild
  3 (3.00%) high severe

cursor/4096             time:   [27.407 µs 27.483 µs 27.600 µs]
                        thrpt:  [141.53 MiB/s 142.13 MiB/s 142.53 MiB/s]
                 change:
                        time:   [-7.1479% -6.2928% -5.6378%] (p = 0.00 < 0.05)
                        thrpt:  [+5.9747% +6.7154% +7.6981%]
                        Performance has improved.
Found 9 outliers among 100 measurements (9.00%)
  1 (1.00%) high mild
  8 (8.00%) high severe
cursor/65536            time:   [848.91 µs 849.70 µs 850.59 µs]
                        thrpt:  [73.478 MiB/s 73.555 MiB/s 73.624 MiB/s]
                 change:
                        time:   [+0.0281% +0.3487% +0.6686%] (p = 0.04 < 0.05)
                        thrpt:  [-0.6642% -0.3475% -0.0281%]
                        Change within noise threshold.
Found 9 outliers among 100 measurements (9.00%)
  5 (5.00%) high mild
  4 (4.00%) high severe

```
</details>

Release Notes:

- N/A
2025-10-11 10:29:54 +03:00
Danilo Leal
42365df12f settings_ui: Fix content page title (#39987)
Follow up to https://github.com/zed-industries/zed/pull/39979. The
previous PR made it the title would change even if you were on a
non-root tree view item. This PR fixes that by fixating the title to
show only the root tree view item.

Release Notes:

- N/A
2025-10-10 20:56:18 -03:00
Ben Kunkle
201124e13f Cleanup default.json (#39986)
Closes #ISSUE

Annotated our `default.json` with `$schema` to get diagnostics, then
fixed the non-language not installed warnings.

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-10-10 23:21:59 +00:00
Ben Kunkle
3ba4b84107 Deprecate code actions on format setting (#39983)
Closes #ISSUE

Release Notes:

- settings: Deprecated `code_actions_on_format` in favor of specifying
code actions to run on format inline in the `formatter` array.

Previously, you would configure code actions to run on format like this:

```json
{
  "code_actions_on_format": {
    "source.organizeImports": true,
    "source.fixAll.eslint": true
  }
}
```

This has been migrated to the new format:

```json
{
  "formatter": [
    {
      "code_action": "source.organizeImports"
    },
    {
      "code_action": "source.fixAll.eslint"
    }
  ]
}
```

This change will be automatically migrated for you. If you had an
existing `formatter` setting, the code actions are prepended to your
formatter array (matching the existing behavior). This migration applies
to both global settings and language-specific settings
2025-10-10 19:01:07 -04:00
Ben Kunkle
f7e7a304e0 settings_ui: Expand nav entries by default when searching (#39980)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-10-10 18:39:16 -04:00
warrenjokinen
65a38a27a9 auto_update: Improve error message when rsync was not found (#39791)
Reworded the error message when the `rsync` utility could not be found.

Release Notes:

- N/A
2025-10-10 23:44:32 +02:00
Cyandev
d6becab3be gpui: Fix broken rendering with nested opacity (#35407)
Rendering breaks when both an element and its parent have opacity set.
The following code reproduces the issue:

```rust
struct Repro;

impl Render for Repro {
    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
        fn make_box(bg: impl Into<Fill>) -> impl IntoElement {
            div().size_8().bg(bg).hover(|style| style.opacity(0.5))
        }

        div()
            .flex()
            .items_center()
            .justify_center()
            .size(px(500.0))
            .hover(|style| style.opacity(0.5))
            .child(make_box(gpui::red()))
            .child(make_box(gpui::green()))
            .child(make_box(gpui::blue()))
    }
}
```

Before (broken behavior):


https://github.com/user-attachments/assets/2c5c1e31-88b2-4f39-81f8-40060e3fe958

The child element resets its parent and siblings' opacity, which is an
unexpected behavior.

After (fixed behavior):


https://github.com/user-attachments/assets/48527033-b06f-4737-b6c3-0ee3d133f138

Release Notes:

- Fixed an issue where nested opacity is rendered incorrectly.
2025-10-10 23:17:20 +02:00
Danilo Leal
924e7e61a5 settings_ui: Add page title label (#39979)
Release Notes:

- N/A
2025-10-10 17:29:30 -03:00
Danilo Leal
18405dece8 Rename settings and keymap actions (#39970)
This PR renames the following actions to make it easier and prioritize
the UI version of interacting with them:

| Before | After |
|--------|--------|
| `OpenSettingsEditor` | `OpenSettings` |
| `OpenSettings` | `OpenSettingsFile` |
| `OpenKeymapEditor` | `OpenKeymap` |
| `OpenKeymap` | `OpenKeymapFile` | 

Release Notes:

- Rename actions to open settings (UI/window and JSON file) as well as
to open the keymap (editor tab and JSON file).
2025-10-10 17:29:20 -03:00
Ben Kunkle
120faadef8 settings_ui: Use bm25 search (#39967)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-10-10 15:51:40 -04:00
Agus Zubiaga
6a9639f62f zeta2 cli: Split retrieval stats module (#39977)
Refactors zeta2 cli a bit. Merging this by itself to prevent conflicts.

Release Notes:

- N/A
2025-10-10 19:35:51 +00:00
Agus Zubiaga
a696e829ac zeta2: Boost declarations included by others (#39975)
Release Notes:

- N/A

Co-authored-by: Michael Sloan <michael@zed.dev>
2025-10-10 19:06:43 +00:00
Shoghy Martinez
eb8510cb39 docs: Fix grammar in sentence about overridden dev extension (#39968)
Added missing comma after "After installing" and removed duplicated
"that" in developing-extensions.md.

Release Notes:

- N/A
2025-10-10 19:21:18 +02:00
localcc
a54cf3c74e Initial layout rounding implementation (#39712)
Release Notes:

- N/A

---------

Co-authored-by: John Tur <john-tur@outlook.com>
2025-10-10 16:45:38 +00:00
Ben Kunkle
41cac5e032 settings_ui: Improve search by fuzzy matching on words (#39961)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-10-10 12:40:23 -04:00
David Kleingeld
59c109f77f Gpui use readme as docs (#39966)
Removes the duplication between `gui.rs` doc comments and the `README.md` file.

Release Notes:

- N/A
2025-10-10 16:39:07 +00:00
Ben Kunkle
5e78fb0f94 settings_ui: Refactor item renderers to render entire field (#39959)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-10-10 12:35:40 -04:00
Kevin Rambaud
63032f6c66 Fix redirect stdin command for fish shell (#39963)
This fixes an issue introduced via
[v0.208.0-pre](https://github.com/zed-industries/zed/releases/tag/v0.208.0-pre)
and reported via
https://github.com/zed-industries/zed/issues/34530#issuecomment-3386042577
where, when using fish shell as the default shell and using a Claude
Code thread in Zed, all command were failing because `(command)` in fish
is for command substitution. Using it creates this type of error:

```
fish: command substitutions not allowed in command position. Try var=(your-cmd) $var ...
(npm ci) </dev/null
^~~~~~~~~~~~^
```

or in the editor itself:

<img width="1624" height="1060" alt="image"
src="https://github.com/user-attachments/assets/64fc3126-2cdd-450e-bc85-ef91c56b3705"
/>


Using the appropriate syntax to redirect to stdin for fish fixes the
issue.

Release Notes:

- Fixed redirect stdin command for fish shell
2025-10-10 16:21:48 +00:00
Cole Miller
5f857ffbb1 Fix menu navigation in remote projects modal (#39965)
Previously we were always adding a `Navigable` entry for the "new WSL
connection" option in this modal, even though we don't have the
corresponding button on non-Windows. This was causing `menu::SelectNext`
to behave incorrectly (focusing the center pane instead) when `Connect
New Server` was selected on macOS and Linux.

Release Notes:

- Fixed a bug with keyboard navigation in the remote project modal.
2025-10-10 16:07:35 +00:00
Cave Bats Of Ware
a78b560b8b Improve GPU selection on Windows (#39264)
Closes #39263

Release Notes:
- N/A 

from
https://github.com/zed-industries/zed/issues/39263#issuecomment-3358220988

> 
> > If you replace that code with
> > 
> > let adapter: IDXGIAdapter1 = unsafe { 
> >    dxgi_factory.EnumAdapters(adapter_index) 
> > }?.cast()?; 
> > 
> > does it not select the right GPU?
>  
> @reflectronic That does seem to select the active gpu for me, meaning
whichever GPU is currently connected. This is a much simpler solution
than the one I have here
(https://github.com/zed-industries/zed/pull/39264 - updated) and while
I'm sure I could imagine someone wanting to choose their GPU to render
Zed on, that may not be something that the application really needs to
support.
> 
> I have a branch with just this as the only change that I can push to
that PR if the simpler solution is preferred.
> 
> ```rust
>         let adapter: IDXGIAdapter1 = unsafe {
>             dxgi_factory.EnumAdapters(adapter_index)?.cast()?
>         };
> ```
2025-10-10 11:47:57 -04:00
morgankrey
b9a6660b93 Grok docs (#39962)
Adds docs for Zed hosted Grok models

Release Notes:

- N/A
2025-10-10 10:46:12 -05:00
Agus Zubiaga
a693d44553 zeta2 cli: Resumable LSP declarations gathering (#39828)
Gathering LSP declarations in zeta_cli can take a really long time for
big repos and has to be started from scratch if interrupted.

Instead of writing the cache file once we have walked the whole
worktree, we'll now do so incrementally as we complete each file. On
subsequent runs, we'll load as many valid declarations as has been
previously written to the cache, and then continue to request the rest
from the LSP which will append to the existing file as it makes
progress. If the last cache entry is incomplete, we'll truncate the
cache file to the end of the last valid line and continue from there, so
we can just `ctrl-c` without breaking resumability.

Release Notes:

- N/A
2025-10-10 12:44:36 -03:00
Dino
41ee92e5f2 agent_ui: Improve quote selections to consider message being edited (#39947)
- Update `AcpThreadView.insert_selections` to take into account whether
the user is currently editing an existing message and, if it is, insert
the selection into that message instead of the thread's message editor
- Update Window's default keymap to use the `agent::QuoteSelection`
action instead of the deprecated `assistant::QuoteSelection` action
- Introduce `AcpThreadView.active_editor` to allow callers to retrieve
either the thread view's message editor or the editor for the message
being edited, in case `AcpThreadView.editing_message` is not `None`
- Improve `AcpThreadView.focus_handle` to focus on the message being
currently edited in case the user navigates back to the editor and then
to the thread view again, all while editing a message
- Add tests for `AcpThreadView.insert_selections`, ensuring that the
selection is inserted in the message being currently edited, if a
message is being edited, or the thread view's message editor if no
message is being edited

Closes #39693 

Release Notes:

- Improved `agent: quote selection` to also work for a message that was
already sent but is being edited

---------

Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
2025-10-10 16:35:37 +01:00
Joseph T. Lyons
a9eb480f3c Remove feedback modal (#39954)
The feedback modal did not match our keyboard-driven design. We can
revisit this later if we want, but for now, removing it makes sense. All
actions have been inlined in the `Help` menu to maintain
discoverability.

Additionally, not all feedback-based actions in the command palette were
namespaced under `feedback:`, and now they are, so they can all be found
there easily.

Release Notes:

- Notice: The `Give Feedback` modal has been removed. The options to
file bug reports, feature requests, email us, and open the Zed
repository can now be found within the `Help` menu directly. The command
palette actions have undergone the following changes:

- `feedback: give feedback` (removed)
- `feedback: file bug report` (no change)
- `zed: request feature` → `feedback: request feature`
- `zed: email zed` → `feedback: email zed`
- `zed: open zed repo` → `contribute: open zed repo`
2025-10-10 15:14:37 +00:00
localcc
5698636c92 Change windows asset name to match other platforms (#39936) 2025-10-10 15:44:48 +02:00
localcc
bbd735905f Fix settings window on Linux/Windows being immovable (#39939) 2025-10-10 15:44:31 +02:00
Bennet Bo Fenner
3d5ddcccf0 ollama: Resolve context window size via API (#39941)
Previously we were guessing the context window size here:
8c3f09e31e/crates/ollama/src/ollama.rs (L22)

This is inaccurate and must be updated manually. This PR ensures that we
extract the context window size from the request in the same way that
the Ollama CLI does when running `ollama show <model-name>` (Relevant
code is
[here](3d32249c74/cmd/cmd.go (L860)))

The format looks like this:

```json
{
  "model_info": {
    "general.architecture": "llama",
    "llama.context_length": 132000
  }
}
```

Once this PR is merged we could technically remove the old code
8c3f09e31e/crates/ollama/src/ollama.rs (L22)
I decided to keep it for now, as it is unclear if the necessary fields
are available via the API on older Ollama versions.

Release Notes:

- Fixed an issue where Ollama models would use the wrong context window
size
2025-10-10 12:59:52 +00:00
Smit Barmase
4dae3a15cc gpui: Fix uniform list scroll to offset for Top and Bottom strategies (#39938)
Closes #39863

Regressed in https://github.com/zed-industries/zed/pull/36653

Release Notes:

- Fixed an issue where clicking a sticky item in the project panel
wouldn’t correctly scroll the view to show its start.
2025-10-10 18:19:58 +05:30
Xiaobo Liu
c6373cc26d Enable test_remote_git_diffs_when_recv_update_repository_delay on Windows (#39866)
Release Notes:

- N/A

Signed-off-by: Xiaobo Liu <cppcoffee@gmail.com>
2025-10-10 09:09:24 +02:00
Cole Miller
a4ec693e34 windows: Don't throw an error when the settings file is empty (#39908)
Closes #39585 

Release Notes:

- N/A
2025-10-09 23:00:16 +00:00
Joseph T. Lyons
08a2b6898b Add a non-beta Windows issue template (#39904)
The beta template will be removed after Windows launch, the new url will
be:


https://github.com/zed-industries/zed/issues/new?template=07_bug_windows.yml

Release Notes:

- N/A
2025-10-09 21:35:16 +00:00
Danilo Leal
13b17b3a85 ui: Make tree view item styles more consistent with similar components (#39892)
This is a small step toward a future where all tree view item-like
elements in Zed can actually use this component.

Release Notes:

- N/A
2025-10-09 16:54:37 -03:00
Anthony Eid
e4f0fbbf80 settings_ui: Fix page scroll bar lagging behind when jumping to a section (#39897)
The issue was caused by the scroll handle taking a couple of frames to
update its offset correctly after calling
`ScrollHandle::scroll_to_top_of_item`. The fast fix is forcing 3 frames
to render back-to-back.

In the future, we should look into `ScrollHandle` and see if there's any
way to update its state outside of paint.

Release Notes:

- N/A

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
Co-authored-by: Katie Geer <katie@zed.dev>
Co-authored-by: Ben Kunkle <ben@zed.dev>
2025-10-09 19:24:02 +00:00
Mikayla Maki
98d4c34199 settings_ui: Restore settings UI keybinding hint (#39896)
Now that the toggle nav focus works well, we can advertise it!

Release Notes:

- N/A
2025-10-09 11:58:44 -07:00
Andrew Farkas
c24f365b69 Fix Git permalinks not being URL-escaped (#39895)
Closes #39875

Release Notes:

- Fixed "open/copy permalink to line" paths not being URL-escaped

Co-authored-by: Cole Miller <cole@zed.dev>
2025-10-09 18:33:05 +00:00
Ben Kunkle
2dfde55367 settings_ui: Fix tab and ID bugs (#39888)
Closes #39883

Release Notes:

- N/A *or* Added/Fixed/Improved ...

---------

Co-authored-by: Anthony <anthony@zed.dev>
2025-10-09 13:54:26 -04:00
Remco Smits
e946a06efe markdown: Add Support for HTML img tags in text (#38107)
Re-adds: https://github.com/zed-industries/zed/pull/37264

This PR re-adds basic support for showing HTML images, without touching
the display mode for images.
The initial PR changed the `div().flex().flex_col()` to
`h_flex().flex_wrap()` but this broke the text wrapping in almost all
cases.

**Note**: This does not add support for showing the images inline,
because we haven't figured out how they correctly do this.
I'm working on adding the CSS `inline` display feature support to taffy
that hopefully allows us to correctly show images/other elements inline
without breaking the text wrapping.

**Before (nightly) and after (dev) for the README file inside Zed.
(nothing has changed, which is good)**
<img width="3440" height="1380" alt="Screenshot 2025-09-13 at 12 49 08"
src="https://github.com/user-attachments/assets/9cbdcb07-dbe9-4236-9d20-e59acc0e955e"
/>

**Result**
<img width="1717" height="1314" alt="Screenshot 2025-09-13 at 12 51 54"
src="https://github.com/user-attachments/assets/1c0f8507-c63d-472e-8e82-a654a63f7153"
/>

cc @SomeoneToIgnore

Release Notes:

- markdown preview: Added support for HTML `img` tags inside paragraphs
2025-10-09 19:11:42 +02:00
Bennet Bo Fenner
75067c94ad gpui: Fix ascent/descent calculation on macOS (#39886)
As you can see in the image, we were previously returning different
`ascent`s/`descent`s when a line would/would not contain an Emoji.

<img width="104" height="36" alt="image"
src="https://github.com/user-attachments/assets/436aeda0-87c0-4dee-943b-6da83681d466"
/>

---
CoreTexts `CTLineGetTypographicBounds` seems to return a different
ascent/descent depending on if an Emoji is there or not AFAIK it is not
documented if this is intended behaviour or not. For us it is
undesirable, as typing an Emoji causes the line to be shifted to the
bottom, see here:


https://github.com/user-attachments/assets/2ad1c82e-6297-48ac-a522-fb382ea56eea

--- 
Instead of using `CTLineGetTypographicBounds` to resolve the
ascent/descent, we look at every run and choose the maximum
ascent/descent. This matches how it [works on
Linux](f1d17fcfbe/crates/gpui/src/platform/linux/text_system.rs (L452))

Release Notes:

- Fixed an issue on macOS where typing an emoji on a line would cause
the line to shift downwards by a few pixels
2025-10-09 18:43:37 +02:00
Ben Brandt
d7143009fc Remove codex feature flag (#39878)
Release Notes:

- N/A
2025-10-09 16:17:49 +00:00
Francisco Gonzalez
a22c29c5f9 gpui: Fix partial dashed border rendering (#38190)
Closes #38189 

- Fixed border dashed for diverse scenarios, as demonstrated in the
images below.
- This change has no impact on the rendering of solid borders, as it was
implemented inside an if block for dashed styles

Release Notes:
  - N/A

## Before Images
<details><summary>click to expand (small top border, medium right
border, large bottom border)</summary>
<img width="289" height="95" alt="Screenshot From 2025-09-15 13-28-14"
src="https://github.com/user-attachments/assets/5226cd0a-49c2-43b8-9df9-f64390e3759e"
/>
</details>
<details><summary> click to expand (Same size pairs of borders)
</summary>
<img width="289" height="95" alt="Screenshot From 2025-09-15 13-32-22"
src="https://github.com/user-attachments/assets/603e7b49-e8b1-45a4-ac35-1b3aedf52bca"
/>
<img width="289" height="95" alt="Screenshot From 2025-09-15 13-33-24"
src="https://github.com/user-attachments/assets/4243786c-4c9d-4419-91d6-4594b5ee4390"
/>
</details>

## After Images

<details><summary>click to expand (small top border, medium right
border, large bottom border)</summary>

<img width="289" height="95" alt="Screenshot From 2025-09-15 13-17-28"
src="https://github.com/user-attachments/assets/e2652b38-1c24-432e-b7fd-c6f4d4c71de6"
/>

</details>


<details><summary> click to expand (same size pairs of
borders)</summary>
<img width="289" height="95" alt="Screenshot From 2025-09-15 13-37-59"
src="https://github.com/user-attachments/assets/05228431-4a91-4531-adcd-d70acd2c3b44"
/>

<img width="289" height="95" alt="Screenshot From 2025-09-15 13-36-34"
src="https://github.com/user-attachments/assets/6da946b8-1ccd-4ed1-9b38-539eba4edf42"
/>
</details>
2025-10-09 17:26:23 +02:00
Ben Kunkle
c543709d5f settings_ui: Add terminal settings (#39874)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-10-09 10:59:08 -04:00
Dino
c58931ac04 git_ui: Fix open diff for untracked files when sorting by path enabled (#39862)
Fixes the `Open Diff` action for untracked files when the `sort_by_path`
setting is enabled. The `ProjectDiff` wasn't correctly moving the
multibuffer's cursor to the untracked file because, when that setting is
enabled, it's sort prefix is changed to the tracked files sort prefix, and that
wasn't accounted for in `move_to_entry`.

Before these changes, the `sort_prefix` field for `PathKey` was called `namespace`, it was renamed to be clearer what its purpose is.

Closes #39529 

Release Notes:

- Fixed 'Open Diff' action for untracked files when `sort_by_path` is
enabled

---------

Co-authored-by: David Kleingeld <davidsk@zed.dev>
2025-10-09 14:34:52 +00:00
Ben Brandt
dd5da592f0 Provide codex as an option on remote sessions (#39774)
Release Notes:

- N/A

---------

Co-authored-by: Cole Miller <cole@zed.dev>
2025-10-09 16:10:56 +02:00
Ben Brandt
f1d17fcfbe acp: Simplify auth check and allow for custom /logout commands (#39867)
- Prefer agent-specific logout handling to allow state reset 
- Treat any auth method as supported; remove provider-specific filter 
- Avoid prompting auth when issuing /logout and agent supports it

Release Notes:

- N/A
2025-10-09 12:58:59 +00:00
Sunli
ccfc1ce387 gpui: Fix drawing rotated SVGs (#33288)
Fixes: https://github.com/longbridge/gpui-component/issues/994

1. When SVG is rotated, incorrect graphics are drawn.

For example: the original aspect ratio of the SVG is 1:1, if the bounds
used to render the SVG are 400x200 (aspect ratio 2:1),
[here](21f985a018/crates/gpui/src/svg_renderer.rs (L91))
the width is used as the scaling factor, causing the rendered SVG to
only have half the height. This PR ensures the complete SVG image is
always rendered.

2. The clipping region has no transformation applied, I added a function
called `distance_from_clip_rect_transformed` in the shader.

3. Fixed `monochrome_sprite_fragment` in `shader.metal` not applying
clipping region.

### Before:


https://github.com/user-attachments/assets/8f93ac36-281e-4837-96cd-c308bfbf92d1

### After:


https://github.com/user-attachments/assets/f52b67a6-4cb9-4d6c-b759-bbb91b59c1cf

Release Notes:

- N/A

---------

Co-authored-by: Jason Lee <huacnlee@gmail.com>
2025-10-09 14:53:36 +02:00
331 changed files with 14271 additions and 8635 deletions

View File

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

View File

@@ -862,16 +862,16 @@ jobs:
working-directory: ${{ env.ZED_WORKSPACE }}
run: script/bundle-windows.ps1
- name: Upload installer (x86_64) to Workflow - zed (main branch preview or run-bundling)
- name: Upload installer (x86_64) to Workflow - zed (run-bundling)
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
if: (${{ github.ref == 'refs/heads/main' }} && ${{ env.RELEASE_CHANNEL == 'preview' }}) || contains(github.event.pull_request.labels.*.name, 'run-bundling')
if: contains(github.event.pull_request.labels.*.name, 'run-bundling')
with:
name: ZedEditorUserSetup-x64-${{ github.event.pull_request.head.sha || github.sha }}.exe
name: Zed_${{ github.event.pull_request.head.sha || github.sha }}-x86_64.exe
path: ${{ env.SETUP_PATH }}
- name: Upload Artifacts to release
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
if: ${{ env.RELEASE_CHANNEL == 'preview' }}
if: ${{ !(contains(github.event.pull_request.labels.*.name, 'run-bundling')) }}
with:
draft: true
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}

2141
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -274,7 +274,7 @@ cloud_llm_client = { path = "crates/cloud_llm_client" }
cloud_zeta2_prompt = { path = "crates/cloud_zeta2_prompt" }
collab = { path = "crates/collab" }
collab_ui = { path = "crates/collab_ui" }
collections = { path = "crates/collections", package = "zed-collections", version = "0.1.0" }
collections = { path = "crates/collections", version = "0.1.0" }
command_palette = { path = "crates/command_palette" }
command_palette_hooks = { path = "crates/command_palette_hooks" }
component = { path = "crates/component" }
@@ -290,7 +290,7 @@ debug_adapter_extension = { path = "crates/debug_adapter_extension" }
debugger_tools = { path = "crates/debugger_tools" }
debugger_ui = { path = "crates/debugger_ui" }
deepseek = { path = "crates/deepseek" }
derive_refineable = { path = "crates/refineable/derive_refineable", package = "zed-derive-refineable", version = "0.1.0" }
derive_refineable = { path = "crates/refineable/derive_refineable" }
diagnostics = { path = "crates/diagnostics" }
editor = { path = "crates/editor" }
extension = { path = "crates/extension" }
@@ -309,10 +309,10 @@ git_ui = { path = "crates/git_ui" }
go_to_line = { path = "crates/go_to_line" }
google_ai = { path = "crates/google_ai" }
gpui = { path = "crates/gpui", default-features = false }
gpui_macros = { path = "crates/gpui_macros", package = "gpui-macros", version = "0.1.0" }
gpui_macros = { path = "crates/gpui_macros" }
gpui_tokio = { path = "crates/gpui_tokio" }
html_to_markdown = { path = "crates/html_to_markdown" }
http_client = { path = "crates/http_client", package = "zed-http-client", version = "0.1.0" }
http_client = { path = "crates/http_client" }
http_client_tls = { path = "crates/http_client_tls" }
icons = { path = "crates/icons" }
image_viewer = { path = "crates/image_viewer" }
@@ -341,7 +341,7 @@ lsp = { path = "crates/lsp" }
markdown = { path = "crates/markdown" }
markdown_preview = { path = "crates/markdown_preview" }
svg_preview = { path = "crates/svg_preview" }
media = { path = "crates/media", package = "zed-media", version = "0.1.0" }
media = { path = "crates/media" }
menu = { path = "crates/menu" }
migrator = { path = "crates/migrator" }
mistral = { path = "crates/mistral" }
@@ -358,7 +358,7 @@ outline = { path = "crates/outline" }
outline_panel = { path = "crates/outline_panel" }
panel = { path = "crates/panel" }
paths = { path = "crates/paths" }
perf = { path = "tooling/perf", package = "zed-perf", version = "0.1.0" }
perf = { path = "tooling/perf" }
picker = { path = "crates/picker" }
plugin = { path = "crates/plugin" }
plugin_macros = { path = "crates/plugin_macros" }
@@ -370,7 +370,7 @@ project_symbols = { path = "crates/project_symbols" }
prompt_store = { path = "crates/prompt_store" }
proto = { path = "crates/proto" }
recent_projects = { path = "crates/recent_projects" }
refineable = { path = "crates/refineable", package = "zed-refineable", version = "0.1.0" }
refineable = { path = "crates/refineable" }
release_channel = { path = "crates/release_channel" }
scheduler = { path = "crates/scheduler" }
remote = { path = "crates/remote" }
@@ -383,7 +383,7 @@ rope = { path = "crates/rope" }
rpc = { path = "crates/rpc" }
rules_library = { path = "crates/rules_library" }
search = { path = "crates/search" }
semantic_version = { path = "crates/semantic_version", package = "zed-semantic-version", version = "0.1.0" }
semantic_version = { path = "crates/semantic_version" }
session = { path = "crates/session" }
settings = { path = "crates/settings" }
settings_macros = { path = "crates/settings_macros" }
@@ -396,7 +396,7 @@ sqlez_macros = { path = "crates/sqlez_macros" }
story = { path = "crates/story" }
storybook = { path = "crates/storybook" }
streaming_diff = { path = "crates/streaming_diff" }
sum_tree = { path = "crates/sum_tree", package = "zed-sum-tree", version = "0.1.0" }
sum_tree = { path = "crates/sum_tree" }
supermaven = { path = "crates/supermaven" }
supermaven_api = { path = "crates/supermaven_api" }
codestral = { path = "crates/codestral" }
@@ -420,8 +420,8 @@ ui = { path = "crates/ui" }
ui_input = { path = "crates/ui_input" }
ui_macros = { path = "crates/ui_macros" }
ui_prompt = { path = "crates/ui_prompt" }
util = { path = "crates/util", package = "zed-util", version = "0.1.0" }
util_macros = { path = "crates/util_macros", package = "zed-util-macros", version = "0.1.0" }
util = { path = "crates/util" }
util_macros = { path = "crates/util_macros" }
vercel = { path = "crates/vercel" }
vim = { path = "crates/vim" }
vim_mode_setting = { path = "crates/vim_mode_setting" }
@@ -654,7 +654,7 @@ strum = { version = "0.27.0", features = ["derive"] }
subtle = "2.5.0"
syn = { version = "2.0.101", features = ["full", "extra-traits", "visit-mut"] }
sys-locale = "0.3.1"
sysinfo = "0.31.0"
sysinfo = "0.37.0"
take-until = "0.2.0"
tempfile = "3.20.0"
thiserror = "2.0.12"
@@ -805,7 +805,7 @@ wasmtime = { opt-level = 3 }
activity_indicator = { codegen-units = 1 }
assets = { codegen-units = 1 }
breadcrumbs = { codegen-units = 1 }
zed-collections = { codegen-units = 1 }
collections = { codegen-units = 1 }
command_palette = { codegen-units = 1 }
command_palette_hooks = { codegen-units = 1 }
extension_cli = { codegen-units = 1 }
@@ -825,11 +825,11 @@ outline = { codegen-units = 1 }
paths = { codegen-units = 1 }
prettier = { codegen-units = 1 }
project_symbols = { codegen-units = 1 }
zed-refineable = { codegen-units = 1 }
refineable = { codegen-units = 1 }
release_channel = { codegen-units = 1 }
reqwest_client = { codegen-units = 1 }
rich_text = { codegen-units = 1 }
zed-semantic-version = { codegen-units = 1 }
semantic_version = { codegen-units = 1 }
session = { codegen-units = 1 }
snippet = { codegen-units = 1 }
snippets_ui = { codegen-units = 1 }

View File

@@ -1,2 +0,0 @@
[build]
dockerfile = "Dockerfile-cross"

View File

@@ -1,17 +0,0 @@
# syntax=docker/dockerfile:1
ARG CROSS_BASE_IMAGE
FROM ${CROSS_BASE_IMAGE}
WORKDIR /app
ARG TZ=Etc/UTC \
LANG=C.UTF-8 \
LC_ALL=C.UTF-8 \
DEBIAN_FRONTEND=noninteractive
ENV CARGO_TERM_COLOR=always
COPY script/install-mold script/
RUN ./script/install-mold "2.34.0"
COPY script/remote-server script/
RUN ./script/remote-server
COPY . .

View File

@@ -1,9 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.6" d="M3.5 11V5.5L8.5 8L3.5 11Z" fill="black"/>
<path opacity="0.4" d="M8.5 14L3.5 11L8.5 8V14Z" fill="black"/>
<path opacity="0.6" d="M8.5 5.5H3.5L8.5 2.5L8.5 5.5Z" fill="black"/>
<path opacity="0.8" d="M8.5 5.5V2.5L13.5 5.5H8.5Z" fill="black"/>
<path opacity="0.2" d="M13.5 11L8.5 14L11 9.5L13.5 11Z" fill="black"/>
<path opacity="0.5" d="M13.5 11L11 9.5L13.5 5V11Z" fill="black"/>
<path d="M3.5 11V5L8.5 2.11325L13.5 5V11L8.5 13.8868L3.5 11Z" stroke="black"/>
<path d="M13.2806 4.66818L8.26042 1.76982C8.09921 1.67673 7.9003 1.67673 7.73909 1.76982L2.71918 4.66818C2.58367 4.74642 2.5 4.89112 2.5 5.04785V10.8924C2.5 11.0489 2.58367 11.1938 2.71918 11.2721L7.73934 14.1704C7.90054 14.2635 8.09946 14.2635 8.26066 14.1704L13.2808 11.2721C13.4163 11.1938 13.5 11.0491 13.5 10.8924V5.04785C13.5 4.89136 13.4163 4.74642 13.2808 4.66818H13.2806ZM12.9653 5.28212L8.11901 13.676C8.08626 13.7326 7.99977 13.7095 7.99977 13.6439V8.14771C7.99977 8.03788 7.94107 7.9363 7.84586 7.88115L3.08613 5.13317C3.02957 5.10041 3.05266 5.0139 3.11818 5.0139H12.8106C12.9483 5.0139 13.0343 5.1631 12.9655 5.28236H12.9653V5.28212Z" fill="#C4CAD4"/>
</svg>

Before

Width:  |  Height:  |  Size: 583 B

After

Width:  |  Height:  |  Size: 769 B

View File

@@ -1 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M2.6 5v3.6h3.6"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M13.4 11A5.4 5.4 0 0 0 8 5.6a5.4 5.4 0 0 0-3.6 1.38L2.6 8.6"/></svg>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.125 9.25001L3 6.125L6.125 3" stroke="#C4CAD4" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3 6.125H9.56251C10.0139 6.125 10.4609 6.21391 10.878 6.38666C11.295 6.55942 11.674 6.81262 11.9932 7.13182C12.3124 7.45102 12.5656 7.82997 12.7383 8.24703C12.9111 8.66408 13 9.11108 13 9.5625C13 10.0139 12.9111 10.4609 12.7383 10.878C12.5656 11.295 12.3124 11.674 11.9932 11.9932C11.674 12.3124 11.295 12.5656 10.878 12.7383C10.4609 12.9111 10.0139 13 9.56251 13H7.375" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 339 B

After

Width:  |  Height:  |  Size: 692 B

View File

@@ -30,8 +30,8 @@
"ctrl-+": ["zed::IncreaseBufferFontSize", { "persist": false }],
"ctrl--": ["zed::DecreaseBufferFontSize", { "persist": false }],
"ctrl-0": ["zed::ResetBufferFontSize", { "persist": false }],
"ctrl-,": "zed::OpenSettingsEditor",
"ctrl-alt-,": "zed::OpenSettings",
"ctrl-,": "zed::OpenSettings",
"ctrl-alt-,": "zed::OpenSettingsFile",
"ctrl-q": "zed::Quit",
"f4": "debugger::Start",
"shift-f5": "debugger::Stop",
@@ -621,7 +621,7 @@
"ctrl-shift-f": "pane::DeploySearch",
"ctrl-shift-h": ["pane::DeploySearch", { "replace_enabled": true }],
"ctrl-shift-t": "pane::ReopenClosedItem",
"ctrl-k ctrl-s": "zed::OpenKeymapEditor",
"ctrl-k ctrl-s": "zed::OpenKeymap",
"ctrl-k ctrl-t": "theme_selector::Toggle",
"ctrl-alt-super-p": "settings_profile_selector::Toggle",
"ctrl-t": "project_symbols::Toggle",
@@ -1249,6 +1249,7 @@
"escape": "workspace::CloseWindow",
"ctrl-m": "settings_editor::Minimize",
"ctrl-f": "search::FocusSearch",
"left": "settings_editor::ToggleFocusNav",
"ctrl-shift-e": "settings_editor::ToggleFocusNav",
// todo(settings_ui): cut this down based on the max files and overflow UI
"ctrl-1": ["settings_editor::FocusFile", 0],
@@ -1269,6 +1270,8 @@
"context": "SettingsWindow > NavigationMenu",
"use_key_equivalents": true,
"bindings": {
"up": "settings_editor::FocusPreviousNavEntry",
"down": "settings_editor::FocusNextNavEntry",
"right": "settings_editor::ExpandNavEntry",
"left": "settings_editor::CollapseNavEntry",
"pageup": "settings_editor::FocusPreviousRootNavEntry",

View File

@@ -39,8 +39,8 @@
"cmd-+": ["zed::IncreaseBufferFontSize", { "persist": false }],
"cmd--": ["zed::DecreaseBufferFontSize", { "persist": false }],
"cmd-0": ["zed::ResetBufferFontSize", { "persist": false }],
"cmd-,": "zed::OpenSettingsEditor",
"cmd-alt-,": "zed::OpenSettings",
"cmd-,": "zed::OpenSettings",
"cmd-alt-,": "zed::OpenSettingsFile",
"cmd-q": "zed::Quit",
"cmd-h": "zed::Hide",
"alt-cmd-h": "zed::HideOthers",
@@ -690,7 +690,7 @@
"cmd-shift-f": "pane::DeploySearch",
"cmd-shift-h": ["pane::DeploySearch", { "replace_enabled": true }],
"cmd-shift-t": "pane::ReopenClosedItem",
"cmd-k cmd-s": "zed::OpenKeymapEditor",
"cmd-k cmd-s": "zed::OpenKeymap",
"cmd-k cmd-t": "theme_selector::Toggle",
"ctrl-alt-cmd-p": "settings_profile_selector::Toggle",
"cmd-t": "project_symbols::Toggle",
@@ -1354,6 +1354,7 @@
"escape": "workspace::CloseWindow",
"cmd-m": "settings_editor::Minimize",
"cmd-f": "search::FocusSearch",
"left": "settings_editor::ToggleFocusNav",
"cmd-shift-e": "settings_editor::ToggleFocusNav",
// todo(settings_ui): cut this down based on the max files and overflow UI
"ctrl-1": ["settings_editor::FocusFile", 0],
@@ -1374,6 +1375,8 @@
"context": "SettingsWindow > NavigationMenu",
"use_key_equivalents": true,
"bindings": {
"up": "settings_editor::FocusPreviousNavEntry",
"down": "settings_editor::FocusNextNavEntry",
"right": "settings_editor::ExpandNavEntry",
"left": "settings_editor::CollapseNavEntry",
"pageup": "settings_editor::FocusPreviousRootNavEntry",

View File

@@ -29,8 +29,8 @@
"ctrl-shift-=": ["zed::IncreaseBufferFontSize", { "persist": false }],
"ctrl--": ["zed::DecreaseBufferFontSize", { "persist": false }],
"ctrl-0": ["zed::ResetBufferFontSize", { "persist": false }],
"ctrl-,": "zed::OpenSettingsEditor",
"ctrl-alt-,": "zed::OpenSettings",
"ctrl-,": "zed::OpenSettings",
"ctrl-alt-,": "zed::OpenSettingsFile",
"ctrl-q": "zed::Quit",
"f4": "debugger::Start",
"shift-f5": "debugger::Stop",
@@ -134,7 +134,7 @@
"ctrl-k z": "editor::ToggleSoftWrap",
"ctrl-f": "buffer_search::Deploy",
"ctrl-h": "buffer_search::DeployReplace",
"ctrl-shift-.": "assistant::QuoteSelection",
"ctrl-shift-.": "agent::QuoteSelection",
"ctrl-shift-,": "assistant::InsertIntoEditor",
"shift-alt-e": "editor::SelectEnclosingSymbol",
"ctrl-shift-backspace": "editor::GoToPreviousChange",
@@ -244,7 +244,7 @@
"ctrl-shift-i": "agent::ToggleOptionsMenu",
// "ctrl-shift-alt-n": "agent::ToggleNewThreadMenu",
"shift-alt-escape": "agent::ExpandMessageEditor",
"ctrl-shift-.": "assistant::QuoteSelection",
"ctrl-shift-.": "agent::QuoteSelection",
"shift-alt-e": "agent::RemoveAllContext",
"ctrl-shift-e": "project_panel::ToggleFocus",
"ctrl-shift-enter": "agent::ContinueThread",
@@ -623,7 +623,7 @@
"ctrl-shift-f": "pane::DeploySearch",
"ctrl-shift-h": ["pane::DeploySearch", { "replace_enabled": true }],
"ctrl-shift-t": "pane::ReopenClosedItem",
"ctrl-k ctrl-s": "zed::OpenKeymapEditor",
"ctrl-k ctrl-s": "zed::OpenKeymap",
"ctrl-k ctrl-t": "theme_selector::Toggle",
"ctrl-alt-super-p": "settings_profile_selector::Toggle",
"ctrl-t": "project_symbols::Toggle",
@@ -1270,6 +1270,7 @@
"escape": "workspace::CloseWindow",
"ctrl-m": "settings_editor::Minimize",
"ctrl-f": "search::FocusSearch",
"left": "settings_editor::ToggleFocusNav",
"ctrl-shift-e": "settings_editor::ToggleFocusNav",
// todo(settings_ui): cut this down based on the max files and overflow UI
"ctrl-1": ["settings_editor::FocusFile", 0],
@@ -1290,6 +1291,8 @@
"context": "SettingsWindow > NavigationMenu",
"use_key_equivalents": true,
"bindings": {
"up": "settings_editor::FocusPreviousNavEntry",
"down": "settings_editor::FocusNextNavEntry",
"right": "settings_editor::ExpandNavEntry",
"left": "settings_editor::CollapseNavEntry",
"pageup": "settings_editor::FocusPreviousRootNavEntry",

View File

@@ -1,7 +1,7 @@
[
{
"bindings": {
"ctrl-alt-s": "zed::OpenSettings",
"ctrl-alt-s": "zed::OpenSettingsFile",
"ctrl-{": "pane::ActivatePreviousItem",
"ctrl-}": "pane::ActivateNextItem",
"shift-escape": null, // Unmap workspace::zoom

View File

@@ -1,4 +1,5 @@
{
"$schema": "zed://schemas/settings",
/// The displayed name of this project. If not set or empty, the root directory name
/// will be displayed.
"project_name": "",
@@ -721,7 +722,9 @@
// Whether to enable drag-and-drop operations in the project panel.
"drag_and_drop": true,
// Whether to hide the root entry when only one folder is open in the window.
"hide_root": false
"hide_root": false,
// Whether to hide the hidden entries in the project panel.
"hide_hidden": false
},
"outline_panel": {
// Whether to show the outline panel button in the status bar
@@ -903,6 +906,7 @@
"now": true,
"find_path": true,
"read_file": true,
"open": true,
"grep": true,
"terminal": true,
"thinking": true,
@@ -914,7 +918,6 @@
// We don't know which of the context server tools are safe for the "Ask" profile, so we don't enable them by default.
// "enable_all_context_servers": true,
"tools": {
"contents": true,
"diagnostics": true,
"fetch": true,
"list_directory": true,
@@ -1101,25 +1104,31 @@
// Removes any lines containing only whitespace at the end of the file and
// ensures just one newline at the end.
"ensure_final_newline_on_save": true,
// Whether or not to perform a buffer format before saving: [on, off, prettier, language_server]
// Whether or not to perform a buffer format before saving: [on, off]
// Keep in mind, if the autosave with delay is enabled, format_on_save will be ignored
"format_on_save": "on",
// How to perform a buffer format. This setting can take 4 values:
// How to perform a buffer format. This setting can take multiple values:
//
// 1. Format code using the current language server:
// 1. Default. Format files using Zed's Prettier integration (if applicable),
// or falling back to formatting via language server:
// "formatter": "auto"
// 2. Format code using the current language server:
// "formatter": "language_server"
// 2. Format code using an external command:
// 3. Format code using a specific language server:
// "formatter": {"language_server": {"name": "ruff"}}
// 4. Format code using an external command:
// "formatter": {
// "external": {
// "command": "prettier",
// "arguments": ["--stdin-filepath", "{buffer_path}"]
// }
// }
// 3. Format code using Zed's Prettier integration:
// 5. Format code using Zed's Prettier integration:
// "formatter": "prettier"
// 4. Default. Format files using Zed's Prettier integration (if applicable),
// or falling back to formatting via language server:
// "formatter": "auto"
// 6. Format code using a code action
// "formatter": {"code_action": "source.fixAll.eslint"}
// 7. An array of any format step specified above to apply in order
// "formatter": [{"code_action": "source.fixAll.eslint"}, "prettier"]
"formatter": "auto",
// How to soft-wrap long lines of text.
// Possible values:
@@ -1404,8 +1413,8 @@
// 4. A box drawn around the following character
// "hollow"
//
// Default: not set, defaults to "block"
"cursor_shape": null,
// Default: "block"
"cursor_shape": "block",
// Set whether Alternate Scroll mode (code: ?1007) is active by default.
// Alternate Scroll mode converts mouse scroll events into up / down key
// presses when in the alternate screen (e.g. when running applications
@@ -1518,7 +1527,6 @@
// A value of 45 preserves colorful themes while ensuring legibility.
"minimum_contrast": 45
},
"code_actions_on_format": {},
// Settings related to running tasks.
"tasks": {
"variables": {},
@@ -1688,9 +1696,7 @@
"preferred_line_length": 72
},
"Go": {
"code_actions_on_format": {
"source.organizeImports": true
},
"formatter": [{ "code_action": "source.organizeImports" }, "language_server"],
"debuggers": ["Delve"]
},
"GraphQL": {
@@ -2054,7 +2060,7 @@
// }
// }
// }
"profiles": [],
"profiles": {},
// A map of log scopes to the desired log level.
// Useful for filtering out noisy logs or enabling more verbose logging.

View File

@@ -9,6 +9,8 @@ disallowed-methods = [
{ path = "std::process::Command::spawn", reason = "Spawning `std::process::Command` can block the current thread for an unknown duration", replacement = "smol::process::Command::spawn" },
{ path = "std::process::Command::output", reason = "Spawning `std::process::Command` can block the current thread for an unknown duration", replacement = "smol::process::Command::output" },
{ path = "std::process::Command::status", reason = "Spawning `std::process::Command` can block the current thread for an unknown duration", replacement = "smol::process::Command::status" },
{ path = "serde_json::from_reader", reason = "Parsing from a buffer is much slower than first reading the buffer into a Vec/String, see https://github.com/serde-rs/json/issues/160#issuecomment-253446892. Use `serde_json::from_slice` instead." },
{ path = "serde_json_lenient::from_reader", reason = "Parsing from a buffer is much slower than first reading the buffer into a Vec/String, see https://github.com/serde-rs/json/issues/160#issuecomment-253446892, Use `serde_json_lenient::from_slice` instead." },
]
disallowed-types = [
# { path = "std::collections::HashMap", replacement = "collections::HashMap" },

View File

@@ -2112,6 +2112,7 @@ impl AcpThread {
let project = self.project.clone();
let language_registry = project.read(cx).languages().clone();
let is_windows = project.read(cx).path_style(cx).is_windows();
let terminal_id = acp::TerminalId(Uuid::new_v4().to_string().into());
let terminal_task = cx.spawn({
@@ -2125,9 +2126,10 @@ impl AcpThread {
.and_then(|r| r.read(cx).default_system_shell())
})?
.unwrap_or_else(|| get_default_system_shell_preferring_bash());
let (task_command, task_args) = ShellBuilder::new(&Shell::Program(shell))
.redirect_stdin_to_dev_null()
.build(Some(command.clone()), &args);
let (task_command, task_args) =
ShellBuilder::new(&Shell::Program(shell), is_windows)
.redirect_stdin_to_dev_null()
.build(Some(command.clone()), &args);
let terminal = project
.update(cx, |project, cx| {
project.create_terminal_task(

View File

@@ -4,22 +4,26 @@ use std::{
fmt::Display,
rc::{Rc, Weak},
sync::Arc,
time::Duration,
};
use agent_client_protocol as acp;
use collections::HashMap;
use gpui::{
App, Empty, Entity, EventEmitter, FocusHandle, Focusable, Global, ListAlignment, ListState,
StyleRefinement, Subscription, Task, TextStyleRefinement, Window, actions, list, prelude::*,
App, ClipboardItem, Empty, Entity, EventEmitter, FocusHandle, Focusable, Global, ListAlignment,
ListState, StyleRefinement, Subscription, Task, TextStyleRefinement, Window, actions, list,
prelude::*,
};
use language::LanguageRegistry;
use markdown::{CodeBlockRenderer, Markdown, MarkdownElement, MarkdownStyle};
use project::Project;
use settings::Settings;
use theme::ThemeSettings;
use ui::prelude::*;
use ui::{Tooltip, prelude::*};
use util::ResultExt as _;
use workspace::{Item, Workspace};
use workspace::{
Item, ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
};
actions!(dev, [OpenAcpLogs]);
@@ -227,6 +231,34 @@ impl AcpTools {
cx.notify();
}
fn serialize_observed_messages(&self) -> Option<String> {
let connection = self.watched_connection.as_ref()?;
let messages: Vec<serde_json::Value> = connection
.messages
.iter()
.filter_map(|message| {
let params = match &message.params {
Ok(Some(params)) => params.clone(),
Ok(None) => serde_json::Value::Null,
Err(err) => serde_json::to_value(err).ok()?,
};
Some(serde_json::json!({
"_direction": match message.direction {
acp::StreamMessageDirection::Incoming => "incoming",
acp::StreamMessageDirection::Outgoing => "outgoing",
},
"_type": message.message_type.to_string().to_lowercase(),
"id": message.request_id,
"method": message.name.to_string(),
"params": params,
}))
})
.collect();
serde_json::to_string_pretty(&messages).ok()
}
fn render_message(
&mut self,
index: usize,
@@ -492,3 +524,92 @@ impl Render for AcpTools {
})
}
}
pub struct AcpToolsToolbarItemView {
acp_tools: Option<Entity<AcpTools>>,
just_copied: bool,
}
impl AcpToolsToolbarItemView {
pub fn new() -> Self {
Self {
acp_tools: None,
just_copied: false,
}
}
}
impl Render for AcpToolsToolbarItemView {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let Some(acp_tools) = self.acp_tools.as_ref() else {
return Empty.into_any_element();
};
let acp_tools = acp_tools.clone();
h_flex()
.gap_2()
.child(
IconButton::new(
"copy_all_messages",
if self.just_copied {
IconName::Check
} else {
IconName::Copy
},
)
.icon_size(IconSize::Small)
.tooltip(Tooltip::text(if self.just_copied {
"Copied!"
} else {
"Copy All Messages"
}))
.disabled(
acp_tools
.read(cx)
.watched_connection
.as_ref()
.is_none_or(|connection| connection.messages.is_empty()),
)
.on_click(cx.listener(move |this, _, _window, cx| {
if let Some(content) = acp_tools.read(cx).serialize_observed_messages() {
cx.write_to_clipboard(ClipboardItem::new_string(content));
this.just_copied = true;
cx.spawn(async move |this, cx| {
cx.background_executor().timer(Duration::from_secs(2)).await;
this.update(cx, |this, cx| {
this.just_copied = false;
cx.notify();
})
})
.detach();
}
})),
)
.into_any()
}
}
impl EventEmitter<ToolbarItemEvent> for AcpToolsToolbarItemView {}
impl ToolbarItemView for AcpToolsToolbarItemView {
fn set_active_pane_item(
&mut self,
active_pane_item: Option<&dyn ItemHandle>,
_window: &mut Window,
cx: &mut Context<Self>,
) -> ToolbarItemLocation {
if let Some(item) = active_pane_item
&& let Some(acp_tools) = item.downcast::<AcpTools>()
{
self.acp_tools = Some(acp_tools);
cx.notify();
return ToolbarItemLocation::PrimaryRight;
}
if self.acp_tools.take().is_some() {
cx.notify();
}
ToolbarItemLocation::Hidden
}
}

View File

@@ -20,7 +20,6 @@ use std::{
cmp::Reverse,
collections::HashSet,
fmt::Write,
path::Path,
sync::Arc,
time::{Duration, Instant},
};
@@ -328,17 +327,13 @@ impl ActivityIndicator {
.flatten()
}
fn pending_environment_errors<'a>(
&'a self,
cx: &'a App,
) -> impl Iterator<Item = (&'a Arc<Path>, &'a EnvironmentErrorMessage)> {
self.project.read(cx).shell_environment_errors(cx)
fn pending_environment_error<'a>(&'a self, cx: &'a App) -> Option<&'a EnvironmentErrorMessage> {
self.project.read(cx).peek_environment_error(cx)
}
fn content_to_render(&mut self, cx: &mut Context<Self>) -> Option<Content> {
// Show if any direnv calls failed
if let Some((abs_path, error)) = self.pending_environment_errors(cx).next() {
let abs_path = abs_path.clone();
if let Some(error) = self.pending_environment_error(cx) {
return Some(Content {
icon: Some(
Icon::new(IconName::Warning)
@@ -348,7 +343,7 @@ impl ActivityIndicator {
message: error.0.clone(),
on_click: Some(Arc::new(move |this, window, cx| {
this.project.update(cx, |project, cx| {
project.remove_environment_error(&abs_path, cx);
project.pop_environment_error(cx);
});
window.dispatch_action(Box::new(workspace::OpenLog), cx);
})),

View File

@@ -39,7 +39,6 @@ heed.workspace = true
http_client.workspace = true
icons.workspace = true
indoc.workspace = true
itertools.workspace = true
language.workspace = true
language_model.workspace = true
log.workspace = true

View File

@@ -2,7 +2,6 @@ pub mod agent_profile;
pub mod context;
pub mod context_server_tool;
pub mod context_store;
pub mod history_store;
pub mod thread;
pub mod thread_store;
pub mod tool_use;

View File

@@ -1,253 +0,0 @@
use crate::{ThreadId, thread_store::SerializedThreadMetadata};
use anyhow::{Context as _, Result};
use assistant_context::SavedContextMetadata;
use chrono::{DateTime, Utc};
use gpui::{App, AsyncApp, Entity, SharedString, Task, prelude::*};
use itertools::Itertools;
use paths::contexts_dir;
use serde::{Deserialize, Serialize};
use std::{collections::VecDeque, path::Path, sync::Arc, time::Duration};
use util::ResultExt as _;
const MAX_RECENTLY_OPENED_ENTRIES: usize = 6;
const NAVIGATION_HISTORY_PATH: &str = "agent-navigation-history.json";
const SAVE_RECENTLY_OPENED_ENTRIES_DEBOUNCE: Duration = Duration::from_millis(50);
#[derive(Clone, Debug)]
pub enum HistoryEntry {
Thread(SerializedThreadMetadata),
Context(SavedContextMetadata),
}
impl HistoryEntry {
pub fn updated_at(&self) -> DateTime<Utc> {
match self {
HistoryEntry::Thread(thread) => thread.updated_at,
HistoryEntry::Context(context) => context.mtime.to_utc(),
}
}
pub fn id(&self) -> HistoryEntryId {
match self {
HistoryEntry::Thread(thread) => HistoryEntryId::Thread(thread.id.clone()),
HistoryEntry::Context(context) => HistoryEntryId::Context(context.path.clone()),
}
}
pub fn title(&self) -> &SharedString {
match self {
HistoryEntry::Thread(thread) => &thread.summary,
HistoryEntry::Context(context) => &context.title,
}
}
}
/// Generic identifier for a history entry.
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum HistoryEntryId {
Thread(ThreadId),
Context(Arc<Path>),
}
#[derive(Serialize, Deserialize)]
enum SerializedRecentOpen {
Thread(String),
ContextName(String),
/// Old format which stores the full path
Context(String),
}
pub struct HistoryStore {
context_store: Entity<assistant_context::ContextStore>,
recently_opened_entries: VecDeque<HistoryEntryId>,
_subscriptions: Vec<gpui::Subscription>,
_save_recently_opened_entries_task: Task<()>,
}
impl HistoryStore {
pub fn new(
context_store: Entity<assistant_context::ContextStore>,
initial_recent_entries: impl IntoIterator<Item = HistoryEntryId>,
cx: &mut Context<Self>,
) -> Self {
let subscriptions = vec![cx.observe(&context_store, |_, _, cx| cx.notify())];
cx.spawn(async move |this, cx| {
let entries = Self::load_recently_opened_entries(cx).await.log_err()?;
this.update(cx, |this, _| {
this.recently_opened_entries
.extend(
entries.into_iter().take(
MAX_RECENTLY_OPENED_ENTRIES
.saturating_sub(this.recently_opened_entries.len()),
),
);
})
.ok()
})
.detach();
Self {
context_store,
recently_opened_entries: initial_recent_entries.into_iter().collect(),
_subscriptions: subscriptions,
_save_recently_opened_entries_task: Task::ready(()),
}
}
pub fn entries(&self, cx: &mut Context<Self>) -> Vec<HistoryEntry> {
let mut history_entries = Vec::new();
#[cfg(debug_assertions)]
if std::env::var("ZED_SIMULATE_NO_THREAD_HISTORY").is_ok() {
return history_entries;
}
history_entries.extend(
self.context_store
.read(cx)
.unordered_contexts()
.cloned()
.map(HistoryEntry::Context),
);
history_entries.sort_unstable_by_key(|entry| std::cmp::Reverse(entry.updated_at()));
history_entries
}
pub fn recent_entries(&self, limit: usize, cx: &mut Context<Self>) -> Vec<HistoryEntry> {
self.entries(cx).into_iter().take(limit).collect()
}
pub fn recently_opened_entries(&self, cx: &App) -> Vec<HistoryEntry> {
#[cfg(debug_assertions)]
if std::env::var("ZED_SIMULATE_NO_THREAD_HISTORY").is_ok() {
return Vec::new();
}
let context_entries =
self.context_store
.read(cx)
.unordered_contexts()
.flat_map(|context| {
self.recently_opened_entries
.iter()
.enumerate()
.flat_map(|(index, entry)| match entry {
HistoryEntryId::Context(path) if &context.path == path => {
Some((index, HistoryEntry::Context(context.clone())))
}
_ => None,
})
});
context_entries
// optimization to halt iteration early
.take(self.recently_opened_entries.len())
.sorted_unstable_by_key(|(index, _)| *index)
.map(|(_, entry)| entry)
.collect()
}
fn save_recently_opened_entries(&mut self, cx: &mut Context<Self>) {
let serialized_entries = self
.recently_opened_entries
.iter()
.filter_map(|entry| match entry {
HistoryEntryId::Context(path) => path.file_name().map(|file| {
SerializedRecentOpen::ContextName(file.to_string_lossy().into_owned())
}),
HistoryEntryId::Thread(id) => Some(SerializedRecentOpen::Thread(id.to_string())),
})
.collect::<Vec<_>>();
self._save_recently_opened_entries_task = cx.spawn(async move |_, cx| {
cx.background_executor()
.timer(SAVE_RECENTLY_OPENED_ENTRIES_DEBOUNCE)
.await;
cx.background_spawn(async move {
let path = paths::data_dir().join(NAVIGATION_HISTORY_PATH);
let content = serde_json::to_string(&serialized_entries)?;
std::fs::write(path, content)?;
anyhow::Ok(())
})
.await
.log_err();
});
}
fn load_recently_opened_entries(cx: &AsyncApp) -> Task<Result<Vec<HistoryEntryId>>> {
cx.background_spawn(async move {
let path = paths::data_dir().join(NAVIGATION_HISTORY_PATH);
let contents = match smol::fs::read_to_string(path).await {
Ok(it) => it,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
return Ok(Vec::new());
}
Err(e) => {
return Err(e)
.context("deserializing persisted agent panel navigation history");
}
};
let entries = serde_json::from_str::<Vec<SerializedRecentOpen>>(&contents)
.context("deserializing persisted agent panel navigation history")?
.into_iter()
.take(MAX_RECENTLY_OPENED_ENTRIES)
.flat_map(|entry| match entry {
SerializedRecentOpen::Thread(id) => {
Some(HistoryEntryId::Thread(id.as_str().into()))
}
SerializedRecentOpen::ContextName(file_name) => Some(HistoryEntryId::Context(
contexts_dir().join(file_name).into(),
)),
SerializedRecentOpen::Context(path) => {
Path::new(&path).file_name().map(|file_name| {
HistoryEntryId::Context(contexts_dir().join(file_name).into())
})
}
})
.collect::<Vec<_>>();
Ok(entries)
})
}
pub fn push_recently_opened_entry(&mut self, entry: HistoryEntryId, cx: &mut Context<Self>) {
self.recently_opened_entries
.retain(|old_entry| old_entry != &entry);
self.recently_opened_entries.push_front(entry);
self.recently_opened_entries
.truncate(MAX_RECENTLY_OPENED_ENTRIES);
self.save_recently_opened_entries(cx);
}
pub fn remove_recently_opened_thread(&mut self, id: ThreadId, cx: &mut Context<Self>) {
self.recently_opened_entries.retain(
|entry| !matches!(entry, HistoryEntryId::Thread(thread_id) if thread_id == &id),
);
self.save_recently_opened_entries(cx);
}
pub fn replace_recently_opened_text_thread(
&mut self,
old_path: &Path,
new_path: &Arc<Path>,
cx: &mut Context<Self>,
) {
for entry in &mut self.recently_opened_entries {
match entry {
HistoryEntryId::Context(path) if path.as_ref() == old_path => {
*entry = HistoryEntryId::Context(new_path.clone());
break;
}
_ => {}
}
}
self.save_recently_opened_entries(cx);
}
pub fn remove_recently_opened_entry(&mut self, entry: &HistoryEntryId, cx: &mut Context<Self>) {
self.recently_opened_entries
.retain(|old_entry| old_entry != entry);
self.save_recently_opened_entries(cx);
}
}

View File

@@ -790,7 +790,7 @@ mod tests {
store.update_user_settings(cx, |settings| {
settings.project.all_languages.defaults.format_on_save = Some(FormatOnSave::On);
settings.project.all_languages.defaults.formatter =
Some(language::language_settings::SelectedFormatter::Auto);
Some(language::language_settings::FormatterList::default());
});
});
});

View File

@@ -9,8 +9,9 @@ use futures::io::BufReader;
use project::Project;
use project::agent_server_store::AgentServerCommand;
use serde::Deserialize;
use settings::{Settings as _, SettingsLocation};
use task::Shell;
use util::ResultExt as _;
use util::{ResultExt as _, get_default_system_shell_preferring_bash};
use std::path::PathBuf;
use std::{any::Any, cell::RefCell};
@@ -22,7 +23,7 @@ use gpui::{App, AppContext as _, AsyncApp, Entity, SharedString, Task, WeakEntit
use acp_thread::{AcpThread, AuthRequired, LoadError, TerminalProviderEvent};
use terminal::TerminalBuilder;
use terminal::terminal_settings::{AlternateScroll, CursorShape};
use terminal::terminal_settings::{AlternateScroll, CursorShape, TerminalSettings};
#[derive(Debug, Error)]
#[error("Unsupported version")]
@@ -168,7 +169,10 @@ impl AcpConnection {
meta: None,
},
terminal: true,
meta: None,
meta: Some(serde_json::json!({
// Experimental: Allow for rendering terminal output from the agents
"terminal_output": true,
})),
},
meta: None,
})
@@ -815,13 +819,25 @@ impl acp::Client for ClientDelegate {
let mut env = if let Some(dir) = &args.cwd {
project
.update(&mut self.cx.clone(), |project, cx| {
project.directory_environment(&task::Shell::System, dir.clone().into(), cx)
let worktree = project.find_worktree(dir.as_path(), cx);
let shell = TerminalSettings::get(
worktree.as_ref().map(|(worktree, path)| SettingsLocation {
worktree_id: worktree.read(cx).id(),
path: &path,
}),
cx,
)
.shell
.clone();
project.directory_environment(&shell, dir.clone().into(), cx)
})?
.await
.unwrap_or_default()
} else {
Default::default()
};
// Disables paging for `git` and hopefully other commands
env.insert("PAGER".into(), "".into());
for var in args.env {
env.insert(var.name, var.value);
}
@@ -834,8 +850,11 @@ impl acp::Client for ClientDelegate {
.and_then(|r| r.read(cx).default_system_shell())
.map(Shell::Program)
})?
.unwrap_or(task::Shell::System);
let (task_command, task_args) = task::ShellBuilder::new(&shell)
.unwrap_or_else(|| Shell::Program(get_default_system_shell_preferring_bash()));
let is_windows = project
.read_with(&self.cx, |project, cx| project.path_style(cx).is_windows())
.unwrap_or(cfg!(windows));
let (task_command, task_args) = task::ShellBuilder::new(&shell, is_windows)
.redirect_stdin_to_dev_null()
.build(Some(args.command.clone()), &args.args);

View File

@@ -1,11 +1,16 @@
use std::rc::Rc;
use std::sync::Arc;
use std::{any::Any, path::Path};
use crate::{AgentServer, AgentServerDelegate, load_proxy_env};
use acp_thread::AgentConnection;
use agent_client_protocol as acp;
use anyhow::{Context as _, Result};
use gpui::{App, SharedString, Task};
use project::agent_server_store::CODEX_NAME;
use fs::Fs;
use gpui::{App, AppContext as _, SharedString, Task};
use project::agent_server_store::{AllAgentServersSettings, CODEX_NAME};
use settings::{SettingsStore, update_settings_file};
use crate::{AgentServer, AgentServerDelegate, load_proxy_env};
#[derive(Clone)]
pub struct Codex;
@@ -30,6 +35,27 @@ impl AgentServer for Codex {
ui::IconName::AiOpenAi
}
fn default_mode(&self, cx: &mut App) -> Option<acp::SessionModeId> {
let settings = cx.read_global(|settings: &SettingsStore, _| {
settings.get::<AllAgentServersSettings>(None).codex.clone()
});
settings
.as_ref()
.and_then(|s| s.default_mode.clone().map(|m| acp::SessionModeId(m.into())))
}
fn set_default_mode(&self, mode_id: Option<acp::SessionModeId>, fs: Arc<dyn Fs>, cx: &mut App) {
update_settings_file(fs, cx, |settings, _| {
settings
.agent_servers
.get_or_insert_default()
.codex
.get_or_insert_default()
.default_mode = mode_id.map(|m| m.to_string())
});
}
fn connect(
&self,
root_dir: Option<&Path>,

View File

@@ -12,7 +12,7 @@ use anyhow::Result;
use editor::{CompletionProvider, Editor, ExcerptId};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{App, Entity, Task, WeakEntity};
use language::{Buffer, CodeLabel, HighlightId};
use language::{Buffer, CodeLabel, CodeLabelBuilder, HighlightId};
use lsp::CompletionContext;
use project::lsp_store::{CompletionDocumentation, SymbolLocation};
use project::{
@@ -27,7 +27,7 @@ use util::rel_path::RelPath;
use workspace::Workspace;
use crate::AgentPanel;
use crate::acp::message_editor::{MessageEditor, MessageEditorEvent};
use crate::acp::message_editor::MessageEditor;
use crate::context_picker::file_context_picker::{FileMatch, search_files};
use crate::context_picker::rules_context_picker::{RulesContextEntry, search_rules};
use crate::context_picker::symbol_context_picker::SymbolMatch;
@@ -673,7 +673,7 @@ impl ContextPickerCompletionProvider {
fn build_code_label_for_full_path(file_name: &str, directory: Option<&str>, cx: &App) -> CodeLabel {
let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
let mut label = CodeLabel::default();
let mut label = CodeLabelBuilder::default();
label.push_str(file_name, None);
label.push_str(" ", None);
@@ -682,9 +682,7 @@ fn build_code_label_for_full_path(file_name: &str, directory: Option<&str>, cx:
label.push_str(directory, comment_id);
}
label.filter_range = 0..label.text().len();
label
label.build()
}
impl CompletionProvider for ContextPickerCompletionProvider {
@@ -759,13 +757,13 @@ impl CompletionProvider for ContextPickerCompletionProvider {
let editor = editor.clone();
move |cx| {
editor
.update(cx, |_editor, cx| {
.update(cx, |editor, cx| {
match intent {
CompletionIntent::Complete
| CompletionIntent::CompleteWithInsert
| CompletionIntent::CompleteWithReplace => {
if !is_missing_argument {
cx.emit(MessageEditorEvent::Send);
editor.send(cx);
}
}
CompletionIntent::Compose => {}
@@ -775,7 +773,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
}
});
}
is_missing_argument
false
}
})),
}
@@ -910,6 +908,17 @@ impl CompletionProvider for ContextPickerCompletionProvider {
offset_to_line,
self.prompt_capabilities.borrow().embedded_context,
)
.filter(|completion| {
// Right now we don't support completing arguments of slash commands
let is_slash_command_with_argument = matches!(
completion,
ContextCompletion::SlashCommand(SlashCommandCompletion {
argument: Some(_),
..
})
);
!is_slash_command_with_argument
})
.map(|completion| {
completion.source_range().start <= offset_to_line + position.column as usize
&& completion.source_range().end >= offset_to_line + position.column as usize

View File

@@ -141,7 +141,9 @@ impl MessageEditor {
subscriptions.push(cx.subscribe_in(&editor, window, {
move |this, editor, event, window, cx| {
if let EditorEvent::Edited { .. } = event {
if let EditorEvent::Edited { .. } = event
&& !editor.read(cx).read_only(cx)
{
let snapshot = editor.update(cx, |editor, cx| {
let new_hints = this
.command_hint(editor.buffer(), cx)
@@ -823,13 +825,20 @@ impl MessageEditor {
});
}
fn send(&mut self, _: &Chat, _: &mut Window, cx: &mut Context<Self>) {
pub fn send(&mut self, cx: &mut Context<Self>) {
if self.is_empty(cx) {
return;
}
self.editor.update(cx, |editor, cx| {
editor.clear_inlay_hints(cx);
});
cx.emit(MessageEditorEvent::Send)
}
fn chat(&mut self, _: &Chat, _: &mut Window, cx: &mut Context<Self>) {
self.send(cx);
}
fn cancel(&mut self, _: &editor::actions::Cancel, _: &mut Window, cx: &mut Context<Self>) {
cx.emit(MessageEditorEvent::Cancel)
}
@@ -1030,6 +1039,7 @@ impl MessageEditor {
) else {
return;
};
self.editor.update(cx, |message_editor, cx| {
message_editor.edit([(cursor_anchor..cursor_anchor, completion.new_text)], cx);
});
@@ -1287,7 +1297,7 @@ impl Render for MessageEditor {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.key_context("MessageEditor")
.on_action(cx.listener(Self::send))
.on_action(cx.listener(Self::chat))
.on_action(cx.listener(Self::cancel))
.capture_action(cx.listener(Self::paste))
.flex_1()
@@ -2011,21 +2021,11 @@ mod tests {
editor.update_in(&mut cx, |editor, _window, cx| {
assert_eq!(editor.text(cx), "/say-hello ");
assert_eq!(editor.display_text(cx), "/say-hello <name>");
assert!(editor.has_visible_completions_menu());
assert_eq!(
current_completion_labels_with_documentation(editor),
&[("say-hello".into(), "Say hello to whoever you want".into())]
);
assert!(!editor.has_visible_completions_menu());
});
cx.simulate_input("GPT5");
editor.update_in(&mut cx, |editor, window, cx| {
assert!(editor.has_visible_completions_menu());
editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
});
cx.run_until_parked();
editor.update_in(&mut cx, |editor, window, cx| {
@@ -2034,7 +2034,7 @@ mod tests {
assert!(!editor.has_visible_completions_menu());
// Delete argument
for _ in 0..4 {
for _ in 0..5 {
editor.backspace(&editor::actions::Backspace, window, cx);
}
});
@@ -2042,13 +2042,12 @@ mod tests {
cx.run_until_parked();
editor.update_in(&mut cx, |editor, window, cx| {
assert_eq!(editor.text(cx), "/say-hello ");
assert_eq!(editor.text(cx), "/say-hello");
// Hint is visible because argument was deleted
assert_eq!(editor.display_text(cx), "/say-hello <name>");
// Delete last command letter
editor.backspace(&editor::actions::Backspace, window, cx);
editor.backspace(&editor::actions::Backspace, window, cx);
});
cx.run_until_parked();

View File

@@ -278,7 +278,7 @@ pub struct AcpThreadView {
thread_feedback: ThreadFeedbackState,
list_state: ListState,
auth_task: Option<Task<()>>,
expanded_tool_calls: HashSet<acp::ToolCallId>,
collapsed_tool_calls: HashSet<acp::ToolCallId>,
expanded_thinking_blocks: HashSet<(usize, usize)>,
edits_expanded: bool,
plan_expanded: bool,
@@ -419,7 +419,7 @@ impl AcpThreadView {
thread_error: None,
thread_feedback: Default::default(),
auth_task: None,
expanded_tool_calls: HashSet::default(),
collapsed_tool_calls: HashSet::default(),
expanded_thinking_blocks: HashSet::default(),
editing_message: None,
edits_expanded: false,
@@ -954,17 +954,17 @@ impl AcpThreadView {
) {
match &event.view_event {
ViewEvent::NewDiff(tool_call_id) => {
if AgentSettings::get_global(cx).expand_edit_card {
self.expanded_tool_calls.insert(tool_call_id.clone());
if !AgentSettings::get_global(cx).expand_edit_card {
self.collapsed_tool_calls.insert(tool_call_id.clone());
}
}
ViewEvent::NewTerminal(tool_call_id) => {
if AgentSettings::get_global(cx).expand_terminal_card {
self.expanded_tool_calls.insert(tool_call_id.clone());
if !AgentSettings::get_global(cx).expand_terminal_card {
self.collapsed_tool_calls.insert(tool_call_id.clone());
}
}
ViewEvent::TerminalMovedToBackground(tool_call_id) => {
self.expanded_tool_calls.remove(tool_call_id);
self.collapsed_tool_calls.insert(tool_call_id.clone());
}
ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::Focus) => {
if let Some(thread) = self.thread()
@@ -1046,32 +1046,36 @@ impl AcpThreadView {
};
let connection = thread.read(cx).connection().clone();
let auth_methods = connection.auth_methods();
let has_supported_auth = auth_methods.iter().any(|method| {
let id = method.id.0.as_ref();
id == "claude-login" || id == "spawn-gemini-cli"
});
let can_login = has_supported_auth || auth_methods.is_empty() || self.login.is_some();
if !can_login {
let can_login = !connection.auth_methods().is_empty() || self.login.is_some();
// Does the agent have a specific logout command? Prefer that in case they need to reset internal state.
let logout_supported = text == "/logout"
&& self
.available_commands
.borrow()
.iter()
.any(|command| command.name == "logout");
if can_login && !logout_supported {
self.message_editor
.update(cx, |editor, cx| editor.clear(window, cx));
let this = cx.weak_entity();
let agent = self.agent.clone();
window.defer(cx, |window, cx| {
Self::handle_auth_required(
this,
AuthRequired {
description: None,
provider_id: None,
},
agent,
connection,
window,
cx,
);
});
cx.notify();
return;
};
let this = cx.weak_entity();
let agent = self.agent.clone();
window.defer(cx, |window, cx| {
Self::handle_auth_required(
this,
AuthRequired {
description: None,
provider_id: None,
},
agent,
connection,
window,
cx,
);
});
cx.notify();
return;
}
}
self.send_impl(self.message_editor.clone(), window, cx)
@@ -1250,12 +1254,6 @@ impl AcpThreadView {
.detach();
}
fn open_agent_diff(&mut self, _: &OpenAgentDiff, window: &mut Window, cx: &mut Context<Self>) {
if let Some(thread) = self.thread() {
AgentDiffPane::deploy(thread.clone(), self.workspace.clone(), window, cx).log_err();
}
}
fn open_edited_buffer(
&mut self,
buffer: &Entity<Buffer>,
@@ -2121,7 +2119,7 @@ impl AcpThreadView {
let is_collapsible = !tool_call.content.is_empty() && !needs_confirmation;
let is_open = needs_confirmation || self.expanded_tool_calls.contains(&tool_call.id);
let is_open = needs_confirmation || !self.collapsed_tool_calls.contains(&tool_call.id);
let tool_output_display =
if is_open {
@@ -2271,9 +2269,9 @@ impl AcpThreadView {
let id = tool_call.id.clone();
move |this: &mut Self, _, _, cx: &mut Context<Self>| {
if is_open {
this.expanded_tool_calls.remove(&id);
this.collapsed_tool_calls.insert(id.clone());
} else {
this.expanded_tool_calls.insert(id.clone());
this.collapsed_tool_calls.remove(&id);
}
cx.notify();
}
@@ -2475,7 +2473,7 @@ impl AcpThreadView {
.icon_color(Color::Muted)
.on_click(cx.listener({
move |this: &mut Self, _, _, cx: &mut Context<Self>| {
this.expanded_tool_calls.remove(&tool_call_id);
this.collapsed_tool_calls.insert(tool_call_id.clone());
cx.notify();
}
})),
@@ -2753,7 +2751,7 @@ impl AcpThreadView {
.map(|path| path.display().to_string())
.unwrap_or_else(|| "current directory".to_string());
let is_expanded = self.expanded_tool_calls.contains(&tool_call.id);
let is_expanded = !self.collapsed_tool_calls.contains(&tool_call.id);
let header = h_flex()
.id(header_id)
@@ -2888,9 +2886,9 @@ impl AcpThreadView {
let id = tool_call.id.clone();
move |this, _event, _window, _cx| {
if is_expanded {
this.expanded_tool_calls.remove(&id);
this.collapsed_tool_calls.insert(id.clone());
} else {
this.expanded_tool_calls.insert(id.clone());
this.collapsed_tool_calls.remove(&id);
}
}
})),
@@ -3282,6 +3280,12 @@ impl AcpThreadView {
this.style(ButtonStyle::Outlined)
}
})
.when_some(
method.description.clone(),
|this, description| {
this.tooltip(Tooltip::text(description))
},
)
.on_click({
cx.listener(move |this, _, window, cx| {
telemetry::event!(
@@ -4971,10 +4975,12 @@ impl AcpThreadView {
})
}
/// Inserts the selected text into the message editor or the message being
/// edited, if any.
pub(crate) fn insert_selections(&self, window: &mut Window, cx: &mut Context<Self>) {
self.message_editor.update(cx, |message_editor, cx| {
message_editor.insert_selections(window, cx);
})
self.active_editor(cx).update(cx, |editor, cx| {
editor.insert_selections(window, cx);
});
}
fn render_thread_retry_status_callout(
@@ -5385,6 +5391,23 @@ impl AcpThreadView {
};
task.detach_and_log_err(cx);
}
/// Returns the currently active editor, either for a message that is being
/// edited or the editor for a new message.
fn active_editor(&self, cx: &App) -> Entity<MessageEditor> {
if let Some(index) = self.editing_message
&& let Some(editor) = self
.entry_view_state
.read(cx)
.entry(index)
.and_then(|e| e.message_editor())
.cloned()
{
editor
} else {
self.message_editor.clone()
}
}
}
fn loading_contents_spinner(size: IconSize) -> AnyElement {
@@ -5399,7 +5422,7 @@ impl Focusable for AcpThreadView {
fn focus_handle(&self, cx: &App) -> FocusHandle {
match self.thread_state {
ThreadState::Loading { .. } | ThreadState::Ready { .. } => {
self.message_editor.focus_handle(cx)
self.active_editor(cx).focus_handle(cx)
}
ThreadState::LoadError(_) | ThreadState::Unauthenticated { .. } => {
self.focus_handle.clone()
@@ -5416,7 +5439,6 @@ impl Render for AcpThreadView {
v_flex()
.size_full()
.key_context("AcpThread")
.on_action(cx.listener(Self::open_agent_diff))
.on_action(cx.listener(Self::toggle_burn_mode))
.on_action(cx.listener(Self::keep_all))
.on_action(cx.listener(Self::reject_all))
@@ -6660,4 +6682,146 @@ pub(crate) mod tests {
)
});
}
#[gpui::test]
async fn test_message_editing_insert_selections(cx: &mut TestAppContext) {
init_test(cx);
let connection = StubAgentConnection::new();
connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk {
content: acp::ContentBlock::Text(acp::TextContent {
text: "Response".into(),
annotations: None,
meta: None,
}),
}]);
let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
add_to_workspace(thread_view.clone(), cx);
let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
message_editor.update_in(cx, |editor, window, cx| {
editor.set_text("Original message to edit", window, cx)
});
thread_view.update_in(cx, |thread_view, window, cx| thread_view.send(window, cx));
cx.run_until_parked();
let user_message_editor = thread_view.read_with(cx, |thread_view, cx| {
thread_view
.entry_view_state
.read(cx)
.entry(0)
.expect("Should have at least one entry")
.message_editor()
.expect("Should have message editor")
.clone()
});
cx.focus(&user_message_editor);
thread_view.read_with(cx, |thread_view, _cx| {
assert_eq!(thread_view.editing_message, Some(0));
});
// Ensure to edit the focused message before proceeding otherwise, since
// its content is not different from what was sent, focus will be lost.
user_message_editor.update_in(cx, |editor, window, cx| {
editor.set_text("Original message to edit with ", window, cx)
});
// Create a simple buffer with some text so we can create a selection
// that will then be added to the message being edited.
let (workspace, project) = thread_view.read_with(cx, |thread_view, _cx| {
(thread_view.workspace.clone(), thread_view.project.clone())
});
let buffer = project.update(cx, |project, cx| {
project.create_local_buffer("let a = 10 + 10;", None, false, cx)
});
workspace
.update_in(cx, |workspace, window, cx| {
let editor = cx.new(|cx| {
let mut editor =
Editor::for_buffer(buffer.clone(), Some(project.clone()), window, cx);
editor.change_selections(Default::default(), window, cx, |selections| {
selections.select_ranges([8..15]);
});
editor
});
workspace.add_item_to_active_pane(Box::new(editor), None, false, window, cx);
})
.unwrap();
thread_view.update_in(cx, |thread_view, window, cx| {
assert_eq!(thread_view.editing_message, Some(0));
thread_view.insert_selections(window, cx);
});
user_message_editor.read_with(cx, |editor, cx| {
let text = editor.editor().read(cx).text(cx);
let expected_text = String::from("Original message to edit with selection ");
assert_eq!(text, expected_text);
});
}
#[gpui::test]
async fn test_insert_selections(cx: &mut TestAppContext) {
init_test(cx);
let connection = StubAgentConnection::new();
connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk {
content: acp::ContentBlock::Text(acp::TextContent {
text: "Response".into(),
annotations: None,
meta: None,
}),
}]);
let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
add_to_workspace(thread_view.clone(), cx);
let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
message_editor.update_in(cx, |editor, window, cx| {
editor.set_text("Can you review this snippet ", window, cx)
});
// Create a simple buffer with some text so we can create a selection
// that will then be added to the message being edited.
let (workspace, project) = thread_view.read_with(cx, |thread_view, _cx| {
(thread_view.workspace.clone(), thread_view.project.clone())
});
let buffer = project.update(cx, |project, cx| {
project.create_local_buffer("let a = 10 + 10;", None, false, cx)
});
workspace
.update_in(cx, |workspace, window, cx| {
let editor = cx.new(|cx| {
let mut editor =
Editor::for_buffer(buffer.clone(), Some(project.clone()), window, cx);
editor.change_selections(Default::default(), window, cx, |selections| {
selections.select_ranges([8..15]);
});
editor
});
workspace.add_item_to_active_pane(Box::new(editor), None, false, window, cx);
})
.unwrap();
thread_view.update_in(cx, |thread_view, window, cx| {
assert_eq!(thread_view.editing_message, None);
thread_view.insert_selections(window, cx);
});
thread_view.read_with(cx, |thread_view, cx| {
let text = thread_view.message_editor.read(cx).text(cx);
let expected_txt = String::from("Can you review this snippet selection ");
assert_eq!(text, expected_txt);
})
}
}

View File

@@ -6,7 +6,6 @@ mod tool_picker;
use std::{ops::Range, sync::Arc};
use agent_settings::AgentSettings;
use anyhow::Result;
use assistant_tool::{ToolSource, ToolWorkingSet};
use cloud_llm_client::{Plan, PlanV1, PlanV2};
@@ -15,7 +14,6 @@ use context_server::ContextServerId;
use editor::{Editor, SelectionEffects, scroll::Autoscroll};
use extension::ExtensionManifest;
use extension_host::ExtensionStore;
use feature_flags::{CodexAcpFeatureFlag, FeatureFlagAppExt as _};
use fs::Fs;
use gpui::{
Action, AnyView, App, AsyncWindowContext, Corner, Entity, EventEmitter, FocusHandle, Focusable,
@@ -30,10 +28,10 @@ use project::{
agent_server_store::{AgentServerStore, CLAUDE_CODE_NAME, CODEX_NAME, GEMINI_NAME},
context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore},
};
use settings::{Settings, SettingsStore, update_settings_file};
use settings::{SettingsStore, update_settings_file};
use ui::{
Chip, CommonAnimationExt, ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex,
Indicator, PopoverMenu, Switch, SwitchColor, SwitchField, Tooltip, WithScrollbar, prelude::*,
Indicator, PopoverMenu, Switch, SwitchColor, Tooltip, WithScrollbar, prelude::*,
};
use util::ResultExt as _;
use workspace::{Workspace, create_and_open_local_file};
@@ -403,101 +401,6 @@ impl AgentConfiguration {
)
}
fn render_command_permission(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
let always_allow_tool_actions = AgentSettings::get_global(cx).always_allow_tool_actions;
let fs = self.fs.clone();
SwitchField::new(
"always-allow-tool-actions-switch",
Some("Allow running commands without asking for confirmation"),
Some(
"The agent can perform potentially destructive actions without asking for your confirmation.".into(),
),
always_allow_tool_actions,
move |state, _window, cx| {
let allow = state == &ToggleState::Selected;
update_settings_file(fs.clone(), cx, move |settings, _| {
settings.agent.get_or_insert_default().set_always_allow_tool_actions(allow);
});
},
)
}
fn render_single_file_review(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
let single_file_review = AgentSettings::get_global(cx).single_file_review;
let fs = self.fs.clone();
SwitchField::new(
"single-file-review",
Some("Enable single-file agent reviews"),
Some("Agent edits are also displayed in single-file editors for review.".into()),
single_file_review,
move |state, _window, cx| {
let allow = state == &ToggleState::Selected;
update_settings_file(fs.clone(), cx, move |settings, _| {
settings
.agent
.get_or_insert_default()
.set_single_file_review(allow);
});
},
)
}
fn render_sound_notification(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
let play_sound_when_agent_done = AgentSettings::get_global(cx).play_sound_when_agent_done;
let fs = self.fs.clone();
SwitchField::new(
"sound-notification",
Some("Play sound when finished generating"),
Some(
"Hear a notification sound when the agent is done generating changes or needs your input.".into(),
),
play_sound_when_agent_done,
move |state, _window, cx| {
let allow = state == &ToggleState::Selected;
update_settings_file(fs.clone(), cx, move |settings, _| {
settings.agent.get_or_insert_default().set_play_sound_when_agent_done(allow);
});
},
)
}
fn render_modifier_to_send(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
let use_modifier_to_send = AgentSettings::get_global(cx).use_modifier_to_send;
let fs = self.fs.clone();
SwitchField::new(
"modifier-send",
Some("Use modifier to submit a message"),
Some(
"Make a modifier (cmd-enter on macOS, ctrl-enter on Linux or Windows) required to send messages.".into(),
),
use_modifier_to_send,
move |state, _window, cx| {
let allow = state == &ToggleState::Selected;
update_settings_file(fs.clone(), cx, move |settings, _| {
settings.agent.get_or_insert_default().set_use_modifier_to_send(allow);
});
},
)
}
fn render_general_settings_section(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
v_flex()
.p(DynamicSpacing::Base16.rems(cx))
.pr(DynamicSpacing::Base20.rems(cx))
.gap_2p5()
.border_b_1()
.border_color(cx.theme().colors().border)
.child(Headline::new("General Settings"))
.child(self.render_command_permission(cx))
.child(self.render_single_file_review(cx))
.child(self.render_sound_notification(cx))
.child(self.render_modifier_to_send(cx))
}
fn render_zed_plan_info(&self, plan: Option<Plan>, cx: &mut Context<Self>) -> impl IntoElement {
if let Some(plan) = plan {
let free_chip_bg = cx
@@ -1085,14 +988,11 @@ impl AgentConfiguration {
"Claude Code",
))
.child(Divider::horizontal().color(DividerColor::BorderFaded))
.when(cx.has_flag::<CodexAcpFeatureFlag>(), |this| {
this
.child(self.render_agent_server(
IconName::AiOpenAi,
"Codex",
))
.child(Divider::horizontal().color(DividerColor::BorderFaded))
})
.child(self.render_agent_server(
IconName::AiOpenAi,
"Codex",
))
.child(Divider::horizontal().color(DividerColor::BorderFaded))
.child(self.render_agent_server(
IconName::AiGemini,
"Gemini CLI",
@@ -1145,7 +1045,6 @@ impl Render for AgentConfiguration {
.track_scroll(&self.scroll_handle)
.size_full()
.overflow_y_scroll()
.child(self.render_general_settings_section(cx))
.child(self.render_agent_servers_section(cx))
.child(self.render_context_servers_section(window, cx))
.child(self.render_provider_configuration_section(cx)),

View File

@@ -19,9 +19,10 @@ use zed_actions::agent::{OpenClaudeCodeOnboardingModal, ReauthenticateAgent};
use crate::acp::{AcpThreadHistory, ThreadHistoryEvent};
use crate::ui::{AcpOnboardingModal, ClaudeCodeOnboardingModal};
use crate::{
AddContextServer, DeleteRecentlyOpenThread, Follow, InlineAssistant, NewTextThread, NewThread,
OpenActiveThreadAsMarkdown, OpenHistory, ResetTrialEndUpsell, ResetTrialUpsell,
ToggleNavigationMenu, ToggleNewThreadMenu, ToggleOptionsMenu,
AddContextServer, AgentDiffPane, DeleteRecentlyOpenThread, Follow, InlineAssistant,
NewTextThread, NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff, OpenHistory,
ResetTrialEndUpsell, ResetTrialUpsell, ToggleNavigationMenu, ToggleNewThreadMenu,
ToggleOptionsMenu,
acp::AcpThreadView,
agent_configuration::{AgentConfiguration, AssistantConfigurationEvent},
slash_command::SlashCommandCompletionProvider,
@@ -33,7 +34,6 @@ use crate::{
};
use agent::{
context_store::ContextStore,
history_store::{HistoryEntryId, HistoryStore},
thread_store::{TextThreadStore, ThreadStore},
};
use agent_settings::AgentSettings;
@@ -75,7 +75,6 @@ use zed_actions::{
assistant::{OpenRulesLibrary, ToggleFocus},
};
use feature_flags::{CodexAcpFeatureFlag, FeatureFlagAppExt as _};
const AGENT_PANEL_KEY: &str = "agent_panel";
#[derive(Serialize, Deserialize, Debug)]
@@ -141,6 +140,16 @@ pub fn init(cx: &mut App) {
.register_action(|workspace, _: &Follow, window, cx| {
workspace.follow(CollaboratorId::Agent, window, cx);
})
.register_action(|workspace, _: &OpenAgentDiff, window, cx| {
let thread = workspace
.panel::<AgentPanel>(cx)
.and_then(|panel| panel.read(cx).active_thread_view().cloned())
.and_then(|thread_view| thread_view.read(cx).thread().cloned());
if let Some(thread) = thread {
AgentDiffPane::deploy_in_workspace(thread, workspace, window, cx);
}
})
.register_action(|workspace, _: &ToggleNavigationMenu, window, cx| {
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
workspace.focus_panel::<AgentPanel>(window, cx);
@@ -299,7 +308,6 @@ impl ActiveView {
pub fn prompt_editor(
context_editor: Entity<TextThreadEditor>,
history_store: Entity<HistoryStore>,
acp_history_store: Entity<agent2::HistoryStore>,
language_registry: Arc<LanguageRegistry>,
window: &mut Window,
@@ -367,18 +375,6 @@ impl ActiveView {
})
}
ContextEvent::PathChanged { old_path, new_path } => {
history_store.update(cx, |history_store, cx| {
if let Some(old_path) = old_path {
history_store
.replace_recently_opened_text_thread(old_path, new_path, cx);
} else {
history_store.push_recently_opened_entry(
HistoryEntryId::Context(new_path.clone()),
cx,
);
}
});
acp_history_store.update(cx, |history_store, cx| {
if let Some(old_path) = old_path {
history_store
@@ -420,7 +416,7 @@ pub struct AgentPanel {
language_registry: Arc<LanguageRegistry>,
thread_store: Entity<ThreadStore>,
acp_history: Entity<AcpThreadHistory>,
acp_history_store: Entity<agent2::HistoryStore>,
history_store: Entity<agent2::HistoryStore>,
context_store: Entity<TextThreadStore>,
prompt_store: Option<Entity<PromptStore>>,
inline_assist_context_store: Entity<ContextStore>,
@@ -428,7 +424,6 @@ pub struct AgentPanel {
configuration_subscription: Option<Subscription>,
active_view: ActiveView,
previous_view: Option<ActiveView>,
history_store: Entity<HistoryStore>,
new_thread_menu_handle: PopoverMenuHandle<ContextMenu>,
agent_panel_menu_handle: PopoverMenuHandle<ContextMenu>,
assistant_navigation_menu_handle: PopoverMenuHandle<ContextMenu>,
@@ -561,10 +556,8 @@ impl AgentPanel {
let inline_assist_context_store =
cx.new(|_cx| ContextStore::new(project.downgrade(), Some(thread_store.downgrade())));
let history_store = cx.new(|cx| HistoryStore::new(context_store.clone(), [], cx));
let acp_history_store = cx.new(|cx| agent2::HistoryStore::new(context_store.clone(), cx));
let acp_history = cx.new(|cx| AcpThreadHistory::new(acp_history_store.clone(), window, cx));
let history_store = cx.new(|cx| agent2::HistoryStore::new(context_store.clone(), cx));
let acp_history = cx.new(|cx| AcpThreadHistory::new(history_store.clone(), window, cx));
cx.subscribe_in(
&acp_history,
window,
@@ -586,14 +579,12 @@ impl AgentPanel {
)
.detach();
cx.observe(&history_store, |_, _, cx| cx.notify()).detach();
let panel_type = AgentSettings::get_global(cx).default_view;
let active_view = match panel_type {
DefaultView::Thread => ActiveView::native_agent(
fs.clone(),
prompt_store.clone(),
acp_history_store.clone(),
history_store.clone(),
project.clone(),
workspace.clone(),
window,
@@ -619,7 +610,6 @@ impl AgentPanel {
ActiveView::prompt_editor(
context_editor,
history_store.clone(),
acp_history_store.clone(),
language_registry.clone(),
window,
cx,
@@ -685,7 +675,6 @@ impl AgentPanel {
configuration_subscription: None,
inline_assist_context_store,
previous_view: None,
history_store: history_store.clone(),
new_thread_menu_handle: PopoverMenuHandle::default(),
agent_panel_menu_handle: PopoverMenuHandle::default(),
assistant_navigation_menu_handle: PopoverMenuHandle::default(),
@@ -696,7 +685,7 @@ impl AgentPanel {
pending_serialization: None,
onboarding,
acp_history,
acp_history_store,
history_store,
selected_agent: AgentType::default(),
loading: false,
}
@@ -750,7 +739,7 @@ impl AgentPanel {
cx: &mut Context<Self>,
) {
let Some(thread) = self
.acp_history_store
.history_store
.read(cx)
.thread_from_session_id(&action.from_session_id)
else {
@@ -799,7 +788,6 @@ impl AgentPanel {
ActiveView::prompt_editor(
context_editor.clone(),
self.history_store.clone(),
self.acp_history_store.clone(),
self.language_registry.clone(),
window,
cx,
@@ -831,7 +819,7 @@ impl AgentPanel {
}
let loading = self.loading;
let history = self.acp_history_store.clone();
let history = self.history_store.clone();
cx.spawn_in(window, async move |this, cx| {
let ext_agent = match agent_choice {
@@ -892,7 +880,7 @@ impl AgentPanel {
summarize_thread,
workspace.clone(),
project,
this.acp_history_store.clone(),
this.history_store.clone(),
this.prompt_store.clone(),
window,
cx,
@@ -990,7 +978,6 @@ impl AgentPanel {
ActiveView::prompt_editor(
editor,
self.history_store.clone(),
self.acp_history_store.clone(),
self.language_registry.clone(),
window,
cx,
@@ -1254,11 +1241,6 @@ impl AgentPanel {
match &new_view {
ActiveView::TextThread { context_editor, .. } => {
self.history_store.update(cx, |store, cx| {
if let Some(path) = context_editor.read(cx).context().read(cx).path() {
store.push_recently_opened_entry(HistoryEntryId::Context(path.clone()), cx)
}
});
self.acp_history_store.update(cx, |store, cx| {
if let Some(path) = context_editor.read(cx).context().read(cx).path() {
store.push_recently_opened_entry(
agent2::HistoryEntryId::TextThread(path.clone()),
@@ -1292,7 +1274,7 @@ impl AgentPanel {
) -> ContextMenu {
let entries = panel
.read(cx)
.acp_history_store
.history_store
.read(cx)
.recently_opened_entries(cx);
@@ -1337,7 +1319,7 @@ impl AgentPanel {
move |_window, cx| {
panel
.update(cx, |this, cx| {
this.acp_history_store.update(cx, |history_store, cx| {
this.history_store.update(cx, |history_store, cx| {
history_store.remove_recently_opened_entry(&id, cx);
});
})
@@ -1939,34 +1921,32 @@ impl AgentPanel {
}
}),
)
.when(cx.has_flag::<CodexAcpFeatureFlag>(), |this| {
this.item(
ContextMenuEntry::new("New Codex Thread")
.icon(IconName::AiOpenAi)
.disabled(is_via_collab)
.icon_color(Color::Muted)
.handler({
let workspace = workspace.clone();
move |window, cx| {
if let Some(workspace) = workspace.upgrade() {
workspace.update(cx, |workspace, cx| {
if let Some(panel) =
workspace.panel::<AgentPanel>(cx)
{
panel.update(cx, |panel, cx| {
panel.new_agent_thread(
AgentType::Codex,
window,
cx,
);
});
}
});
}
.item(
ContextMenuEntry::new("New Codex Thread")
.icon(IconName::AiOpenAi)
.disabled(is_via_collab)
.icon_color(Color::Muted)
.handler({
let workspace = workspace.clone();
move |window, cx| {
if let Some(workspace) = workspace.upgrade() {
workspace.update(cx, |workspace, cx| {
if let Some(panel) =
workspace.panel::<AgentPanel>(cx)
{
panel.update(cx, |panel, cx| {
panel.new_agent_thread(
AgentType::Codex,
window,
cx,
);
});
}
});
}
}),
)
})
}
}),
)
.item(
ContextMenuEntry::new("New Gemini CLI Thread")
.icon(IconName::AiGemini)
@@ -2170,10 +2150,7 @@ impl AgentPanel {
false
}
_ => {
let history_is_empty = self.acp_history_store.read(cx).is_empty(cx)
&& self
.history_store
.update(cx, |store, cx| store.recent_entries(1, cx).is_empty());
let history_is_empty = self.history_store.read(cx).is_empty(cx);
let has_configured_non_zed_providers = LanguageModelRegistry::read_global(cx)
.providers()

View File

@@ -11,7 +11,7 @@ use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{App, Entity, Task, WeakEntity};
use http_client::HttpClientWithUrl;
use itertools::Itertools;
use language::{Buffer, CodeLabel, HighlightId};
use language::{Buffer, CodeLabel, CodeLabelBuilder, HighlightId};
use lsp::CompletionContext;
use project::lsp_store::SymbolLocation;
use project::{
@@ -686,7 +686,8 @@ impl ContextPickerCompletionProvider {
};
let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
let mut label = CodeLabel::plain(symbol.name.clone(), None);
let mut label = CodeLabelBuilder::default();
label.push_str(&symbol.name, 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);
@@ -696,7 +697,7 @@ impl ContextPickerCompletionProvider {
Some(Completion {
replace_range: source_range.clone(),
new_text,
label,
label: label.build(),
documentation: None,
source: project::CompletionSource::Custom,
icon_path: Some(IconName::Code.path().into()),
@@ -729,7 +730,7 @@ impl ContextPickerCompletionProvider {
fn build_code_label_for_full_path(file_name: &str, directory: Option<&str>, cx: &App) -> CodeLabel {
let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
let mut label = CodeLabel::default();
let mut label = CodeLabelBuilder::default();
label.push_str(file_name, None);
label.push_str(" ", None);
@@ -738,9 +739,7 @@ fn build_code_label_for_full_path(file_name: &str, directory: Option<&str>, cx:
label.push_str(directory, comment_id);
}
label.filter_range = 0..label.text().len();
label
label.build()
}
impl CompletionProvider for ContextPickerCompletionProvider {

View File

@@ -9,6 +9,7 @@ use anyhow::Result;
use futures::StreamExt;
use futures::stream::{self, BoxStream};
use gpui::{App, SharedString, Task, WeakEntity, Window};
use language::CodeLabelBuilder;
use language::HighlightId;
use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate, OffsetRangeExt};
pub use language_model::Role;
@@ -328,15 +329,15 @@ impl SlashCommandLine {
}
pub fn create_label_for_command(command_name: &str, arguments: &[&str], cx: &App) -> CodeLabel {
let mut label = CodeLabel::default();
let mut label = CodeLabelBuilder::default();
label.push_str(command_name, None);
label.respan_filter_range(None);
label.push_str(" ", None);
label.push_str(
&arguments.join(" "),
cx.theme().syntax().highlight_id("comment").map(HighlightId),
);
label.filter_range = 0..command_name.len();
label
label.build()
}
#[cfg(test)]

View File

@@ -7,7 +7,7 @@ use futures::Stream;
use futures::channel::mpsc;
use fuzzy::PathMatch;
use gpui::{App, Entity, Task, WeakEntity};
use language::{BufferSnapshot, CodeLabel, HighlightId, LineEnding, LspAdapterDelegate};
use language::{BufferSnapshot, CodeLabelBuilder, HighlightId, LineEnding, LspAdapterDelegate};
use project::{PathMatchCandidateSet, Project};
use serde::{Deserialize, Serialize};
use smol::stream::StreamExt;
@@ -168,7 +168,7 @@ impl SlashCommand for FileSlashCommand {
.display(path_style)
.to_string();
let mut label = CodeLabel::default();
let mut label = CodeLabelBuilder::default();
let file_name = path_match.path.file_name()?;
let label_text = if path_match.is_dir {
format!("{}/ ", file_name)
@@ -178,10 +178,10 @@ impl SlashCommand for FileSlashCommand {
label.push_str(label_text.as_str(), None);
label.push_str(&text, comment_id);
label.filter_range = 0..file_name.len();
label.respan_filter_range(Some(file_name));
Some(ArgumentCompletion {
label,
label: label.build(),
new_text: text,
after_completion: AfterCompletion::Compose,
replace_previous_arguments: false,

View File

@@ -7,7 +7,7 @@ use collections::{HashMap, HashSet};
use editor::Editor;
use futures::future::join_all;
use gpui::{Task, WeakEntity};
use language::{BufferSnapshot, CodeLabel, HighlightId, LspAdapterDelegate};
use language::{BufferSnapshot, CodeLabel, CodeLabelBuilder, HighlightId, LspAdapterDelegate};
use std::sync::{Arc, atomic::AtomicBool};
use ui::{ActiveTheme, App, Window, prelude::*};
use util::{ResultExt, paths::PathStyle};
@@ -308,10 +308,10 @@ fn create_tab_completion_label(
comment_id: Option<HighlightId>,
) -> CodeLabel {
let (parent_path, file_name) = path_style.split(path);
let mut label = CodeLabel::default();
let mut label = CodeLabelBuilder::default();
label.push_str(file_name, None);
label.push_str(" ", None);
label.push_str(parent_path.unwrap_or_default(), comment_id);
label.filter_range = 0..file_name.len();
label
label.respan_filter_range(Some(file_name));
label.build()
}

View File

@@ -1538,7 +1538,7 @@ mod tests {
store.update_user_settings(cx, |settings| {
settings.project.all_languages.defaults.format_on_save = Some(FormatOnSave::On);
settings.project.all_languages.defaults.formatter =
Some(language::language_settings::SelectedFormatter::Auto);
Some(language::language_settings::FormatterList::default());
});
});
});

View File

@@ -136,6 +136,7 @@ impl Tool for TerminalTool {
}),
None => Task::ready(None).shared(),
};
let is_windows = project.read(cx).path_style(cx).is_windows();
let shell = project
.update(cx, |project, cx| {
project
@@ -155,7 +156,7 @@ impl Tool for TerminalTool {
let build_cmd = {
let input_command = input.command.clone();
move || {
ShellBuilder::new(&Shell::Program(shell))
ShellBuilder::new(&Shell::Program(shell), is_windows)
.redirect_stdin_to_dev_null()
.build(Some(input_command), &[])
}

View File

@@ -649,7 +649,7 @@ impl AutoUpdater {
#[cfg(not(target_os = "windows"))]
anyhow::ensure!(
which::which("rsync").is_ok(),
"Aborting. Could not find rsync which is required for auto-updates."
"Could not auto-update because the required rsync utility was not found."
);
Ok(())
}
@@ -658,7 +658,7 @@ impl AutoUpdater {
let filename = match OS {
"macos" => anyhow::Ok("Zed.dmg"),
"linux" => Ok("zed.tar.gz"),
"windows" => Ok("zed_editor_installer.exe"),
"windows" => Ok("Zed.exe"),
unsupported_os => anyhow::bail!("not supported: {unsupported_os}"),
}?;

View File

@@ -68,10 +68,13 @@ struct Args {
#[arg(short, long, overrides_with = "add")]
new: bool,
/// Sets a custom directory for all user data (e.g., database, extensions, logs).
/// This overrides the default platform-specific data directory location.
/// On macOS, the default is `~/Library/Application Support/Zed`.
/// On Linux/FreeBSD, the default is `$XDG_DATA_HOME/zed`.
/// On Windows, the default is `%LOCALAPPDATA%\Zed`.
/// This overrides the default platform-specific data directory location:
#[cfg_attr(target_os = "macos", doc = "`~/Library/Application Support/Zed`.")]
#[cfg_attr(target_os = "windows", doc = "`%LOCALAPPDATA%\\Zed`.")]
#[cfg_attr(
not(any(target_os = "windows", target_os = "macos")),
doc = "`$XDG_DATA_HOME/zed`."
)]
#[arg(long, value_name = "DIR")]
user_data_dir: Option<String>,
/// The paths to open in Zed (space-separated).

View File

@@ -1,7 +1,7 @@
use chrono::Duration;
use serde::{Deserialize, Serialize};
use std::{
ops::Range,
ops::{Add, Range, Sub},
path::{Path, PathBuf},
sync::Arc,
};
@@ -18,8 +18,8 @@ pub struct PredictEditsRequest {
pub excerpt_path: Arc<Path>,
/// Within file
pub excerpt_range: Range<usize>,
/// Within `excerpt`
pub cursor_offset: usize,
pub excerpt_line_range: Range<Line>,
pub cursor_point: Point,
/// Within `signatures`
pub excerpt_parent: Option<usize>,
pub signatures: Vec<Signature>,
@@ -47,12 +47,13 @@ pub struct PredictEditsRequest {
pub enum PromptFormat {
MarkedExcerpt,
LabeledSections,
NumberedLines,
/// Prompt format intended for use via zeta_cli
OnlySnippets,
}
impl PromptFormat {
pub const DEFAULT: PromptFormat = PromptFormat::LabeledSections;
pub const DEFAULT: PromptFormat = PromptFormat::NumberedLines;
}
impl Default for PromptFormat {
@@ -73,6 +74,7 @@ impl std::fmt::Display for PromptFormat {
PromptFormat::MarkedExcerpt => write!(f, "Marked Excerpt"),
PromptFormat::LabeledSections => write!(f, "Labeled Sections"),
PromptFormat::OnlySnippets => write!(f, "Only Snippets"),
PromptFormat::NumberedLines => write!(f, "Numbered Lines"),
}
}
}
@@ -97,7 +99,7 @@ pub struct Signature {
pub parent_index: Option<usize>,
/// Range of `text` within the file, possibly truncated according to `text_is_truncated`. The
/// file is implicitly the file that contains the descendant declaration or excerpt.
pub range: Range<usize>,
pub range: Range<Line>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -106,7 +108,7 @@ pub struct ReferencedDeclaration {
pub text: String,
pub text_is_truncated: bool,
/// Range of `text` within file, possibly truncated according to `text_is_truncated`
pub range: Range<usize>,
pub range: Range<Line>,
/// Range within `text`
pub signature_range: Range<usize>,
/// Index within `signatures`.
@@ -142,6 +144,8 @@ pub struct DeclarationScoreComponents {
pub normalized_import_similarity: f32,
pub wildcard_import_similarity: f32,
pub normalized_wildcard_import_similarity: f32,
pub included_by_others: usize,
pub includes_others: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -167,10 +171,36 @@ pub struct DebugInfo {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Edit {
pub path: Arc<Path>,
pub range: Range<usize>,
pub range: Range<Line>,
pub content: String,
}
fn is_default<T: Default + PartialEq>(value: &T) -> bool {
*value == T::default()
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, PartialOrd, Eq, Ord)]
pub struct Point {
pub line: Line,
pub column: u32,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, PartialOrd, Eq, Ord)]
#[serde(transparent)]
pub struct Line(pub u32);
impl Add for Line {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
Self(self.0 + rhs.0)
}
}
impl Sub for Line {
type Output = Self;
fn sub(self, rhs: Self) -> Self::Output {
Self(self.0 - rhs.0)
}
}

View File

@@ -1,7 +1,9 @@
//! Zeta2 prompt planning and generation code shared with cloud.
use anyhow::{Context as _, Result, anyhow};
use cloud_llm_client::predict_edits_v3::{self, Event, PromptFormat, ReferencedDeclaration};
use cloud_llm_client::predict_edits_v3::{
self, Event, Line, Point, PromptFormat, ReferencedDeclaration,
};
use indoc::indoc;
use ordered_float::OrderedFloat;
use rustc_hash::{FxHashMap, FxHashSet};
@@ -43,6 +45,42 @@ const LABELED_SECTIONS_SYSTEM_PROMPT: &str = indoc! {r#"
}
"#};
const NUMBERED_LINES_SYSTEM_PROMPT: &str = indoc! {r#"
# Instructions
You are a code completion assistant helping a programmer finish their work. Your task is to:
1. Analyze the edit history to understand what the programmer is trying to achieve
2. Identify any incomplete refactoring or changes that need to be finished
3. Make the remaining edits that a human programmer would logically make next
4. Apply systematic changes consistently across the entire codebase - if you see a pattern starting, complete it everywhere.
Focus on:
- Understanding the intent behind the changes (e.g., improving error handling, refactoring APIs, fixing bugs)
- Completing any partially-applied changes across the codebase
- Ensuring consistency with the programming style and patterns already established
- Making edits that maintain or improve code quality
- If the programmer started refactoring one instance of a pattern, find and update ALL similar instances
- Don't write a lot of code if you're not sure what to do
Rules:
- Do not just mechanically apply patterns - reason about what changes make sense given the context and the programmer's apparent goals.
- Do not just fix syntax errors - look for the broader refactoring pattern and apply it systematically throughout the code.
- Write the edits in the unified diff format as shown in the example.
# Example output:
```
--- a/distill-claude/tmp-outs/edits_history.txt
+++ b/distill-claude/tmp-outs/edits_history.txt
@@ -1,3 +1,3 @@
-
-
-import sys
+import json
```
"#};
pub struct PlannedPrompt<'a> {
request: &'a predict_edits_v3::PredictEditsRequest,
/// Snippets to include in the prompt. These may overlap - they are merged / deduplicated in
@@ -55,6 +93,7 @@ pub fn system_prompt(format: PromptFormat) -> &'static str {
match format {
PromptFormat::MarkedExcerpt => MARKED_EXCERPT_SYSTEM_PROMPT,
PromptFormat::LabeledSections => LABELED_SECTIONS_SYSTEM_PROMPT,
PromptFormat::NumberedLines => NUMBERED_LINES_SYSTEM_PROMPT,
// only intended for use via zeta_cli
PromptFormat::OnlySnippets => "",
}
@@ -63,7 +102,7 @@ pub fn system_prompt(format: PromptFormat) -> &'static str {
#[derive(Clone, Debug)]
pub struct PlannedSnippet<'a> {
path: Arc<Path>,
range: Range<usize>,
range: Range<Line>,
text: &'a str,
// TODO: Indicate this in the output
#[allow(dead_code)]
@@ -79,7 +118,7 @@ pub enum DeclarationStyle {
#[derive(Clone, Debug, Serialize)]
pub struct SectionLabels {
pub excerpt_index: usize,
pub section_ranges: Vec<(Arc<Path>, Range<usize>)>,
pub section_ranges: Vec<(Arc<Path>, Range<Line>)>,
}
impl<'a> PlannedPrompt<'a> {
@@ -196,10 +235,24 @@ impl<'a> PlannedPrompt<'a> {
declaration.text.len()
));
};
let signature_start_line = declaration.range.start
+ Line(
declaration.text[..declaration.signature_range.start]
.lines()
.count() as u32,
);
let signature_end_line = signature_start_line
+ Line(
declaration.text
[declaration.signature_range.start..declaration.signature_range.end]
.lines()
.count() as u32,
);
let range = signature_start_line..signature_end_line;
PlannedSnippet {
path: declaration.path.clone(),
range: (declaration.signature_range.start + declaration.range.start)
..(declaration.signature_range.end + declaration.range.start),
range,
text,
text_is_truncated: declaration.text_is_truncated,
}
@@ -318,7 +371,7 @@ impl<'a> PlannedPrompt<'a> {
}
let excerpt_snippet = PlannedSnippet {
path: self.request.excerpt_path.clone(),
range: self.request.excerpt_range.clone(),
range: self.request.excerpt_line_range.clone(),
text: &self.request.excerpt,
text_is_truncated: false,
};
@@ -328,32 +381,33 @@ impl<'a> PlannedPrompt<'a> {
let mut excerpt_file_insertions = match self.request.prompt_format {
PromptFormat::MarkedExcerpt => vec![
(
self.request.excerpt_range.start,
Point {
line: self.request.excerpt_line_range.start,
column: 0,
},
EDITABLE_REGION_START_MARKER_WITH_NEWLINE,
),
(self.request.cursor_point, CURSOR_MARKER),
(
self.request.excerpt_range.start + self.request.cursor_offset,
CURSOR_MARKER,
),
(
self.request
.excerpt_range
.end
.saturating_sub(0)
.max(self.request.excerpt_range.start),
Point {
line: self.request.excerpt_line_range.end,
column: 0,
},
EDITABLE_REGION_END_MARKER_WITH_NEWLINE,
),
],
PromptFormat::LabeledSections => vec![(
self.request.excerpt_range.start + self.request.cursor_offset,
CURSOR_MARKER,
)],
PromptFormat::LabeledSections => vec![(self.request.cursor_point, CURSOR_MARKER)],
PromptFormat::NumberedLines => vec![(self.request.cursor_point, CURSOR_MARKER)],
PromptFormat::OnlySnippets => vec![],
};
let mut prompt = String::new();
prompt.push_str("## User Edits\n\n");
Self::push_events(&mut prompt, &self.request.events);
if self.request.events.is_empty() {
prompt.push_str("No edits yet.\n");
} else {
Self::push_events(&mut prompt, &self.request.events);
}
prompt.push_str("\n## Code\n\n");
let section_labels =
@@ -391,13 +445,17 @@ impl<'a> PlannedPrompt<'a> {
if *predicted {
writeln!(
output,
"User accepted prediction {:?}:\n```diff\n{}\n```\n",
"User accepted prediction {:?}:\n`````diff\n{}\n`````\n",
path, diff
)
.unwrap();
} else {
writeln!(output, "User edited {:?}:\n```diff\n{}\n```\n", path, diff)
.unwrap();
writeln!(
output,
"User edited {:?}:\n`````diff\n{}\n`````\n",
path, diff
)
.unwrap();
}
}
}
@@ -407,7 +465,7 @@ impl<'a> PlannedPrompt<'a> {
fn push_file_snippets(
&self,
output: &mut String,
excerpt_file_insertions: &mut Vec<(usize, &'static str)>,
excerpt_file_insertions: &mut Vec<(Point, &'static str)>,
file_snippets: Vec<(&'a Path, Vec<&'a PlannedSnippet>, bool)>,
) -> Result<SectionLabels> {
let mut section_ranges = Vec::new();
@@ -417,15 +475,13 @@ impl<'a> PlannedPrompt<'a> {
snippets.sort_by_key(|s| (s.range.start, Reverse(s.range.end)));
// TODO: What if the snippets get expanded too large to be editable?
let mut current_snippet: Option<(&PlannedSnippet, Range<usize>)> = None;
let mut disjoint_snippets: Vec<(&PlannedSnippet, Range<usize>)> = Vec::new();
let mut current_snippet: Option<(&PlannedSnippet, Range<Line>)> = None;
let mut disjoint_snippets: Vec<(&PlannedSnippet, Range<Line>)> = Vec::new();
for snippet in snippets {
if let Some((_, current_snippet_range)) = current_snippet.as_mut()
&& snippet.range.start < current_snippet_range.end
&& snippet.range.start <= current_snippet_range.end
{
if snippet.range.end > current_snippet_range.end {
current_snippet_range.end = snippet.range.end;
}
current_snippet_range.end = current_snippet_range.end.max(snippet.range.end);
continue;
}
if let Some(current_snippet) = current_snippet.take() {
@@ -437,21 +493,24 @@ impl<'a> PlannedPrompt<'a> {
disjoint_snippets.push(current_snippet);
}
writeln!(output, "```{}", file_path.display()).ok();
// TODO: remove filename=?
writeln!(output, "`````filename={}", file_path.display()).ok();
let mut skipped_last_snippet = false;
for (snippet, range) in disjoint_snippets {
let section_index = section_ranges.len();
match self.request.prompt_format {
PromptFormat::MarkedExcerpt | PromptFormat::OnlySnippets => {
if range.start > 0 && !skipped_last_snippet {
PromptFormat::MarkedExcerpt
| PromptFormat::OnlySnippets
| PromptFormat::NumberedLines => {
if range.start.0 > 0 && !skipped_last_snippet {
output.push_str("\n");
}
}
PromptFormat::LabeledSections => {
if is_excerpt_file
&& range.start <= self.request.excerpt_range.start
&& range.end >= self.request.excerpt_range.end
&& range.start <= self.request.excerpt_line_range.start
&& range.end >= self.request.excerpt_line_range.end
{
writeln!(output, "<|current_section|>").ok();
} else {
@@ -460,46 +519,83 @@ impl<'a> PlannedPrompt<'a> {
}
}
let push_full_snippet = |output: &mut String| {
if self.request.prompt_format == PromptFormat::NumberedLines {
for (i, line) in snippet.text.lines().enumerate() {
writeln!(output, "{}|{}", i as u32 + range.start.0 + 1, line)?;
}
} else {
output.push_str(&snippet.text);
}
anyhow::Ok(())
};
if is_excerpt_file {
if self.request.prompt_format == PromptFormat::OnlySnippets {
if range.start >= self.request.excerpt_range.start
&& range.end <= self.request.excerpt_range.end
if range.start >= self.request.excerpt_line_range.start
&& range.end <= self.request.excerpt_line_range.end
{
skipped_last_snippet = true;
} else {
skipped_last_snippet = false;
output.push_str(snippet.text);
}
} else {
let mut last_offset = range.start;
let mut i = 0;
while i < excerpt_file_insertions.len() {
let (offset, insertion) = &excerpt_file_insertions[i];
let found = *offset >= range.start && *offset <= range.end;
} else if !excerpt_file_insertions.is_empty() {
let lines = snippet.text.lines().collect::<Vec<_>>();
let push_line = |output: &mut String, line_ix: usize| {
if self.request.prompt_format == PromptFormat::NumberedLines {
write!(output, "{}|", line_ix as u32 + range.start.0 + 1)?;
}
anyhow::Ok(writeln!(output, "{}", lines[line_ix])?)
};
let mut last_line_ix = 0;
let mut insertion_ix = 0;
while insertion_ix < excerpt_file_insertions.len() {
let (point, insertion) = &excerpt_file_insertions[insertion_ix];
let found = point.line >= range.start && point.line <= range.end;
if found {
excerpt_index = Some(section_index);
output.push_str(
&snippet.text[last_offset - range.start..offset - range.start],
);
output.push_str(insertion);
last_offset = *offset;
excerpt_file_insertions.remove(i);
let insertion_line_ix = (point.line.0 - range.start.0) as usize;
for line_ix in last_line_ix..insertion_line_ix {
push_line(output, line_ix)?;
}
if let Some(next_line) = lines.get(insertion_line_ix) {
if self.request.prompt_format == PromptFormat::NumberedLines {
write!(
output,
"{}|",
insertion_line_ix as u32 + range.start.0 + 1
)?
}
output.push_str(&next_line[..point.column as usize]);
output.push_str(insertion);
writeln!(output, "{}", &next_line[point.column as usize..])?;
} else {
writeln!(output, "{}", insertion)?;
}
last_line_ix = insertion_line_ix + 1;
excerpt_file_insertions.remove(insertion_ix);
continue;
}
i += 1;
insertion_ix += 1;
}
skipped_last_snippet = false;
output.push_str(&snippet.text[last_offset - range.start..]);
for line_ix in last_line_ix..lines.len() {
push_line(output, line_ix)?;
}
} else {
skipped_last_snippet = false;
push_full_snippet(output)?;
}
} else {
skipped_last_snippet = false;
output.push_str(snippet.text);
push_full_snippet(output)?;
}
section_ranges.push((snippet.path.clone(), range));
}
output.push_str("```\n\n");
output.push_str("`````\n\n");
}
Ok(SectionLabels {

View File

@@ -97,6 +97,7 @@ CREATE TABLE "worktree_entries" (
"is_external" BOOL NOT NULL,
"is_ignored" BOOL NOT NULL,
"is_deleted" BOOL NOT NULL,
"is_hidden" BOOL NOT NULL,
"git_status" INTEGER,
"is_fifo" BOOL NOT NULL,
PRIMARY KEY (project_id, worktree_id, id),

View File

@@ -0,0 +1,2 @@
ALTER TABLE "worktree_entries"
ADD "is_hidden" BOOL NOT NULL DEFAULT FALSE;

View File

@@ -282,6 +282,7 @@ impl Database {
git_status: ActiveValue::set(None),
is_external: ActiveValue::set(entry.is_external),
is_deleted: ActiveValue::set(false),
is_hidden: ActiveValue::set(entry.is_hidden),
scan_id: ActiveValue::set(update.scan_id as i64),
is_fifo: ActiveValue::set(entry.is_fifo),
}
@@ -300,6 +301,7 @@ impl Database {
worktree_entry::Column::MtimeNanos,
worktree_entry::Column::CanonicalPath,
worktree_entry::Column::IsIgnored,
worktree_entry::Column::IsHidden,
worktree_entry::Column::ScanId,
])
.to_owned(),
@@ -905,6 +907,7 @@ impl Database {
canonical_path: db_entry.canonical_path,
is_ignored: db_entry.is_ignored,
is_external: db_entry.is_external,
is_hidden: db_entry.is_hidden,
// This is only used in the summarization backlog, so if it's None,
// that just means we won't be able to detect when to resummarize
// based on total number of backlogged bytes - instead, we'd go

View File

@@ -671,6 +671,7 @@ impl Database {
canonical_path: db_entry.canonical_path,
is_ignored: db_entry.is_ignored,
is_external: db_entry.is_external,
is_hidden: db_entry.is_hidden,
// This is only used in the summarization backlog, so if it's None,
// that just means we won't be able to detect when to resummarize
// based on total number of backlogged bytes - instead, we'd go

View File

@@ -19,6 +19,7 @@ pub struct Model {
pub is_ignored: bool,
pub is_external: bool,
pub is_deleted: bool,
pub is_hidden: bool,
pub scan_id: i64,
pub is_fifo: bool,
pub canonical_path: Option<String>,

View File

@@ -30,9 +30,9 @@ impl fmt::Display for ZedVersion {
impl ZedVersion {
pub fn can_collaborate(&self) -> bool {
// v0.198.4 is the first version where we no longer connect to Collab automatically.
// We reject any clients older than that to prevent them from connecting to Collab just for authentication.
if self.0 < SemanticVersion::new(0, 198, 4) {
// v0.204.1 was the first version after the auto-update bug.
// We reject any clients older than that to hope we can persuade them to upgrade.
if self.0 < SemanticVersion::new(0, 204, 1) {
return false;
}

View File

@@ -4,7 +4,7 @@ use crate::{
};
use call::ActiveCall;
use editor::{
DocumentColorsRenderMode, Editor, RowInfo, SelectionEffects,
DocumentColorsRenderMode, Editor, FETCH_COLORS_DEBOUNCE_TIMEOUT, RowInfo, SelectionEffects,
actions::{
ConfirmCodeAction, ConfirmCompletion, ConfirmRename, ContextMenuFirst,
ExpandMacroRecursively, MoveToEnd, Redo, Rename, SelectAll, ToggleCodeActions, Undo,
@@ -2409,6 +2409,7 @@ async fn test_lsp_document_color(cx_a: &mut TestAppContext, cx_b: &mut TestAppCo
.unwrap();
color_request_handle.next().await.unwrap();
executor.advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
executor.run_until_parked();
assert_eq!(

View File

@@ -25,7 +25,7 @@ use gpui::{
use language::{
Diagnostic, DiagnosticEntry, DiagnosticSourceKind, FakeLspAdapter, Language, LanguageConfig,
LanguageMatcher, LineEnding, OffsetRangeExt, Point, Rope,
language_settings::{Formatter, FormatterList, SelectedFormatter},
language_settings::{Formatter, FormatterList},
tree_sitter_rust, tree_sitter_typescript,
};
use lsp::{LanguageServerId, OneOf};
@@ -39,7 +39,7 @@ use project::{
use prompt_store::PromptBuilder;
use rand::prelude::*;
use serde_json::json;
use settings::{PrettierSettingsContent, SettingsStore};
use settings::{LanguageServerFormatterSpecifier, PrettierSettingsContent, SettingsStore};
use std::{
cell::{Cell, RefCell},
env, future, mem,
@@ -4610,14 +4610,13 @@ async fn test_formatting_buffer(
cx_a.update(|cx| {
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings(cx, |file| {
file.project.all_languages.defaults.formatter = Some(SelectedFormatter::List(
FormatterList::Single(Formatter::External {
file.project.all_languages.defaults.formatter =
Some(FormatterList::Single(Formatter::External {
command: "awk".into(),
arguments: Some(
vec!["{sub(/two/,\"{buffer_path}\")}1".to_string()].into(),
),
}),
));
}));
});
});
});
@@ -4708,7 +4707,7 @@ async fn test_prettier_formatting_buffer(
cx_a.update(|cx| {
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings(cx, |file| {
file.project.all_languages.defaults.formatter = Some(SelectedFormatter::Auto);
file.project.all_languages.defaults.formatter = Some(FormatterList::default());
file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent {
allowed: Some(true),
..Default::default()
@@ -4719,8 +4718,8 @@ async fn test_prettier_formatting_buffer(
cx_b.update(|cx| {
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings(cx, |file| {
file.project.all_languages.defaults.formatter = Some(SelectedFormatter::List(
FormatterList::Single(Formatter::LanguageServer { name: None }),
file.project.all_languages.defaults.formatter = Some(FormatterList::Single(
Formatter::LanguageServer(LanguageServerFormatterSpecifier::Current),
));
file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent {
allowed: Some(true),

View File

@@ -14,7 +14,7 @@ use gpui::{
use http_client::BlockedHttpClient;
use language::{
FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, LanguageRegistry,
language_settings::{Formatter, FormatterList, SelectedFormatter, language_settings},
language_settings::{Formatter, FormatterList, language_settings},
tree_sitter_typescript,
};
use node_runtime::NodeRuntime;
@@ -27,7 +27,7 @@ use remote::RemoteClient;
use remote_server::{HeadlessAppState, HeadlessProject};
use rpc::proto;
use serde_json::json;
use settings::{PrettierSettingsContent, SettingsStore};
use settings::{LanguageServerFormatterSpecifier, PrettierSettingsContent, SettingsStore};
use std::{
path::Path,
sync::{Arc, atomic::AtomicUsize},
@@ -491,7 +491,7 @@ async fn test_ssh_collaboration_formatting_with_prettier(
cx_a.update(|cx| {
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings(cx, |file| {
file.project.all_languages.defaults.formatter = Some(SelectedFormatter::Auto);
file.project.all_languages.defaults.formatter = Some(FormatterList::default());
file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent {
allowed: Some(true),
..Default::default()
@@ -502,8 +502,8 @@ async fn test_ssh_collaboration_formatting_with_prettier(
cx_b.update(|cx| {
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings(cx, |file| {
file.project.all_languages.defaults.formatter = Some(SelectedFormatter::List(
FormatterList::Single(Formatter::LanguageServer { name: None }),
file.project.all_languages.defaults.formatter = Some(FormatterList::Single(
Formatter::LanguageServer(LanguageServerFormatterSpecifier::Current),
));
file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent {
allowed: Some(true),
@@ -550,7 +550,7 @@ async fn test_ssh_collaboration_formatting_with_prettier(
cx_a.update(|cx| {
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings(cx, |file| {
file.project.all_languages.defaults.formatter = Some(SelectedFormatter::Auto);
file.project.all_languages.defaults.formatter = Some(FormatterList::default());
file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent {
allowed: Some(true),
..Default::default()

View File

@@ -2250,7 +2250,7 @@ impl CollabPanel {
})),
)
.child(
div().flex().w_full().items_center().child(
v_flex().w_full().items_center().child(
Label::new("Sign in to enable collaboration.")
.color(Color::Muted)
.size(LabelSize::Small),

View File

@@ -1,8 +1,8 @@
[package]
name = "zed-collections"
name = "collections"
version = "0.1.0"
edition.workspace = true
publish = true
publish = false
license = "Apache-2.0"
description = "Standard collection type re-exports used by Zed and GPUI"

View File

@@ -92,7 +92,10 @@ pub async fn init(crash_init: InitCrashHandler) {
#[cfg(target_os = "macos")]
suspend_all_other_threads();
client.ping().unwrap();
// on macos this "ping" is needed to ensure that all our
// `client.send_message` calls have been processed before we trigger the
// minidump request.
client.ping().ok();
client.request_dump(crash_context).is_ok()
} else {
true

View File

@@ -1,12 +1,12 @@
use crate::*;
use anyhow::Context as _;
use anyhow::{Context as _, bail};
use dap::{DebugRequest, StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
use fs::RemoveOptions;
use futures::{StreamExt, TryStreamExt};
use gpui::http_client::AsyncBody;
use gpui::{AsyncApp, SharedString};
use json_dotpath::DotPaths;
use language::LanguageName;
use language::{LanguageName, Toolchain};
use paths::debug_adapters_dir;
use serde_json::Value;
use smol::fs::File;
@@ -20,7 +20,8 @@ use std::{
ffi::OsStr,
path::{Path, PathBuf},
};
use util::{ResultExt, maybe, paths::PathStyle, rel_path::RelPath};
use util::command::new_smol_command;
use util::{ResultExt, paths::PathStyle, rel_path::RelPath};
#[derive(Default)]
pub(crate) struct PythonDebugAdapter {
@@ -92,12 +93,16 @@ impl PythonDebugAdapter {
})
}
async fn fetch_wheel(&self, delegate: &Arc<dyn DapDelegate>) -> Result<Arc<Path>, String> {
async fn fetch_wheel(
&self,
toolchain: Option<Toolchain>,
delegate: &Arc<dyn DapDelegate>,
) -> Result<Arc<Path>> {
let download_dir = debug_adapters_dir().join(Self::ADAPTER_NAME).join("wheels");
std::fs::create_dir_all(&download_dir).map_err(|e| e.to_string())?;
let system_python = self.base_venv_path(delegate).await?;
std::fs::create_dir_all(&download_dir)?;
let venv_python = self.base_venv_path(toolchain, delegate).await?;
let installation_succeeded = util::command::new_smol_command(system_python.as_ref())
let installation_succeeded = util::command::new_smol_command(venv_python.as_ref())
.args([
"-m",
"pip",
@@ -109,36 +114,36 @@ impl PythonDebugAdapter {
])
.output()
.await
.map_err(|e| format!("{e}"))?
.context("spawn system python")?
.status
.success();
if !installation_succeeded {
return Err("debugpy installation failed (could not fetch Debugpy's wheel)".into());
bail!("debugpy installation failed (could not fetch Debugpy's wheel)");
}
let wheel_path = std::fs::read_dir(&download_dir)
.map_err(|e| e.to_string())?
let wheel_path = std::fs::read_dir(&download_dir)?
.find_map(|entry| {
entry.ok().filter(|e| {
e.file_type().is_ok_and(|typ| typ.is_file())
&& Path::new(&e.file_name()).extension() == Some("whl".as_ref())
})
})
.ok_or_else(|| String::from("Did not find a .whl in {download_dir}"))?;
.with_context(|| format!("Did not find a .whl in {download_dir:?}"))?;
util::archive::extract_zip(
&debug_adapters_dir().join(Self::ADAPTER_NAME),
File::open(&wheel_path.path())
.await
.map_err(|e| e.to_string())?,
File::open(&wheel_path.path()).await?,
)
.await
.map_err(|e| e.to_string())?;
.await?;
Ok(Arc::from(wheel_path.path()))
}
async fn maybe_fetch_new_wheel(&self, delegate: &Arc<dyn DapDelegate>) {
async fn maybe_fetch_new_wheel(
&self,
toolchain: Option<Toolchain>,
delegate: &Arc<dyn DapDelegate>,
) -> Result<()> {
let latest_release = delegate
.http_client()
.get(
@@ -148,62 +153,61 @@ impl PythonDebugAdapter {
)
.await
.log_err();
maybe!(async move {
let response = latest_release.filter(|response| response.status().is_success())?;
let response = latest_release
.filter(|response| response.status().is_success())
.context("getting latest release")?;
let download_dir = debug_adapters_dir().join(Self::ADAPTER_NAME);
std::fs::create_dir_all(&download_dir).ok()?;
let download_dir = debug_adapters_dir().join(Self::ADAPTER_NAME);
std::fs::create_dir_all(&download_dir)?;
let mut output = String::new();
response
.into_body()
.read_to_string(&mut output)
.await
.ok()?;
let as_json = serde_json::Value::from_str(&output).ok()?;
let latest_version = as_json.get("info").and_then(|info| {
let mut output = String::new();
response.into_body().read_to_string(&mut output).await?;
let as_json = serde_json::Value::from_str(&output)?;
let latest_version = as_json
.get("info")
.and_then(|info| {
info.get("version")
.and_then(|version| version.as_str())
.map(ToOwned::to_owned)
})?;
let dist_info_dirname: OsString = format!("debugpy-{latest_version}.dist-info").into();
let is_up_to_date = delegate
.fs()
.read_dir(&debug_adapters_dir().join(Self::ADAPTER_NAME))
.await
.ok()?
.into_stream()
.any(async |entry| {
entry.is_ok_and(|e| e.file_name().is_some_and(|name| name == dist_info_dirname))
})
.await;
})
.context("parsing latest release information")?;
let dist_info_dirname: OsString = format!("debugpy-{latest_version}.dist-info").into();
let is_up_to_date = delegate
.fs()
.read_dir(&debug_adapters_dir().join(Self::ADAPTER_NAME))
.await?
.into_stream()
.any(async |entry| {
entry.is_ok_and(|e| e.file_name().is_some_and(|name| name == dist_info_dirname))
})
.await;
if !is_up_to_date {
delegate
.fs()
.remove_dir(
&debug_adapters_dir().join(Self::ADAPTER_NAME),
RemoveOptions {
recursive: true,
ignore_if_not_exists: true,
},
)
.await
.ok()?;
self.fetch_wheel(delegate).await.ok()?;
}
Some(())
})
.await;
if !is_up_to_date {
delegate
.fs()
.remove_dir(
&debug_adapters_dir().join(Self::ADAPTER_NAME),
RemoveOptions {
recursive: true,
ignore_if_not_exists: true,
},
)
.await?;
self.fetch_wheel(toolchain, delegate).await?;
}
anyhow::Ok(())
}
async fn fetch_debugpy_whl(
&self,
toolchain: Option<Toolchain>,
delegate: &Arc<dyn DapDelegate>,
) -> Result<Arc<Path>, String> {
self.debugpy_whl_base_path
.get_or_init(|| async move {
self.maybe_fetch_new_wheel(delegate).await;
self.maybe_fetch_new_wheel(toolchain, delegate)
.await
.map_err(|e| format!("{e}"))?;
Ok(Arc::from(
debug_adapters_dir()
.join(Self::ADAPTER_NAME)
@@ -216,12 +220,24 @@ impl PythonDebugAdapter {
.clone()
}
async fn base_venv_path(&self, delegate: &Arc<dyn DapDelegate>) -> Result<Arc<Path>, String> {
self.base_venv_path
async fn base_venv_path(
&self,
toolchain: Option<Toolchain>,
delegate: &Arc<dyn DapDelegate>,
) -> Result<Arc<Path>> {
let result = self.base_venv_path
.get_or_init(|| async {
let base_python = Self::system_python_name(delegate)
.await
.ok_or_else(|| String::from("Could not find a Python installation"))?;
let base_python = if let Some(toolchain) = toolchain {
toolchain.path.to_string()
} else {
Self::system_python_name(delegate).await.ok_or_else(|| {
let mut message = "Could not find a Python installation".to_owned();
if cfg!(windows){
message.push_str(". Install Python from the Microsoft Store, or manually from https://www.python.org/downloads/windows.")
}
message
})?
};
let did_succeed = util::command::new_smol_command(base_python)
.args(["-m", "venv", "zed_base_venv"])
@@ -239,35 +255,50 @@ impl PythonDebugAdapter {
return Err("Failed to create base virtual environment".into());
}
const DIR: &str = if cfg!(target_os = "windows") {
"Scripts"
const PYTHON_PATH: &str = if cfg!(target_os = "windows") {
"Scripts/python.exe"
} else {
"bin"
"bin/python3"
};
Ok(Arc::from(
paths::debug_adapters_dir()
.join(Self::DEBUG_ADAPTER_NAME.as_ref())
.join("zed_base_venv")
.join(DIR)
.join("python3")
.join(PYTHON_PATH)
.as_ref(),
))
})
.await
.clone()
.clone();
match result {
Ok(path) => Ok(path),
Err(e) => Err(anyhow::anyhow!("{e}")),
}
}
async fn system_python_name(delegate: &Arc<dyn DapDelegate>) -> Option<String> {
const BINARY_NAMES: [&str; 3] = ["python3", "python", "py"];
let mut name = None;
for cmd in BINARY_NAMES {
name = delegate
.which(OsStr::new(cmd))
let Some(path) = delegate.which(OsStr::new(cmd)).await else {
continue;
};
// Try to detect situations where `python3` exists but is not a real Python interpreter.
// Notably, on fresh Windows installs, `python3` is a shim that opens the Microsoft Store app
// when run with no arguments, and just fails otherwise.
let Some(output) = new_smol_command(&path)
.args(["-c", "print(1 + 2)"])
.output()
.await
.map(|path| path.to_string_lossy().into_owned());
if name.is_some() {
break;
.ok()
else {
continue;
};
if output.stdout.trim_ascii() != b"3" {
continue;
}
name = Some(path.to_string_lossy().into_owned());
break;
}
name
}
@@ -746,15 +777,10 @@ impl DebugAdapter for PythonDebugAdapter {
)
.await;
let debugpy_path = self
.fetch_debugpy_whl(delegate)
self.fetch_debugpy_whl(toolchain.clone(), delegate)
.await
.map_err(|e| anyhow::anyhow!("{e}"))?;
if let Some(toolchain) = &toolchain {
log::debug!(
"Found debugpy in toolchain environment: {}",
debugpy_path.display()
);
return self
.get_installed_binary(
delegate,

View File

@@ -963,26 +963,21 @@ pub fn init(cx: &mut App) {
};
let project = workspace.project();
if project.read(cx).is_local() {
log_store.update(cx, |store, cx| {
store.add_project(project, cx);
});
}
log_store.update(cx, |store, cx| {
store.add_project(project, cx);
});
let log_store = log_store.clone();
workspace.register_action(move |workspace, _: &OpenDebugAdapterLogs, window, cx| {
let project = workspace.project().read(cx);
if project.is_local() {
workspace.add_item_to_active_pane(
Box::new(cx.new(|cx| {
DapLogView::new(workspace.project().clone(), log_store.clone(), window, cx)
})),
None,
true,
window,
cx,
);
}
workspace.add_item_to_active_pane(
Box::new(cx.new(|cx| {
DapLogView::new(workspace.project().clone(), log_store.clone(), window, cx)
})),
None,
true,
window,
cx,
);
});
})
.detach();

View File

@@ -268,12 +268,12 @@ impl DebugPanel {
async move |_, cx| {
if let Err(error) = task.await {
log::error!("{error}");
log::error!("{error:#}");
session
.update(cx, |session, cx| {
session
.console_output(cx)
.unbounded_send(format!("error: {}", error))
.unbounded_send(format!("error: {:#}", error))
.ok();
session.shutdown(cx)
})?

View File

@@ -1,7 +1,7 @@
use std::rc::Rc;
use collections::HashMap;
use gpui::{Entity, WeakEntity};
use gpui::{Corner, Entity, WeakEntity};
use project::debugger::session::{ThreadId, ThreadStatus};
use ui::{CommonAnimationExt, ContextMenu, DropdownMenu, DropdownStyle, Indicator, prelude::*};
use util::{maybe, truncate_and_trailoff};
@@ -211,6 +211,7 @@ impl DebugPanel {
this
}),
)
.attach(Corner::BottomLeft)
.style(DropdownStyle::Ghost)
.handle(self.session_picker_menu_handle.clone());
@@ -322,6 +323,7 @@ impl DebugPanel {
this
}),
)
.attach(Corner::BottomLeft)
.disabled(session_terminated)
.style(DropdownStyle::Ghost)
.handle(self.thread_picker_menu_handle.clone()),

View File

@@ -937,6 +937,7 @@ impl RunningState {
let task_store = project.read(cx).task_store().downgrade();
let weak_project = project.downgrade();
let weak_workspace = workspace.downgrade();
let is_windows = project.read(cx).path_style(cx).is_windows();
let remote_shell = project
.read(cx)
.remote_client()
@@ -1029,7 +1030,7 @@ impl RunningState {
task.resolved.shell = Shell::Program(remote_shell);
}
let builder = ShellBuilder::new(&task.resolved.shell);
let builder = ShellBuilder::new(&task.resolved.shell, is_windows);
let command_label = builder.command_label(task.resolved.command.as_deref().unwrap_or(""));
let (command, args) =
builder.build(task.resolved.command.clone(), &task.resolved.args);

View File

@@ -669,11 +669,7 @@ impl ConsoleQueryBarCompletionProvider {
&snapshot,
),
new_text: string_match.string.clone(),
label: CodeLabel {
filter_range: 0..string_match.string.len(),
text: string_match.string.clone(),
runs: Vec::new(),
},
label: CodeLabel::plain(string_match.string.clone(), None),
icon_path: None,
documentation: Some(CompletionDocumentation::MultiLineMarkdown(
variable_value.into(),
@@ -782,11 +778,7 @@ impl ConsoleQueryBarCompletionProvider {
&snapshot,
),
new_text,
label: CodeLabel {
filter_range: 0..completion.label.len(),
text: completion.label,
runs: Vec::new(),
},
label: CodeLabel::plain(completion.label, None),
icon_path: None,
documentation: completion.detail.map(|detail| {
CompletionDocumentation::MultiLineMarkdown(detail.into())

View File

@@ -965,10 +965,11 @@ async fn heuristic_syntactic_expand(
let row_count = node_end.row - node_start.row + 1;
let mut ancestor_range = None;
let reached_outline_node = cx.background_executor().scoped({
let node_range = node_range.clone();
let outline_range = outline_range.clone();
let ancestor_range = &mut ancestor_range;
|scope| {scope.spawn(async move {
let node_range = node_range.clone();
let outline_range = outline_range.clone();
let ancestor_range = &mut ancestor_range;
|scope| {
scope.spawn(async move {
// Stop if we've exceeded the row count or reached an outline node. Then, find the interval
// of node children which contains the query range. For example, this allows just returning
// the header of a declaration rather than the entire declaration.
@@ -980,8 +981,11 @@ async fn heuristic_syntactic_expand(
if cursor.goto_first_child() {
loop {
let child_node = cursor.node();
let child_range = previous_end..Point::from_ts_point(child_node.end_position());
if included_child_start.is_none() && child_range.contains(&input_range.start) {
let child_range =
previous_end..Point::from_ts_point(child_node.end_position());
if included_child_start.is_none()
&& child_range.contains(&input_range.start)
{
included_child_start = Some(child_range.start);
}
if child_range.contains(&input_range.end) {
@@ -997,19 +1001,22 @@ async fn heuristic_syntactic_expand(
if let Some(start) = included_child_start {
let row_count = end.row - start.row;
if row_count < max_row_count {
*ancestor_range = Some(Some(RangeInclusive::new(start.row, end.row)));
*ancestor_range =
Some(Some(RangeInclusive::new(start.row, end.row)));
return;
}
}
log::info!(
"Expanding to ancestor started on {} node exceeding row limit of {max_row_count}.",
"Expanding to ancestor started on {} node\
exceeding row limit of {max_row_count}.",
node.grammar_name()
);
*ancestor_range = Some(None);
}
})
}});
}
});
reached_outline_node.await;
if let Some(node) = ancestor_range {
return node;

View File

@@ -20,6 +20,8 @@ util.workspace = true
workspace-hack.workspace = true
zed.workspace = true
zlog.workspace = true
task.workspace = true
theme.workspace = true
[lints]
workspace = true

View File

@@ -53,9 +53,20 @@ fn main() -> Result<()> {
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum PreprocessorError {
ActionNotFound { action_name: String },
DeprecatedActionUsed { used: String, should_be: String },
ActionNotFound {
action_name: String,
},
DeprecatedActionUsed {
used: String,
should_be: String,
},
InvalidFrontmatterLine(String),
InvalidSettingsJson {
file: std::path::PathBuf,
line: usize,
snippet: String,
error: String,
},
}
impl PreprocessorError {
@@ -72,6 +83,20 @@ impl PreprocessorError {
}
PreprocessorError::ActionNotFound { action_name }
}
fn new_for_invalid_settings_json(
chapter: &Chapter,
location: usize,
snippet: String,
error: String,
) -> Self {
PreprocessorError::InvalidSettingsJson {
file: chapter.path.clone().expect("chapter has path"),
line: chapter.content[..location].lines().count() + 1,
snippet,
error,
}
}
}
impl std::fmt::Display for PreprocessorError {
@@ -88,6 +113,21 @@ impl std::fmt::Display for PreprocessorError {
"Deprecated action used: {} should be {}",
used, should_be
),
PreprocessorError::InvalidSettingsJson {
file,
line,
snippet,
error,
} => {
write!(
f,
"Invalid settings JSON at {}:{}\nError: {}\n\n{}",
file.display(),
line,
error,
snippet
)
}
}
}
}
@@ -100,11 +140,11 @@ fn handle_preprocessing() -> Result<()> {
let (_ctx, mut book) = CmdPreprocessor::parse_input(input.as_bytes())?;
let mut errors = HashSet::<PreprocessorError>::new();
handle_frontmatter(&mut book, &mut errors);
template_big_table_of_actions(&mut book);
template_and_validate_keybindings(&mut book, &mut errors);
template_and_validate_actions(&mut book, &mut errors);
template_and_validate_json_snippets(&mut book, &mut errors);
if !errors.is_empty() {
const ANSI_RED: &str = "\x1b[31m";
@@ -235,6 +275,161 @@ fn find_binding(os: &str, action: &str) -> Option<String> {
})
}
fn template_and_validate_json_snippets(book: &mut Book, errors: &mut HashSet<PreprocessorError>) {
fn for_each_labeled_code_block_mut(
book: &mut Book,
errors: &mut HashSet<PreprocessorError>,
f: impl Fn(&str, &str) -> anyhow::Result<()>,
) {
const TAGGED_JSON_BLOCK_START: &'static str = "```json [";
const JSON_BLOCK_END: &'static str = "```";
for_each_chapter_mut(book, |chapter| {
let mut offset = 0;
while let Some(loc) = chapter.content[offset..].find(TAGGED_JSON_BLOCK_START) {
let loc = loc + offset;
let tag_start = loc + TAGGED_JSON_BLOCK_START.len();
offset = tag_start;
let Some(tag_end) = chapter.content[tag_start..].find(']') else {
errors.insert(PreprocessorError::new_for_invalid_settings_json(
chapter,
loc,
chapter.content[loc..tag_start].to_string(),
"Unclosed JSON block tag".to_string(),
));
continue;
};
let tag_end = tag_end + tag_start;
let tag = &chapter.content[tag_start..tag_end];
if tag.contains('\n') {
errors.insert(PreprocessorError::new_for_invalid_settings_json(
chapter,
loc,
chapter.content[loc..tag_start].to_string(),
"Unclosed JSON block tag".to_string(),
));
continue;
}
let snippet_start = tag_end + 1;
offset = snippet_start;
let Some(snippet_end) = chapter.content[snippet_start..].find(JSON_BLOCK_END)
else {
errors.insert(PreprocessorError::new_for_invalid_settings_json(
chapter,
loc,
chapter.content[loc..tag_end + 1].to_string(),
"Missing closing code block".to_string(),
));
continue;
};
let snippet_end = snippet_start + snippet_end;
let snippet_json = &chapter.content[snippet_start..snippet_end];
offset = snippet_end + 3;
if let Err(err) = f(tag, snippet_json) {
errors.insert(PreprocessorError::new_for_invalid_settings_json(
chapter,
loc,
chapter.content[loc..snippet_end + 3].to_string(),
err.to_string(),
));
continue;
};
let tag_range_complete = tag_start - 1..tag_end + 1;
offset -= tag_range_complete.len();
chapter.content.replace_range(tag_range_complete, "");
}
});
}
for_each_labeled_code_block_mut(book, errors, |label, snippet_json| {
let mut snippet_json_fixed = snippet_json
.to_string()
.replace("\n>", "\n")
.trim()
.to_string();
while snippet_json_fixed.starts_with("//") {
if let Some(line_end) = snippet_json_fixed.find('\n') {
snippet_json_fixed.replace_range(0..line_end, "");
snippet_json_fixed = snippet_json_fixed.trim().to_string();
}
}
match label {
"settings" => {
if !snippet_json_fixed.starts_with('{') || !snippet_json_fixed.ends_with('}') {
snippet_json_fixed.insert(0, '{');
snippet_json_fixed.push_str("\n}");
}
settings::parse_json_with_comments::<settings::SettingsContent>(
&snippet_json_fixed,
)?;
}
"keymap" => {
if !snippet_json_fixed.starts_with('[') || !snippet_json_fixed.ends_with(']') {
snippet_json_fixed.insert(0, '[');
snippet_json_fixed.push_str("\n]");
}
let keymap = settings::KeymapFile::parse(&snippet_json_fixed)
.context("Failed to parse keymap JSON")?;
for section in keymap.sections() {
for (keystrokes, action) in section.bindings() {
keystrokes
.split_whitespace()
.map(|source| gpui::Keystroke::parse(source))
.collect::<std::result::Result<Vec<_>, _>>()
.context("Failed to parse keystroke")?;
if let Some((action_name, _)) = settings::KeymapFile::parse_action(action)
.map_err(|err| anyhow::format_err!(err))
.context("Failed to parse action")?
{
anyhow::ensure!(
find_action_by_name(action_name).is_some(),
"Action not found: {}",
action_name
);
}
}
}
}
"debug" => {
if !snippet_json_fixed.starts_with('[') || !snippet_json_fixed.ends_with(']') {
snippet_json_fixed.insert(0, '[');
snippet_json_fixed.push_str("\n]");
}
settings::parse_json_with_comments::<task::DebugTaskFile>(&snippet_json_fixed)?;
}
"tasks" => {
if !snippet_json_fixed.starts_with('[') || !snippet_json_fixed.ends_with(']') {
snippet_json_fixed.insert(0, '[');
snippet_json_fixed.push_str("\n]");
}
settings::parse_json_with_comments::<task::TaskTemplates>(&snippet_json_fixed)?;
}
"icon-theme" => {
if !snippet_json_fixed.starts_with('{') || !snippet_json_fixed.ends_with('}') {
snippet_json_fixed.insert(0, '{');
snippet_json_fixed.push_str("\n}");
}
settings::parse_json_with_comments::<theme::IconThemeFamilyContent>(
&snippet_json_fixed,
)?;
}
label => {
anyhow::bail!("Unexpected JSON code block tag: {}", label)
}
};
Ok(())
});
}
/// Removes any configurable options from the stringified action if existing,
/// ensuring that only the actual action name is returned. If the action consists
/// only of a string and nothing else, the string is returned as-is.

View File

@@ -1,3 +1,4 @@
use cloud_llm_client::predict_edits_v3::{self, Line};
use language::{Language, LanguageId};
use project::ProjectEntryId;
use std::ops::Range;
@@ -91,6 +92,18 @@ impl Declaration {
}
}
pub fn item_line_range(&self) -> Range<Line> {
match self {
Declaration::File { declaration, .. } => declaration.item_line_range.clone(),
Declaration::Buffer {
declaration, rope, ..
} => {
Line(rope.offset_to_point(declaration.item_range.start).row)
..Line(rope.offset_to_point(declaration.item_range.end).row)
}
}
}
pub fn item_text(&self) -> (Cow<'_, str>, bool) {
match self {
Declaration::File { declaration, .. } => (
@@ -130,6 +143,18 @@ impl Declaration {
}
}
pub fn signature_line_range(&self) -> Range<Line> {
match self {
Declaration::File { declaration, .. } => declaration.signature_line_range.clone(),
Declaration::Buffer {
declaration, rope, ..
} => {
Line(rope.offset_to_point(declaration.signature_range.start).row)
..Line(rope.offset_to_point(declaration.signature_range.end).row)
}
}
}
pub fn signature_range_in_item_text(&self) -> Range<usize> {
let signature_range = self.signature_range();
let item_range = self.item_range();
@@ -142,7 +167,7 @@ fn expand_range_to_line_boundaries_and_truncate(
range: &Range<usize>,
limit: usize,
rope: &Rope,
) -> (Range<usize>, bool) {
) -> (Range<usize>, Range<predict_edits_v3::Line>, bool) {
let mut point_range = rope.offset_to_point(range.start)..rope.offset_to_point(range.end);
point_range.start.column = 0;
point_range.end.row += 1;
@@ -155,7 +180,10 @@ fn expand_range_to_line_boundaries_and_truncate(
item_range.end = item_range.start + limit;
}
item_range.end = rope.clip_offset(item_range.end, Bias::Left);
(item_range, is_truncated)
let line_range =
predict_edits_v3::Line(point_range.start.row)..predict_edits_v3::Line(point_range.end.row);
(item_range, line_range, is_truncated)
}
#[derive(Debug, Clone)]
@@ -164,25 +192,30 @@ pub struct FileDeclaration {
pub identifier: Identifier,
/// offset range of the declaration in the file, expanded to line boundaries and truncated
pub item_range: Range<usize>,
/// line range of the declaration in the file, potentially truncated
pub item_line_range: Range<predict_edits_v3::Line>,
/// text of `item_range`
pub text: Arc<str>,
/// whether `text` was truncated
pub text_is_truncated: bool,
/// offset range of the signature in the file, expanded to line boundaries and truncated
pub signature_range: Range<usize>,
/// line range of the signature in the file, truncated
pub signature_line_range: Range<Line>,
/// whether `signature` was truncated
pub signature_is_truncated: bool,
}
impl FileDeclaration {
pub fn from_outline(declaration: OutlineDeclaration, rope: &Rope) -> FileDeclaration {
let (item_range_in_file, text_is_truncated) = expand_range_to_line_boundaries_and_truncate(
&declaration.item_range,
ITEM_TEXT_TRUNCATION_LENGTH,
rope,
);
let (item_range_in_file, item_line_range_in_file, text_is_truncated) =
expand_range_to_line_boundaries_and_truncate(
&declaration.item_range,
ITEM_TEXT_TRUNCATION_LENGTH,
rope,
);
let (mut signature_range_in_file, mut signature_is_truncated) =
let (mut signature_range_in_file, signature_line_range, mut signature_is_truncated) =
expand_range_to_line_boundaries_and_truncate(
&declaration.signature_range,
ITEM_TEXT_TRUNCATION_LENGTH,
@@ -202,6 +235,7 @@ impl FileDeclaration {
parent: None,
identifier: declaration.identifier,
signature_range: signature_range_in_file,
signature_line_range,
signature_is_truncated,
text: rope
.chunks_in_range(item_range_in_file.clone())
@@ -209,6 +243,7 @@ impl FileDeclaration {
.into(),
text_is_truncated,
item_range: item_range_in_file,
item_line_range: item_line_range_in_file,
}
}
}
@@ -225,12 +260,13 @@ pub struct BufferDeclaration {
impl BufferDeclaration {
pub fn from_outline(declaration: OutlineDeclaration, rope: &Rope) -> Self {
let (item_range, item_range_is_truncated) = expand_range_to_line_boundaries_and_truncate(
&declaration.item_range,
ITEM_TEXT_TRUNCATION_LENGTH,
rope,
);
let (signature_range, signature_range_is_truncated) =
let (item_range, _item_line_range, item_range_is_truncated) =
expand_range_to_line_boundaries_and_truncate(
&declaration.item_range,
ITEM_TEXT_TRUNCATION_LENGTH,
rope,
);
let (signature_range, _signature_line_range, signature_range_is_truncated) =
expand_range_to_line_boundaries_and_truncate(
&declaration.signature_range,
ITEM_TEXT_TRUNCATION_LENGTH,

View File

@@ -2,10 +2,12 @@ use cloud_llm_client::predict_edits_v3::DeclarationScoreComponents;
use collections::HashMap;
use language::BufferSnapshot;
use ordered_float::OrderedFloat;
use project::ProjectEntryId;
use serde::Serialize;
use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc};
use strum::EnumIter;
use text::{Point, ToPoint};
use util::RangeExt as _;
use crate::{
CachedDeclarationPath, Declaration, EditPredictionExcerpt, Identifier,
@@ -73,7 +75,7 @@ impl ScoredDeclaration {
}
pub fn retrieval_score(&self) -> f32 {
if self.components.is_same_file {
let mut score = if self.components.is_same_file {
10.0 / self.components.same_file_declaration_count as f32
} else if self.components.path_import_match_count > 0 {
3.0
@@ -85,7 +87,10 @@ impl ScoredDeclaration {
0.5 * self.components.normalized_wildcard_import_similarity
} else {
1.0 / self.components.declaration_count as f32
}
};
score *= 1. + self.components.included_by_others as f32 / 2.;
score *= 1. + self.components.includes_others as f32 / 4.;
score
}
pub fn size(&self, style: DeclarationStyle) -> usize {
@@ -133,194 +138,210 @@ pub fn scored_declarations(
}
}
let mut declarations = identifier_to_references
.into_iter()
.flat_map(|(identifier, references)| {
let mut import_occurrences = Vec::new();
let mut import_paths = Vec::new();
let mut found_external_identifier: Option<&Identifier> = None;
let mut scored_declarations = Vec::new();
let mut project_entry_id_to_outline_ranges: HashMap<ProjectEntryId, Vec<Range<usize>>> =
HashMap::default();
for (identifier, references) in identifier_to_references {
let mut import_occurrences = Vec::new();
let mut import_paths = Vec::new();
let mut found_external_identifier: Option<&Identifier> = None;
if let Some(imports) = imports.identifier_to_imports.get(&identifier) {
// only use alias when it's the only import, could be generalized if some language
// has overlapping aliases
//
// TODO: when an aliased declaration is included in the prompt, should include the
// aliasing in the prompt.
//
// TODO: For SourceFuzzy consider having componentwise comparison that pays
// attention to ordering.
if let [
Import::Alias {
module,
external_identifier,
},
] = imports.as_slice()
{
match module {
Module::Namespace(namespace) => {
import_occurrences.push(namespace.occurrences())
}
Module::SourceExact(path) => import_paths.push(path),
Module::SourceFuzzy(path) => {
import_occurrences.push(Occurrences::from_path(&path))
}
if let Some(imports) = imports.identifier_to_imports.get(&identifier) {
// only use alias when it's the only import, could be generalized if some language
// has overlapping aliases
//
// TODO: when an aliased declaration is included in the prompt, should include the
// aliasing in the prompt.
//
// TODO: For SourceFuzzy consider having componentwise comparison that pays
// attention to ordering.
if let [
Import::Alias {
module,
external_identifier,
},
] = imports.as_slice()
{
match module {
Module::Namespace(namespace) => {
import_occurrences.push(namespace.occurrences())
}
found_external_identifier = Some(&external_identifier);
} else {
for import in imports {
match import {
Import::Direct { module } => match module {
Module::Namespace(namespace) => {
import_occurrences.push(namespace.occurrences())
}
Module::SourceExact(path) => import_paths.push(path),
Module::SourceFuzzy(path) => {
import_occurrences.push(Occurrences::from_path(&path))
}
},
Import::Alias { .. } => {}
}
Module::SourceExact(path) => import_paths.push(path),
Module::SourceFuzzy(path) => {
import_occurrences.push(Occurrences::from_path(&path))
}
}
}
let identifier_to_lookup = found_external_identifier.unwrap_or(&identifier);
// TODO: update this to be able to return more declarations? Especially if there is the
// ability to quickly filter a large list (based on imports)
let declarations = index
.declarations_for_identifier::<MAX_IDENTIFIER_DECLARATION_COUNT>(
&identifier_to_lookup,
);
let declaration_count = declarations.len();
if declaration_count == 0 {
return Vec::new();
}
// TODO: option to filter out other candidates when same file / import match
let mut checked_declarations = Vec::new();
for (declaration_id, declaration) in declarations {
match declaration {
Declaration::Buffer {
buffer_id,
declaration: buffer_declaration,
..
} => {
if buffer_id == &current_buffer.remote_id() {
let already_included_in_prompt =
range_intersection(&buffer_declaration.item_range, &excerpt.range)
.is_some()
|| excerpt.parent_declarations.iter().any(
|(excerpt_parent, _)| excerpt_parent == &declaration_id,
);
if !options.omit_excerpt_overlaps || !already_included_in_prompt {
let declaration_line = buffer_declaration
.item_range
.start
.to_point(current_buffer)
.row;
let declaration_line_distance = (cursor_point.row as i32
- declaration_line as i32)
.unsigned_abs();
checked_declarations.push(CheckedDeclaration {
declaration,
same_file_line_distance: Some(declaration_line_distance),
path_import_match_count: 0,
wildcard_path_import_match_count: 0,
});
found_external_identifier = Some(&external_identifier);
} else {
for import in imports {
match import {
Import::Direct { module } => match module {
Module::Namespace(namespace) => {
import_occurrences.push(namespace.occurrences())
}
continue;
} else {
Module::SourceExact(path) => import_paths.push(path),
Module::SourceFuzzy(path) => {
import_occurrences.push(Occurrences::from_path(&path))
}
},
Import::Alias { .. } => {}
}
}
}
}
let identifier_to_lookup = found_external_identifier.unwrap_or(&identifier);
// TODO: update this to be able to return more declarations? Especially if there is the
// ability to quickly filter a large list (based on imports)
let identifier_declarations = index
.declarations_for_identifier::<MAX_IDENTIFIER_DECLARATION_COUNT>(&identifier_to_lookup);
let declaration_count = identifier_declarations.len();
if declaration_count == 0 {
continue;
}
// TODO: option to filter out other candidates when same file / import match
let mut checked_declarations = Vec::with_capacity(declaration_count);
for (declaration_id, declaration) in identifier_declarations {
match declaration {
Declaration::Buffer {
buffer_id,
declaration: buffer_declaration,
..
} => {
if buffer_id == &current_buffer.remote_id() {
let already_included_in_prompt =
range_intersection(&buffer_declaration.item_range, &excerpt.range)
.is_some()
|| excerpt
.parent_declarations
.iter()
.any(|(excerpt_parent, _)| excerpt_parent == &declaration_id);
if !options.omit_excerpt_overlaps || !already_included_in_prompt {
let declaration_line = buffer_declaration
.item_range
.start
.to_point(current_buffer)
.row;
let declaration_line_distance =
(cursor_point.row as i32 - declaration_line as i32).unsigned_abs();
checked_declarations.push(CheckedDeclaration {
declaration,
same_file_line_distance: Some(declaration_line_distance),
path_import_match_count: 0,
wildcard_path_import_match_count: 0,
});
}
continue;
} else {
}
Declaration::File { .. } => {}
}
let declaration_path = declaration.cached_path();
let path_import_match_count = import_paths
.iter()
.filter(|import_path| {
declaration_path_matches_import(&declaration_path, import_path)
})
.count();
let wildcard_path_import_match_count = wildcard_import_paths
.iter()
.filter(|import_path| {
declaration_path_matches_import(&declaration_path, import_path)
})
.count();
checked_declarations.push(CheckedDeclaration {
declaration,
same_file_line_distance: None,
path_import_match_count,
wildcard_path_import_match_count,
});
Declaration::File { .. } => {}
}
let mut max_import_similarity = 0.0;
let mut max_wildcard_import_similarity = 0.0;
let mut scored_declarations_for_identifier = checked_declarations
.into_iter()
.map(|checked_declaration| {
let same_file_declaration_count =
index.file_declaration_count(checked_declaration.declaration);
let declaration = score_declaration(
&identifier,
&references,
checked_declaration,
same_file_declaration_count,
declaration_count,
&excerpt_occurrences,
&adjacent_occurrences,
&import_occurrences,
&wildcard_import_occurrences,
cursor_point,
current_buffer,
);
if declaration.components.import_similarity > max_import_similarity {
max_import_similarity = declaration.components.import_similarity;
}
if declaration.components.wildcard_import_similarity
> max_wildcard_import_similarity
{
max_wildcard_import_similarity =
declaration.components.wildcard_import_similarity;
}
declaration
let declaration_path = declaration.cached_path();
let path_import_match_count = import_paths
.iter()
.filter(|import_path| {
declaration_path_matches_import(&declaration_path, import_path)
})
.collect::<Vec<_>>();
.count();
let wildcard_path_import_match_count = wildcard_import_paths
.iter()
.filter(|import_path| {
declaration_path_matches_import(&declaration_path, import_path)
})
.count();
checked_declarations.push(CheckedDeclaration {
declaration,
same_file_line_distance: None,
path_import_match_count,
wildcard_path_import_match_count,
});
}
if max_import_similarity > 0.0 || max_wildcard_import_similarity > 0.0 {
for declaration in scored_declarations_for_identifier.iter_mut() {
if max_import_similarity > 0.0 {
declaration.components.max_import_similarity = max_import_similarity;
declaration.components.normalized_import_similarity =
declaration.components.import_similarity / max_import_similarity;
}
if max_wildcard_import_similarity > 0.0 {
declaration.components.normalized_wildcard_import_similarity =
declaration.components.wildcard_import_similarity
/ max_wildcard_import_similarity;
}
}
let mut max_import_similarity = 0.0;
let mut max_wildcard_import_similarity = 0.0;
let mut scored_declarations_for_identifier = Vec::with_capacity(checked_declarations.len());
for checked_declaration in checked_declarations {
let same_file_declaration_count =
index.file_declaration_count(checked_declaration.declaration);
let declaration = score_declaration(
&identifier,
&references,
checked_declaration,
same_file_declaration_count,
declaration_count,
&excerpt_occurrences,
&adjacent_occurrences,
&import_occurrences,
&wildcard_import_occurrences,
cursor_point,
current_buffer,
);
if declaration.components.import_similarity > max_import_similarity {
max_import_similarity = declaration.components.import_similarity;
}
scored_declarations_for_identifier
})
.collect::<Vec<_>>();
if declaration.components.wildcard_import_similarity > max_wildcard_import_similarity {
max_wildcard_import_similarity = declaration.components.wildcard_import_similarity;
}
declarations.sort_unstable_by_key(|declaration| {
let score_density = declaration
.score_density(DeclarationStyle::Declaration)
.max(declaration.score_density(DeclarationStyle::Signature));
Reverse(OrderedFloat(score_density))
project_entry_id_to_outline_ranges
.entry(declaration.declaration.project_entry_id())
.or_default()
.push(declaration.declaration.item_range());
scored_declarations_for_identifier.push(declaration);
}
if max_import_similarity > 0.0 || max_wildcard_import_similarity > 0.0 {
for declaration in scored_declarations_for_identifier.iter_mut() {
if max_import_similarity > 0.0 {
declaration.components.max_import_similarity = max_import_similarity;
declaration.components.normalized_import_similarity =
declaration.components.import_similarity / max_import_similarity;
}
if max_wildcard_import_similarity > 0.0 {
declaration.components.normalized_wildcard_import_similarity =
declaration.components.wildcard_import_similarity
/ max_wildcard_import_similarity;
}
}
}
scored_declarations.extend(scored_declarations_for_identifier);
}
// TODO: Inform this via import / retrieval scores of outline items
// TODO: Consider using a sweepline
for scored_declaration in scored_declarations.iter_mut() {
let project_entry_id = scored_declaration.declaration.project_entry_id();
let Some(ranges) = project_entry_id_to_outline_ranges.get(&project_entry_id) else {
continue;
};
for range in ranges {
if range.contains_inclusive(&scored_declaration.declaration.item_range()) {
scored_declaration.components.included_by_others += 1
} else if scored_declaration
.declaration
.item_range()
.contains_inclusive(range)
{
scored_declaration.components.includes_others += 1
}
}
}
scored_declarations.sort_unstable_by_key(|declaration| {
Reverse(OrderedFloat(
declaration.score(DeclarationStyle::Declaration),
))
});
declarations
scored_declarations
}
struct CheckedDeclaration<'a> {
@@ -465,6 +486,8 @@ fn score_declaration(
normalized_import_similarity: 0.0,
wildcard_import_similarity,
normalized_wildcard_import_similarity: 0.0,
included_by_others: 0,
includes_others: 0,
};
ScoredDeclaration {

View File

@@ -9,6 +9,7 @@ pub mod text_similarity;
use std::{path::Path, sync::Arc};
use cloud_llm_client::predict_edits_v3;
use collections::HashMap;
use gpui::{App, AppContext as _, Entity, Task};
use language::BufferSnapshot;
@@ -21,6 +22,8 @@ pub use imports::*;
pub use reference::*;
pub use syntax_index::*;
pub use predict_edits_v3::Line;
#[derive(Clone, Debug, PartialEq)]
pub struct EditPredictionContextOptions {
pub use_imports: bool,
@@ -32,7 +35,7 @@ pub struct EditPredictionContextOptions {
pub struct EditPredictionContext {
pub excerpt: EditPredictionExcerpt,
pub excerpt_text: EditPredictionExcerptText,
pub cursor_offset_in_excerpt: usize,
pub cursor_point: Point,
pub declarations: Vec<ScoredDeclaration>,
}
@@ -124,8 +127,6 @@ impl EditPredictionContext {
);
let cursor_offset_in_file = cursor_point.to_offset(buffer);
// TODO fix this to not need saturating_sub
let cursor_offset_in_excerpt = cursor_offset_in_file.saturating_sub(excerpt.range.start);
let declarations = if let Some(index_state) = index_state {
let references = get_references(&excerpt, &excerpt_text, buffer);
@@ -148,7 +149,7 @@ impl EditPredictionContext {
Some(Self {
excerpt,
excerpt_text,
cursor_offset_in_excerpt,
cursor_point,
declarations,
})
}

View File

@@ -4,7 +4,7 @@ use text::{Point, ToOffset as _, ToPoint as _};
use tree_sitter::{Node, TreeCursor};
use util::RangeExt;
use crate::{BufferDeclaration, declaration::DeclarationId, syntax_index::SyntaxIndexState};
use crate::{BufferDeclaration, Line, declaration::DeclarationId, syntax_index::SyntaxIndexState};
// TODO:
//
@@ -35,6 +35,7 @@ pub struct EditPredictionExcerptOptions {
#[derive(Debug, Clone)]
pub struct EditPredictionExcerpt {
pub range: Range<usize>,
pub line_range: Range<Line>,
pub parent_declarations: Vec<(DeclarationId, Range<usize>)>,
pub size: usize,
}
@@ -86,12 +87,19 @@ impl EditPredictionExcerpt {
buffer.len(),
options.max_bytes
);
return Some(EditPredictionExcerpt::new(0..buffer.len(), Vec::new()));
let offset_range = 0..buffer.len();
let line_range = Line(0)..Line(buffer.max_point().row);
return Some(EditPredictionExcerpt::new(
offset_range,
line_range,
Vec::new(),
));
}
let query_offset = query_point.to_offset(buffer);
let query_range = Point::new(query_point.row, 0).to_offset(buffer)
..Point::new(query_point.row + 1, 0).to_offset(buffer);
let query_line_range = query_point.row..query_point.row + 1;
let query_range = Point::new(query_line_range.start, 0).to_offset(buffer)
..Point::new(query_line_range.end, 0).to_offset(buffer);
if query_range.len() >= options.max_bytes {
return None;
}
@@ -107,6 +115,7 @@ impl EditPredictionExcerpt {
let excerpt_selector = ExcerptSelector {
query_offset,
query_range,
query_line_range: Line(query_line_range.start)..Line(query_line_range.end),
parent_declarations: &parent_declarations,
buffer,
options,
@@ -130,7 +139,11 @@ impl EditPredictionExcerpt {
excerpt_selector.select_lines()
}
fn new(range: Range<usize>, parent_declarations: Vec<(DeclarationId, Range<usize>)>) -> Self {
fn new(
range: Range<usize>,
line_range: Range<Line>,
parent_declarations: Vec<(DeclarationId, Range<usize>)>,
) -> Self {
let size = range.len()
+ parent_declarations
.iter()
@@ -140,10 +153,11 @@ impl EditPredictionExcerpt {
range,
parent_declarations,
size,
line_range,
}
}
fn with_expanded_range(&self, new_range: Range<usize>) -> Self {
fn with_expanded_range(&self, new_range: Range<usize>, new_line_range: Range<Line>) -> Self {
if !new_range.contains_inclusive(&self.range) {
// this is an issue because parent_signature_ranges may be incorrect
log::error!("bug: with_expanded_range called with disjoint range");
@@ -155,7 +169,7 @@ impl EditPredictionExcerpt {
}
parent_declarations.push((*declaration_id, range.clone()));
}
Self::new(new_range, parent_declarations)
Self::new(new_range, new_line_range, parent_declarations)
}
fn parent_signatures_size(&self) -> usize {
@@ -166,6 +180,7 @@ impl EditPredictionExcerpt {
struct ExcerptSelector<'a> {
query_offset: usize,
query_range: Range<usize>,
query_line_range: Range<Line>,
parent_declarations: &'a [(DeclarationId, &'a BufferDeclaration)],
buffer: &'a BufferSnapshot,
options: &'a EditPredictionExcerptOptions,
@@ -178,10 +193,13 @@ impl<'a> ExcerptSelector<'a> {
let mut cursor = selected_layer_root.walk();
loop {
let excerpt_range = node_line_start(cursor.node()).to_offset(&self.buffer)
..node_line_end(cursor.node()).to_offset(&self.buffer);
let line_start = node_line_start(cursor.node());
let line_end = node_line_end(cursor.node());
let line_range = Line(line_start.row)..Line(line_end.row);
let excerpt_range =
line_start.to_offset(&self.buffer)..line_end.to_offset(&self.buffer);
if excerpt_range.contains_inclusive(&self.query_range) {
let excerpt = self.make_excerpt(excerpt_range);
let excerpt = self.make_excerpt(excerpt_range, line_range);
if excerpt.size <= self.options.max_bytes {
return Some(self.expand_to_siblings(&mut cursor, excerpt));
}
@@ -272,9 +290,13 @@ impl<'a> ExcerptSelector<'a> {
let mut forward = None;
while !forward_done {
let new_end = node_line_end(forward_cursor.node()).to_offset(&self.buffer);
let new_end_point = node_line_end(forward_cursor.node());
let new_end = new_end_point.to_offset(&self.buffer);
if new_end > excerpt.range.end {
let new_excerpt = excerpt.with_expanded_range(excerpt.range.start..new_end);
let new_excerpt = excerpt.with_expanded_range(
excerpt.range.start..new_end,
excerpt.line_range.start..Line(new_end_point.row),
);
if new_excerpt.size <= self.options.max_bytes {
forward = Some(new_excerpt);
break;
@@ -289,9 +311,13 @@ impl<'a> ExcerptSelector<'a> {
let mut backward = None;
while !backward_done {
let new_start = node_line_start(backward_cursor.node()).to_offset(&self.buffer);
let new_start_point = node_line_start(backward_cursor.node());
let new_start = new_start_point.to_offset(&self.buffer);
if new_start < excerpt.range.start {
let new_excerpt = excerpt.with_expanded_range(new_start..excerpt.range.end);
let new_excerpt = excerpt.with_expanded_range(
new_start..excerpt.range.end,
Line(new_start_point.row)..excerpt.line_range.end,
);
if new_excerpt.size <= self.options.max_bytes {
backward = Some(new_excerpt);
break;
@@ -339,7 +365,7 @@ impl<'a> ExcerptSelector<'a> {
fn select_lines(&self) -> Option<EditPredictionExcerpt> {
// early return if line containing query_offset is already too large
let excerpt = self.make_excerpt(self.query_range.clone());
let excerpt = self.make_excerpt(self.query_range.clone(), self.query_line_range.clone());
if excerpt.size > self.options.max_bytes {
log::debug!(
"excerpt for cursor line is {} bytes, which exceeds the window",
@@ -353,24 +379,24 @@ impl<'a> ExcerptSelector<'a> {
let before_bytes =
(self.options.target_before_cursor_over_total_bytes * bytes_remaining as f32) as usize;
let start_point = {
let start_line = {
let offset = self.query_offset.saturating_sub(before_bytes);
let point = offset.to_point(self.buffer);
Point::new(point.row + 1, 0)
Line(point.row + 1)
};
let start_offset = start_point.to_offset(&self.buffer);
let end_point = {
let start_offset = Point::new(start_line.0, 0).to_offset(&self.buffer);
let end_line = {
let offset = start_offset + bytes_remaining;
let point = offset.to_point(self.buffer);
Point::new(point.row, 0)
Line(point.row)
};
let end_offset = end_point.to_offset(&self.buffer);
let end_offset = Point::new(end_line.0, 0).to_offset(&self.buffer);
// this could be expanded further since recalculated `signature_size` may be smaller, but
// skipping that for now for simplicity
//
// TODO: could also consider checking if lines immediately before / after fit.
let excerpt = self.make_excerpt(start_offset..end_offset);
let excerpt = self.make_excerpt(start_offset..end_offset, start_line..end_line);
if excerpt.size > self.options.max_bytes {
log::error!(
"bug: line-based excerpt selection has size {}, \
@@ -382,14 +408,14 @@ impl<'a> ExcerptSelector<'a> {
return Some(excerpt);
}
fn make_excerpt(&self, range: Range<usize>) -> EditPredictionExcerpt {
fn make_excerpt(&self, range: Range<usize>, line_range: Range<Line>) -> EditPredictionExcerpt {
let parent_declarations = self
.parent_declarations
.iter()
.filter(|(_, declaration)| declaration.item_range.contains_inclusive(&range))
.map(|(id, declaration)| (*id, declaration.signature_range.clone()))
.collect();
EditPredictionExcerpt::new(range, parent_declarations)
EditPredictionExcerpt::new(range, line_range, parent_declarations)
}
/// Returns `true` if the `forward` excerpt is a better choice than the `backward` excerpt.

View File

@@ -328,11 +328,7 @@ impl CompletionsMenu {
.map(|choice| Completion {
replace_range: selection.start.text_anchor..selection.end.text_anchor,
new_text: choice.to_string(),
label: CodeLabel {
text: choice.to_string(),
runs: Default::default(),
filter_range: Default::default(),
},
label: CodeLabel::plain(choice.to_string(), None),
icon_path: None,
documentation: None,
confirm: None,
@@ -1518,6 +1514,7 @@ impl CodeActionsMenu {
this.child(
h_flex()
.overflow_hidden()
.when(is_quick_action_bar, |this| this.text_ui(cx))
.child(task.resolved_label.replace("\n", ""))
.when(selected, |this| {
this.text_color(colors.text_accent)
@@ -1528,6 +1525,7 @@ impl CodeActionsMenu {
this.child(
h_flex()
.overflow_hidden()
.when(is_quick_action_bar, |this| this.text_ui(cx))
.child("debug: ")
.child(scenario.label.clone())
.when(selected, |this| {

View File

@@ -226,6 +226,7 @@ pub const SELECTION_HIGHLIGHT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis
pub(crate) const CODE_ACTION_TIMEOUT: Duration = Duration::from_secs(5);
pub(crate) const FORMAT_TIMEOUT: Duration = Duration::from_secs(5);
pub(crate) const SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT: Duration = Duration::from_secs(1);
pub const FETCH_COLORS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(150);
pub(crate) const EDIT_PREDICTION_KEY_CONTEXT: &str = "edit_prediction";
pub(crate) const EDIT_PREDICTION_CONFLICT_KEY_CONTEXT: &str = "edit_prediction_conflict";
@@ -1189,6 +1190,7 @@ pub struct Editor {
inline_value_cache: InlineValueCache,
selection_drag_state: SelectionDragState,
colors: Option<LspColorData>,
refresh_colors_task: Task<()>,
folding_newlines: Task<()>,
pub lookup_key: Option<Box<dyn Any + Send + Sync>>,
}
@@ -2244,6 +2246,7 @@ impl Editor {
tasks_update_task: None,
pull_diagnostics_task: Task::ready(()),
colors: None,
refresh_colors_task: Task::ready(()),
next_color_inlay_id: 0,
linked_edit_ranges: Default::default(),
in_project_search: false,
@@ -3511,26 +3514,46 @@ impl Editor {
) {
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let tail = self.selections.newest::<usize>(cx).tail();
let click_count = click_count.max(match self.selections.select_mode() {
SelectMode::Character => 1,
SelectMode::Word(_) => 2,
SelectMode::Line(_) => 3,
SelectMode::All => 4,
});
self.begin_selection(position, false, click_count, window, cx);
let position = position.to_offset(&display_map, Bias::Left);
let tail_anchor = display_map.buffer_snapshot().anchor_before(tail);
let current_selection = match self.selections.select_mode() {
SelectMode::Character | SelectMode::All => tail_anchor..tail_anchor,
SelectMode::Word(range) | SelectMode::Line(range) => range.clone(),
};
let mut pending_selection = self
.selections
.pending_anchor()
.cloned()
.expect("extend_selection not called with pending selection");
if position >= tail {
pending_selection.start = tail_anchor;
} else {
pending_selection.end = tail_anchor;
if pending_selection
.start
.cmp(&current_selection.start, display_map.buffer_snapshot())
== Ordering::Greater
{
pending_selection.start = current_selection.start;
}
if pending_selection
.end
.cmp(&current_selection.end, display_map.buffer_snapshot())
== Ordering::Less
{
pending_selection.end = current_selection.end;
pending_selection.reversed = true;
}
let mut pending_mode = self.selections.pending_mode().unwrap();
match &mut pending_mode {
SelectMode::Word(range) | SelectMode::Line(range) => *range = tail_anchor..tail_anchor,
SelectMode::Word(range) | SelectMode::Line(range) => *range = current_selection,
_ => {}
}
@@ -3541,7 +3564,8 @@ impl Editor {
};
self.change_selections(effects, window, cx, |s| {
s.set_pending(pending_selection.clone(), pending_mode)
s.set_pending(pending_selection.clone(), pending_mode);
s.set_is_extending(true);
});
}
@@ -3810,11 +3834,16 @@ impl Editor {
fn end_selection(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.columnar_selection_state.take();
if self.selections.pending_anchor().is_some() {
if let Some(pending_mode) = self.selections.pending_mode() {
let selections = self.selections.all::<usize>(cx);
self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select(selections);
s.clear_pending();
if s.is_extending() {
s.set_is_extending(false);
} else {
s.set_select_mode(pending_mode);
}
});
}
}
@@ -5214,15 +5243,7 @@ impl Editor {
if enabled {
(InvalidationStrategy::RefreshRequested, None)
} else {
self.splice_inlays(
&self
.visible_inlay_hints(cx)
.iter()
.map(|inlay| inlay.id)
.collect::<Vec<InlayId>>(),
Vec::new(),
cx,
);
self.clear_inlay_hints(cx);
return;
}
}
@@ -5234,15 +5255,7 @@ impl Editor {
if enabled {
(InvalidationStrategy::RefreshRequested, None)
} else {
self.splice_inlays(
&self
.visible_inlay_hints(cx)
.iter()
.map(|inlay| inlay.id)
.collect::<Vec<InlayId>>(),
Vec::new(),
cx,
);
self.clear_inlay_hints(cx);
return;
}
} else {
@@ -5253,7 +5266,7 @@ impl Editor {
match self.inlay_hint_cache.update_settings(
&self.buffer,
new_settings,
self.visible_inlay_hints(cx),
self.visible_inlay_hints(cx).cloned().collect::<Vec<_>>(),
cx,
) {
ControlFlow::Break(Some(InlaySplice {
@@ -5303,13 +5316,25 @@ impl Editor {
}
}
fn visible_inlay_hints(&self, cx: &Context<Editor>) -> Vec<Inlay> {
pub fn clear_inlay_hints(&self, cx: &mut Context<Editor>) {
self.splice_inlays(
&self
.visible_inlay_hints(cx)
.map(|inlay| inlay.id)
.collect::<Vec<_>>(),
Vec::new(),
cx,
);
}
fn visible_inlay_hints<'a>(
&'a self,
cx: &'a Context<Editor>,
) -> impl Iterator<Item = &'a Inlay> {
self.display_map
.read(cx)
.current_inlays()
.filter(move |inlay| matches!(inlay.id, InlayId::Hint(_)))
.cloned()
.collect()
}
pub fn visible_excerpts(
@@ -6976,6 +7001,7 @@ impl Editor {
) else {
return Vec::default();
};
let query_range = query_range.to_anchors(&multi_buffer_snapshot);
for (buffer_snapshot, search_range, excerpt_id) in buffer_ranges {
match_ranges.extend(
regex
@@ -10470,29 +10496,33 @@ impl Editor {
let buffer = display_map.buffer_snapshot();
let mut edit_start = ToOffset::to_offset(&Point::new(rows.start.0, 0), buffer);
let edit_end = if buffer.max_point().row >= rows.end.0 {
let (edit_end, target_row) = if buffer.max_point().row >= rows.end.0 {
// If there's a line after the range, delete the \n from the end of the row range
ToOffset::to_offset(&Point::new(rows.end.0, 0), buffer)
(
ToOffset::to_offset(&Point::new(rows.end.0, 0), buffer),
rows.end,
)
} else {
// If there isn't a line after the range, delete the \n from the line before the
// start of the row range
edit_start = edit_start.saturating_sub(1);
buffer.len()
(buffer.len(), rows.start.previous_row())
};
let (cursor, goal) = movement::down_by_rows(
&display_map,
let text_layout_details = self.text_layout_details(window);
let x = display_map.x_for_display_point(
selection.head().to_display_point(&display_map),
rows.len() as u32,
selection.goal,
false,
&self.text_layout_details(window),
&text_layout_details,
);
let row = Point::new(target_row.0, 0)
.to_display_point(&display_map)
.row();
let column = display_map.display_column_for_x(row, x, &text_layout_details);
new_cursors.push((
selection.id,
buffer.anchor_after(cursor.to_point(&display_map)),
goal,
buffer.anchor_after(DisplayPoint::new(row, column).to_point(&display_map)),
SelectionGoal::None,
));
edit_ranges.push(edit_start..edit_end);
}
@@ -14419,6 +14449,10 @@ impl Editor {
let last_selection = selections.iter().max_by_key(|s| s.id).unwrap();
let mut next_selected_range = None;
// Collect and sort selection ranges for efficient overlap checking
let mut selection_ranges: Vec<_> = selections.iter().map(|s| s.range()).collect();
selection_ranges.sort_by_key(|r| r.start);
let bytes_after_last_selection =
buffer.bytes_in_range(last_selection.end..buffer.len());
let bytes_before_first_selection = buffer.bytes_in_range(0..first_selection.start);
@@ -14440,11 +14474,20 @@ impl Editor {
|| (!buffer.is_inside_word(offset_range.start, None)
&& !buffer.is_inside_word(offset_range.end, None))
{
// TODO: This is n^2, because we might check all the selections
if !selections
.iter()
.any(|selection| selection.range().overlaps(&offset_range))
{
// Use binary search to check for overlap (O(log n))
let overlaps = selection_ranges
.binary_search_by(|range| {
if range.end <= offset_range.start {
std::cmp::Ordering::Less
} else if range.start >= offset_range.end {
std::cmp::Ordering::Greater
} else {
std::cmp::Ordering::Equal
}
})
.is_ok();
if !overlaps {
next_selected_range = Some(offset_range);
break;
}
@@ -23034,11 +23077,7 @@ fn snippet_completions(
}),
lsp_defaults: None,
},
label: CodeLabel {
text: matching_prefix.clone(),
runs: Vec::new(),
filter_range: 0..matching_prefix.len(),
},
label: CodeLabel::plain(matching_prefix.clone(), None),
icon_path: None,
documentation: Some(CompletionDocumentation::SingleLineAndMultiLinePlainText {
single_line: snippet.name.clone().into(),
@@ -24645,7 +24684,7 @@ impl Render for MissingEditPredictionKeybindingTooltip {
.items_end()
.w_full()
.child(Button::new("open-keymap", "Assign Keybinding").size(ButtonSize::Compact).on_click(|_ev, window, cx| {
window.dispatch_action(zed_actions::OpenKeymap.boxed_clone(), cx)
window.dispatch_action(zed_actions::OpenKeymapFile.boxed_clone(), cx)
}))
.child(Button::new("see-docs", "See Docs").size(ButtonSize::Compact).on_click(|_ev, _window, cx| {
cx.open_url("https://zed.dev/docs/completions#edit-predictions-missing-keybinding");

View File

@@ -27,7 +27,6 @@ use language::{
LanguageConfigOverride, LanguageMatcher, LanguageName, Override, Point,
language_settings::{
CompletionSettingsContent, FormatterList, LanguageSettingsContent, LspInsertMode,
SelectedFormatter,
},
tree_sitter_python,
};
@@ -619,6 +618,93 @@ fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
});
}
#[gpui::test]
fn test_extending_selection(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let editor = cx.add_window(|window, cx| {
let buffer = MultiBuffer::build_simple("aaa bbb ccc ddd eee", cx);
build_editor(buffer, window, cx)
});
_ = editor.update(cx, |editor, window, cx| {
editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), false, 1, window, cx);
editor.end_selection(window, cx);
assert_eq!(
editor.selections.display_ranges(cx),
[DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)]
);
editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
editor.end_selection(window, cx);
assert_eq!(
editor.selections.display_ranges(cx),
[DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 10)]
);
editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
editor.end_selection(window, cx);
editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 2, window, cx);
assert_eq!(
editor.selections.display_ranges(cx),
[DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 11)]
);
editor.update_selection(
DisplayPoint::new(DisplayRow(0), 1),
0,
gpui::Point::<f32>::default(),
window,
cx,
);
editor.end_selection(window, cx);
assert_eq!(
editor.selections.display_ranges(cx),
[DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 0)]
);
editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), true, 1, window, cx);
editor.end_selection(window, cx);
editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), true, 2, window, cx);
editor.end_selection(window, cx);
assert_eq!(
editor.selections.display_ranges(cx),
[DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 7)]
);
editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
assert_eq!(
editor.selections.display_ranges(cx),
[DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 11)]
);
editor.update_selection(
DisplayPoint::new(DisplayRow(0), 6),
0,
gpui::Point::<f32>::default(),
window,
cx,
);
assert_eq!(
editor.selections.display_ranges(cx),
[DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 7)]
);
editor.update_selection(
DisplayPoint::new(DisplayRow(0), 1),
0,
gpui::Point::<f32>::default(),
window,
cx,
);
editor.end_selection(window, cx);
assert_eq!(
editor.selections.display_ranges(cx),
[DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 0)]
);
});
}
#[gpui::test]
fn test_clone(cx: &mut TestAppContext) {
init_test(cx, |_| {});
@@ -4300,8 +4386,8 @@ fn test_delete_line(cx: &mut TestAppContext) {
assert_eq!(
editor.selections.display_ranges(cx),
vec![
DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
]
);
});
@@ -4323,6 +4409,24 @@ fn test_delete_line(cx: &mut TestAppContext) {
vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
);
});
let editor = cx.add_window(|window, cx| {
let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n\njkl\nmno", cx);
build_editor(buffer, window, cx)
});
_ = editor.update(cx, |editor, window, cx| {
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_display_ranges([
DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(2), 1)
])
});
editor.delete_line(&DeleteLine, window, cx);
assert_eq!(editor.display_text(cx), "\njkl\nmno");
assert_eq!(
editor.selections.display_ranges(cx),
vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
);
});
}
#[gpui::test]
@@ -11803,8 +11907,8 @@ async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppC
#[gpui::test]
async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
init_test(cx, |settings| {
settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
Formatter::LanguageServer { name: None },
settings.defaults.formatter = Some(FormatterList::Single(Formatter::LanguageServer(
settings::LanguageServerFormatterSpecifier::Current,
)))
});
@@ -11929,11 +12033,11 @@ async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
async fn test_multiple_formatters(cx: &mut TestAppContext) {
init_test(cx, |settings| {
settings.defaults.remove_trailing_whitespace_on_save = Some(true);
settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
Formatter::LanguageServer { name: None },
settings.defaults.formatter = Some(FormatterList::Vec(vec![
Formatter::LanguageServer(settings::LanguageServerFormatterSpecifier::Current),
Formatter::CodeAction("code-action-1".into()),
Formatter::CodeAction("code-action-2".into()),
])))
]))
});
let fs = FakeFs::new(cx.executor());
@@ -12188,9 +12292,9 @@ async fn test_multiple_formatters(cx: &mut TestAppContext) {
#[gpui::test]
async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
init_test(cx, |settings| {
settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
Formatter::LanguageServer { name: None },
])))
settings.defaults.formatter = Some(FormatterList::Vec(vec![Formatter::LanguageServer(
settings::LanguageServerFormatterSpecifier::Current,
)]))
});
let fs = FakeFs::new(cx.executor());
@@ -12393,7 +12497,7 @@ async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
#[gpui::test]
async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
init_test(cx, |settings| {
settings.defaults.formatter = Some(SelectedFormatter::Auto)
settings.defaults.formatter = Some(FormatterList::default())
});
let mut cx = EditorLspTestContext::new_rust(
@@ -14774,12 +14878,7 @@ async fn test_multiline_completion(cx: &mut TestAppContext) {
} else {
item.label.clone()
};
let len = text.len();
Some(language::CodeLabel {
text,
runs: Vec::new(),
filter_range: 0..len,
})
Some(language::CodeLabel::plain(text, None))
})),
..FakeLspAdapter::default()
},
@@ -16518,7 +16617,7 @@ async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
leader.update(cx, |leader, cx| {
leader.buffer.update(cx, |multibuffer, cx| {
multibuffer.set_excerpts_for_path(
PathKey::namespaced(1, rel_path("b.txt").into_arc()),
PathKey::with_sort_prefix(1, rel_path("b.txt").into_arc()),
buffer_1.clone(),
vec![
Point::row_range(0..3),
@@ -16529,7 +16628,7 @@ async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
cx,
);
multibuffer.set_excerpts_for_path(
PathKey::namespaced(1, rel_path("a.txt").into_arc()),
PathKey::with_sort_prefix(1, rel_path("a.txt").into_arc()),
buffer_2.clone(),
vec![Point::row_range(0..6), Point::row_range(8..12)],
0,
@@ -18082,9 +18181,7 @@ fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
#[gpui::test]
async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
init_test(cx, |settings| {
settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
Formatter::Prettier,
)))
settings.defaults.formatter = Some(FormatterList::Single(Formatter::Prettier))
});
let fs = FakeFs::new(cx.executor());
@@ -18151,7 +18248,7 @@ async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
);
update_test_language_settings(cx, |settings| {
settings.defaults.formatter = Some(SelectedFormatter::Auto)
settings.defaults.formatter = Some(FormatterList::default())
});
let format = editor.update_in(cx, |editor, window, cx| {
editor.perform_format(
@@ -21032,7 +21129,7 @@ async fn test_display_diff_hunks(cx: &mut TestAppContext) {
for buffer in &buffers {
let snapshot = buffer.read(cx).snapshot();
multibuffer.set_excerpts_for_path(
PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
PathKey::with_sort_prefix(0, buffer.read(cx).file().unwrap().path().clone()),
buffer.clone(),
vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
2,
@@ -25706,7 +25803,7 @@ async fn test_document_colors(cx: &mut TestAppContext) {
.set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
panic!("Should not be called");
});
cx.executor().advance_clock(Duration::from_millis(100));
cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
color_request_handle.next().await.unwrap();
cx.run_until_parked();
assert_eq!(
@@ -25790,9 +25887,9 @@ async fn test_document_colors(cx: &mut TestAppContext) {
color_request_handle.next().await.unwrap();
cx.run_until_parked();
assert_eq!(
3,
2,
requests_made.load(atomic::Ordering::Acquire),
"Should query for colors once per save and once per formatting after save"
"Should query for colors once per save (deduplicated) and once per formatting after save"
);
drop(editor);
@@ -25813,7 +25910,7 @@ async fn test_document_colors(cx: &mut TestAppContext) {
.unwrap();
close.await.unwrap();
assert_eq!(
3,
2,
requests_made.load(atomic::Ordering::Acquire),
"After saving and closing all editors, no extra requests should be made"
);
@@ -25833,7 +25930,7 @@ async fn test_document_colors(cx: &mut TestAppContext) {
})
})
.unwrap();
cx.executor().advance_clock(Duration::from_millis(100));
cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
cx.run_until_parked();
let editor = workspace
.update(cx, |workspace, _, cx| {
@@ -25844,9 +25941,9 @@ async fn test_document_colors(cx: &mut TestAppContext) {
.expect("Should be an editor")
})
.unwrap();
color_request_handle.next().await.unwrap();
assert_eq!(
3,
2,
requests_made.load(atomic::Ordering::Acquire),
"Cache should be reused on buffer close and reopen"
);
@@ -25887,10 +25984,11 @@ async fn test_document_colors(cx: &mut TestAppContext) {
});
save.await.unwrap();
cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
empty_color_request_handle.next().await.unwrap();
cx.run_until_parked();
assert_eq!(
4,
3,
requests_made.load(atomic::Ordering::Acquire),
"Should query for colors once per save only, as formatting was not requested"
);

View File

@@ -681,6 +681,7 @@ impl EditorElement {
.drag_and_drop_selection
.enabled
&& click_count == 1
&& !modifiers.shift
{
let newest_anchor = editor.selections.newest_anchor();
let snapshot = editor.snapshot(window, cx);
@@ -739,6 +740,35 @@ impl EditorElement {
}
}
if !is_singleton {
let display_row = (ScrollPixelOffset::from(
(event.position - gutter_hitbox.bounds.origin).y / position_map.line_height,
) + position_map.scroll_position.y) as u32;
let multi_buffer_row = position_map
.snapshot
.display_point_to_point(DisplayPoint::new(DisplayRow(display_row), 0), Bias::Right)
.row;
if line_numbers
.get(&MultiBufferRow(multi_buffer_row))
.and_then(|line_number| line_number.hitbox.as_ref())
.is_some_and(|hitbox| hitbox.contains(&event.position))
{
let line_offset_from_top = display_row - position_map.scroll_position.y as u32;
editor.open_excerpts_common(
Some(JumpData::MultiBufferRow {
row: MultiBufferRow(multi_buffer_row),
line_offset_from_top,
}),
modifiers.alt,
window,
cx,
);
cx.stop_propagation();
return;
}
}
let position = point_for_position.previous_valid;
if let Some(mode) = Editor::columnar_selection_mode(&modifiers, cx) {
editor.select(
@@ -776,34 +806,6 @@ impl EditorElement {
);
}
cx.stop_propagation();
if !is_singleton {
let display_row = (ScrollPixelOffset::from(
(event.position - gutter_hitbox.bounds.origin).y / position_map.line_height,
) + position_map.scroll_position.y) as u32;
let multi_buffer_row = position_map
.snapshot
.display_point_to_point(DisplayPoint::new(DisplayRow(display_row), 0), Bias::Right)
.row;
if line_numbers
.get(&MultiBufferRow(multi_buffer_row))
.and_then(|line_number| line_number.hitbox.as_ref())
.is_some_and(|hitbox| hitbox.contains(&event.position))
{
let line_offset_from_top = display_row - position_map.scroll_position.y as u32;
editor.open_excerpts_common(
Some(JumpData::MultiBufferRow {
row: MultiBufferRow(multi_buffer_row),
line_offset_from_top,
}),
modifiers.alt,
window,
cx,
);
cx.stop_propagation();
}
}
}
fn mouse_right_down(

View File

@@ -307,7 +307,6 @@ pub fn update_inlay_link_and_hover_points(
buffer_snapshot.anchor_after(point_for_position.next_valid.to_point(snapshot));
if let Some(hovered_hint) = editor
.visible_inlay_hints(cx)
.into_iter()
.skip_while(|hint| {
hint.position
.cmp(&previous_valid_anchor, &buffer_snapshot)

View File

@@ -1013,7 +1013,7 @@ fn fetch_and_update_hints(
.cloned()
})?;
let visible_hints = editor.update(cx, |editor, cx| editor.visible_inlay_hints(cx))?;
let visible_hints = editor.update(cx, |editor, cx| editor.visible_inlay_hints(cx).cloned().collect::<Vec<_>>())?;
let new_hints = match inlay_hints_fetch_task {
Some(fetch_task) => {
log::debug!(
@@ -3570,7 +3570,6 @@ pub mod tests {
pub fn visible_hint_labels(editor: &Editor, cx: &Context<Editor>) -> Vec<String> {
editor
.visible_inlay_hints(cx)
.into_iter()
.map(|hint| hint.text().to_string())
.collect()
}

View File

@@ -13,8 +13,8 @@ use ui::{App, Context, Window};
use util::post_inc;
use crate::{
DisplayPoint, Editor, EditorSettings, EditorSnapshot, InlayId, InlaySplice, RangeToAnchorExt,
display_map::Inlay, editor_settings::DocumentColorsRenderMode,
DisplayPoint, Editor, EditorSettings, EditorSnapshot, FETCH_COLORS_DEBOUNCE_TIMEOUT, InlayId,
InlaySplice, RangeToAnchorExt, display_map::Inlay, editor_settings::DocumentColorsRenderMode,
};
#[derive(Debug)]
@@ -193,7 +193,12 @@ impl Editor {
})
.collect::<Vec<_>>()
});
cx.spawn(async move |editor, cx| {
self.refresh_colors_task = cx.spawn(async move |editor, cx| {
cx.background_executor()
.timer(FETCH_COLORS_DEBOUNCE_TIMEOUT)
.await;
let all_colors = join_all(all_colors_task).await;
if all_colors.is_empty() {
return;
@@ -420,7 +425,6 @@ impl Editor {
}
})
.ok();
})
.detach();
});
}
}

View File

@@ -35,6 +35,8 @@ pub struct SelectionsCollection {
disjoint: Arc<[Selection<Anchor>]>,
/// A pending selection, such as when the mouse is being dragged
pending: Option<PendingSelection>,
select_mode: SelectMode,
is_extending: bool,
}
impl SelectionsCollection {
@@ -55,6 +57,8 @@ impl SelectionsCollection {
},
mode: SelectMode::Character,
}),
select_mode: SelectMode::Character,
is_extending: false,
}
}
@@ -456,6 +460,22 @@ impl SelectionsCollection {
pub fn set_line_mode(&mut self, line_mode: bool) {
self.line_mode = line_mode;
}
pub fn select_mode(&self) -> &SelectMode {
&self.select_mode
}
pub fn set_select_mode(&mut self, select_mode: SelectMode) {
self.select_mode = select_mode;
}
pub fn is_extending(&self) -> bool {
self.is_extending
}
pub fn set_is_extending(&mut self, is_extending: bool) {
self.is_extending = is_extending;
}
}
pub struct MutableSelectionsCollection<'a> {
@@ -590,21 +610,32 @@ impl<'a> MutableSelectionsCollection<'a> {
self.select(selections);
}
pub fn select<T>(&mut self, mut selections: Vec<Selection<T>>)
pub fn select<T>(&mut self, selections: Vec<Selection<T>>)
where
T: ToOffset + ToPoint + Ord + std::marker::Copy + std::fmt::Debug,
T: ToOffset + std::marker::Copy + std::fmt::Debug,
{
let buffer = self.buffer.read(self.cx).snapshot(self.cx);
let mut selections = selections
.into_iter()
.map(|selection| selection.map(|it| it.to_offset(&buffer)))
.map(|mut selection| {
if selection.start > selection.end {
mem::swap(&mut selection.start, &mut selection.end);
selection.reversed = true
}
selection
})
.collect::<Vec<_>>();
selections.sort_unstable_by_key(|s| s.start);
// Merge overlapping selections.
let mut i = 1;
while i < selections.len() {
if selections[i - 1].end >= selections[i].start {
if selections[i].start <= selections[i - 1].end {
let removed = selections.remove(i);
if removed.start < selections[i - 1].start {
selections[i - 1].start = removed.start;
}
if removed.end > selections[i - 1].end {
if selections[i - 1].end < removed.end {
selections[i - 1].end = removed.end;
}
} else {
@@ -948,13 +979,10 @@ impl DerefMut for MutableSelectionsCollection<'_> {
}
}
fn selection_to_anchor_selection<T>(
selection: Selection<T>,
fn selection_to_anchor_selection(
selection: Selection<usize>,
buffer: &MultiBufferSnapshot,
) -> Selection<Anchor>
where
T: ToOffset + Ord,
{
) -> Selection<Anchor> {
let end_bias = if selection.start == selection.end {
Bias::Right
} else {
@@ -992,7 +1020,7 @@ fn resolve_selections_point<'a>(
})
}
// Panics if passed selections are not in order
/// Panics if passed selections are not in order
fn resolve_selections_display<'a>(
selections: impl 'a + IntoIterator<Item = &'a Selection<Anchor>>,
map: &'a DisplaySnapshot,
@@ -1024,7 +1052,7 @@ fn resolve_selections_display<'a>(
coalesce_selections(selections)
}
// Panics if passed selections are not in order
/// Panics if passed selections are not in order
pub(crate) fn resolve_selections<'a, D, I>(
selections: I,
map: &'a DisplaySnapshot,

View File

@@ -1,5 +1,5 @@
use crate::{
AnchorRangeExt, DisplayPoint, Editor, MultiBuffer, RowExt,
AnchorRangeExt, DisplayPoint, Editor, ExcerptId, MultiBuffer, MultiBufferSnapshot, RowExt,
display_map::{HighlightKey, ToDisplayPoint},
};
use buffer_diff::DiffHunkStatusKind;
@@ -24,6 +24,7 @@ use std::{
atomic::{AtomicUsize, Ordering},
},
};
use text::Selection;
use util::{
assert_set_eq,
test::{generate_marked_text, marked_text_ranges},
@@ -388,6 +389,23 @@ impl EditorTestContext {
#[track_caller]
pub fn assert_excerpts_with_selections(&mut self, marked_text: &str) {
let actual_text = self.to_format_multibuffer_as_marked_text();
let fmt_additional_notes = || {
struct Format<'a, T: std::fmt::Display>(&'a str, &'a T);
impl<T: std::fmt::Display> std::fmt::Display for Format<'_, T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"\n\n----- EXPECTED: -----\n\n{}\n\n----- ACTUAL: -----\n\n{}\n\n",
self.0, self.1
)
}
}
Format(marked_text, &actual_text)
};
let expected_excerpts = marked_text
.strip_prefix("[EXCERPT]\n")
.unwrap()
@@ -408,9 +426,10 @@ impl EditorTestContext {
assert!(
excerpts.len() == expected_excerpts.len(),
"should have {} excerpts, got {}",
"should have {} excerpts, got {}{}",
expected_excerpts.len(),
excerpts.len()
excerpts.len(),
fmt_additional_notes(),
);
for (ix, (excerpt_id, snapshot, range)) in excerpts.into_iter().enumerate() {
@@ -424,18 +443,25 @@ impl EditorTestContext {
if !expected_selections.is_empty() {
assert!(
is_selected,
"excerpt {ix} should be selected. got {:?}",
"excerpt {ix} should contain selections. got {:?}{}",
self.editor_state(),
fmt_additional_notes(),
);
} else {
assert!(
!is_selected,
"excerpt {ix} should not be selected, got: {selections:?}",
"excerpt {ix} should not contain selections, got: {selections:?}{}",
fmt_additional_notes(),
);
}
continue;
}
assert!(!is_folded, "excerpt {} should not be folded", ix);
assert!(
!is_folded,
"excerpt {} should not be folded{}",
ix,
fmt_additional_notes()
);
assert_eq!(
multibuffer_snapshot
.text_for_range(Anchor::range_in_buffer(
@@ -444,7 +470,9 @@ impl EditorTestContext {
range.context.clone()
))
.collect::<String>(),
expected_text
expected_text,
"{}",
fmt_additional_notes(),
);
let selections = selections
@@ -460,13 +488,38 @@ impl EditorTestContext {
.collect::<Vec<_>>();
// todo: selections that cross excerpt boundaries..
assert_eq!(
selections, expected_selections,
"excerpt {} has incorrect selections",
selections,
expected_selections,
"excerpt {} has incorrect selections{}",
ix,
fmt_additional_notes()
);
}
}
fn to_format_multibuffer_as_marked_text(&mut self) -> FormatMultiBufferAsMarkedText {
let (multibuffer_snapshot, selections, excerpts) = self.update_editor(|editor, _, cx| {
let multibuffer_snapshot = editor.buffer.read(cx).snapshot(cx);
let selections = editor.selections.disjoint_anchors_arc().to_vec();
let excerpts = multibuffer_snapshot
.excerpts()
.map(|(e_id, snapshot, range)| {
let is_folded = editor.is_buffer_folded(snapshot.remote_id(), cx);
(e_id, snapshot.clone(), range, is_folded)
})
.collect::<Vec<_>>();
(multibuffer_snapshot, selections, excerpts)
});
FormatMultiBufferAsMarkedText {
multibuffer_snapshot,
selections,
excerpts,
}
}
/// Make an assertion about the editor's text and the ranges and directions
/// of its selections using a string containing embedded range markers.
///
@@ -571,6 +624,63 @@ impl EditorTestContext {
}
}
struct FormatMultiBufferAsMarkedText {
multibuffer_snapshot: MultiBufferSnapshot,
selections: Vec<Selection<Anchor>>,
excerpts: Vec<(ExcerptId, BufferSnapshot, ExcerptRange<text::Anchor>, bool)>,
}
impl std::fmt::Display for FormatMultiBufferAsMarkedText {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Self {
multibuffer_snapshot,
selections,
excerpts,
} = self;
for (excerpt_id, snapshot, range, is_folded) in excerpts.into_iter() {
write!(f, "[EXCERPT]\n")?;
if *is_folded {
write!(f, "[FOLDED]\n")?;
}
let mut text = multibuffer_snapshot
.text_for_range(Anchor::range_in_buffer(
*excerpt_id,
snapshot.remote_id(),
range.context.clone(),
))
.collect::<String>();
let selections = selections
.iter()
.filter(|&s| s.head().excerpt_id == *excerpt_id)
.map(|s| {
let head = text::ToOffset::to_offset(&s.head().text_anchor, &snapshot)
- text::ToOffset::to_offset(&range.context.start, &snapshot);
let tail = text::ToOffset::to_offset(&s.head().text_anchor, &snapshot)
- text::ToOffset::to_offset(&range.context.start, &snapshot);
tail..head
})
.rev()
.collect::<Vec<_>>();
for selection in selections {
if selection.is_empty() {
text.insert(selection.start, 'ˇ');
continue;
}
text.insert(selection.end, '»');
text.insert(selection.start, '«');
}
write!(f, "{text}")?;
}
Ok(())
}
}
#[track_caller]
pub fn assert_state_with_diff(
editor: &Entity<Editor>,

View File

@@ -1,4 +1,4 @@
use anyhow::{Context as _, Result, bail};
use anyhow::{Context as _, Result, anyhow, bail};
use collections::{BTreeMap, HashMap};
use fs::Fs;
use language::LanguageName;
@@ -226,8 +226,9 @@ impl ExtensionManifest {
.load(&extension_manifest_path)
.await
.with_context(|| format!("failed to load {extension_name} extension.toml"))?;
toml::from_str(&manifest_content)
.with_context(|| format!("invalid extension.toml for extension {extension_name}"))
toml::from_str(&manifest_content).map_err(|err| {
anyhow!("Invalid extension.toml for extension {extension_name}:\n{err}")
})
}
}
}

View File

@@ -17,9 +17,3 @@ pub struct PanicFeatureFlag;
impl FeatureFlag for PanicFeatureFlag {
const NAME: &'static str = "panic";
}
pub struct CodexAcpFeatureFlag;
impl FeatureFlag for CodexAcpFeatureFlag {
const NAME: &'static str = "codex-acp";
}

View File

@@ -16,14 +16,12 @@ test-support = []
[dependencies]
gpui.workspace = true
menu.workspace = true
system_specs.workspace = true
ui.workspace = true
urlencoding.workspace = true
util.workspace = true
workspace-hack.workspace = true
workspace.workspace = true
zed_actions.workspace = true
workspace-hack.workspace = true
[dev-dependencies]
editor = { workspace = true, features = ["test-support"] }

View File

@@ -2,19 +2,13 @@ use gpui::{App, ClipboardItem, PromptLevel, actions};
use system_specs::{CopySystemSpecsIntoClipboard, SystemSpecs};
use util::ResultExt;
use workspace::Workspace;
use zed_actions::feedback::FileBugReport;
pub mod feedback_modal;
use zed_actions::feedback::{EmailZed, FileBugReport, RequestFeature};
actions!(
zed,
[
/// Opens email client to send feedback to Zed support.
EmailZed,
/// Opens the Zed repository on GitHub.
OpenZedRepo,
/// Opens the feature request form.
RequestFeature,
]
);
@@ -48,11 +42,7 @@ fn email_body(specs: &SystemSpecs) -> String {
}
pub fn init(cx: &mut App) {
cx.observe_new(|workspace: &mut Workspace, window, cx| {
let Some(window) = window else {
return;
};
feedback_modal::FeedbackModal::register(workspace, window, cx);
cx.observe_new(|workspace: &mut Workspace, _, _| {
workspace
.register_action(|_, _: &CopySystemSpecsIntoClipboard, window, cx| {
let specs = SystemSpecs::new(window, cx);

View File

@@ -1,113 +0,0 @@
use gpui::{App, Context, DismissEvent, EventEmitter, FocusHandle, Focusable, Render, Window};
use ui::{IconPosition, prelude::*};
use workspace::{ModalView, Workspace};
use zed_actions::feedback::GiveFeedback;
use crate::{EmailZed, FileBugReport, OpenZedRepo, RequestFeature};
pub struct FeedbackModal {
focus_handle: FocusHandle,
}
impl Focusable for FeedbackModal {
fn focus_handle(&self, _: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl EventEmitter<DismissEvent> for FeedbackModal {}
impl ModalView for FeedbackModal {}
impl FeedbackModal {
pub fn register(workspace: &mut Workspace, _: &mut Window, cx: &mut Context<Workspace>) {
let _handle = cx.entity().downgrade();
workspace.register_action(move |workspace, _: &GiveFeedback, window, cx| {
workspace.toggle_modal(window, cx, move |_, cx| FeedbackModal::new(cx));
});
}
pub fn new(cx: &mut Context<Self>) -> Self {
Self {
focus_handle: cx.focus_handle(),
}
}
fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
cx.emit(DismissEvent)
}
}
impl Render for FeedbackModal {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let open_zed_repo =
cx.listener(|_, _, window, cx| window.dispatch_action(Box::new(OpenZedRepo), cx));
v_flex()
.key_context("GiveFeedback")
.on_action(cx.listener(Self::cancel))
.elevation_3(cx)
.w_96()
.h_auto()
.p_4()
.gap_2()
.child(
h_flex()
.w_full()
.justify_between()
.child(Headline::new("Give Feedback"))
.child(
IconButton::new("close-btn", IconName::Close)
.icon_color(Color::Muted)
.on_click(cx.listener(move |_, _, window, cx| {
cx.spawn_in(window, async move |this, cx| {
this.update(cx, |_, cx| cx.emit(DismissEvent)).ok();
})
.detach();
})),
),
)
.child(Label::new("Thanks for using Zed! To share your experience with us, reach for the channel that's the most appropriate:"))
.child(
Button::new("file-a-bug-report", "File a Bug Report")
.full_width()
.icon(IconName::Debug)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.icon_position(IconPosition::Start)
.on_click(cx.listener(|_, _, window, cx| {
window.dispatch_action(Box::new(FileBugReport), cx);
})),
)
.child(
Button::new("request-a-feature", "Request a Feature")
.full_width()
.icon(IconName::Sparkle)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.icon_position(IconPosition::Start)
.on_click(cx.listener(|_, _, window, cx| {
window.dispatch_action(Box::new(RequestFeature), cx);
})),
)
.child(
Button::new("send-us_an-email", "Send an Email")
.full_width()
.icon(IconName::Envelope)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.icon_position(IconPosition::Start)
.on_click(cx.listener(|_, _, window, cx| {
window.dispatch_action(Box::new(EmailZed), cx);
})),
)
.child(
Button::new("zed_repository", "GitHub Repository")
.full_width()
.icon(IconName::Github)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.icon_position(IconPosition::Start)
.on_click(open_zed_repo),
)
}
}

View File

@@ -755,7 +755,7 @@ impl PickerDelegate for OpenPathDelegate {
.with_default_highlights(
&window.text_style(),
vec![(
delta..delta + label_len,
delta..label_len,
HighlightStyle::color(Color::Conflict.color(cx)),
)],
)
@@ -765,7 +765,7 @@ impl PickerDelegate for OpenPathDelegate {
.with_default_highlights(
&window.text_style(),
vec![(
delta..delta + label_len,
delta..label_len,
HighlightStyle::color(Color::Created.color(cx)),
)],
)

View File

@@ -749,6 +749,7 @@ impl Fs for RealFs {
events
.into_iter()
.map(|event| {
log::trace!("fs path event: {event:?}");
let kind = if event.flags.contains(StreamFlags::ITEM_REMOVED) {
Some(PathEventKind::Removed)
} else if event.flags.contains(StreamFlags::ITEM_CREATED) {
@@ -806,6 +807,7 @@ impl Fs for RealFs {
// Check if path is a symlink and follow the target parent
if let Some(mut target) = self.read_link(path).await.ok() {
log::trace!("watch symlink {path:?} -> {target:?}");
// Check if symlink target is relative path, if so make it absolute
if target.is_relative()
&& let Some(parent) = path.parent()

View File

@@ -46,6 +46,7 @@ impl Drop for FsWatcher {
impl Watcher for FsWatcher {
fn add(&self, path: &std::path::Path) -> anyhow::Result<()> {
log::trace!("watcher add: {path:?}");
let tx = self.tx.clone();
let pending_paths = self.pending_path_events.clone();
@@ -63,11 +64,15 @@ impl Watcher for FsWatcher {
.next_back()
&& path.starts_with(watched_path.as_ref())
{
log::trace!(
"path to watch is covered by existing registration: {path:?}, {watched_path:?}"
);
return Ok(());
}
}
#[cfg(target_os = "linux")]
{
log::trace!("path to watch is already watched: {path:?}");
if self.registrations.lock().contains_key(path) {
return Ok(());
}
@@ -85,6 +90,7 @@ impl Watcher for FsWatcher {
let path = path.clone();
|g| {
g.add(path, mode, move |event: &notify::Event| {
log::trace!("watcher received event: {event:?}");
let kind = match event.kind {
EventKind::Create(_) => Some(PathEventKind::Created),
EventKind::Modify(_) => Some(PathEventKind::Changed),
@@ -126,6 +132,7 @@ impl Watcher for FsWatcher {
}
fn remove(&self, path: &std::path::Path) -> anyhow::Result<()> {
log::trace!("remove watched path: {path:?}");
let Some(registration) = self.registrations.lock().remove(path) else {
return Ok(());
};
@@ -215,6 +222,7 @@ static FS_WATCHER_INSTANCE: OnceLock<anyhow::Result<GlobalWatcher, notify::Error
OnceLock::new();
fn handle_event(event: Result<notify::Event, notify::Error>) {
log::trace!("global handle event: {event:?}");
// Filter out access events, which could lead to a weird bug on Linux after upgrading notify
// https://github.com/zed-industries/zed/actions/runs/14085230504/job/39449448832
let Some(event) = event

View File

@@ -32,6 +32,7 @@ impl MacWatcher {
impl Watcher for MacWatcher {
fn add(&self, path: &Path) -> Result<()> {
log::trace!("mac watcher add: {:?}", path);
let handles = self
.handles
.upgrade()
@@ -44,6 +45,9 @@ impl Watcher for MacWatcher {
.next_back()
&& path.starts_with(watched_path)
{
log::trace!(
"mac watched path starts with existing watched path: {watched_path:?}, {path:?}"
);
return Ok(());
}

View File

@@ -23,6 +23,7 @@ derive_more.workspace = true
git2.workspace = true
gpui.workspace = true
http_client.workspace = true
itertools.workspace = true
log.workspace = true
parking_lot.workspace = true
regex.workspace = true
@@ -36,6 +37,7 @@ text.workspace = true
thiserror.workspace = true
time.workspace = true
url.workspace = true
urlencoding.workspace = true
util.workspace = true
uuid.workspace = true
futures.workspace = true

View File

@@ -5,9 +5,12 @@ use async_trait::async_trait;
use derive_more::{Deref, DerefMut};
use gpui::{App, Global, SharedString};
use http_client::HttpClient;
use itertools::Itertools;
use parking_lot::RwLock;
use url::Url;
use crate::repository::RepoPath;
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct PullRequest {
pub number: u32,
@@ -55,10 +58,21 @@ pub struct BuildCommitPermalinkParams<'a> {
pub struct BuildPermalinkParams<'a> {
pub sha: &'a str,
pub path: &'a str,
/// URL-escaped path using unescaped `/` as the directory separator.
pub path: String,
pub selection: Option<Range<u32>>,
}
impl<'a> BuildPermalinkParams<'a> {
pub fn new(sha: &'a str, path: &RepoPath, selection: Option<Range<u32>>) -> Self {
Self {
sha,
path: path.components().map(urlencoding::encode).join("/"),
selection,
}
}
}
/// A Git hosting provider.
#[async_trait]
pub trait GitHostingProvider {

View File

@@ -30,3 +30,4 @@ workspace-hack.workspace = true
indoc.workspace = true
serde_json.workspace = true
pretty_assertions.workspace = true
git = { workspace = true, features = ["test-support"] }

View File

@@ -126,6 +126,7 @@ impl GitHostingProvider for Bitbucket {
#[cfg(test)]
mod tests {
use git::repository::repo_path;
use pretty_assertions::assert_eq;
use super::*;
@@ -182,11 +183,7 @@ mod tests {
owner: "zed-industries".into(),
repo: "zed".into(),
},
BuildPermalinkParams {
sha: "f00b4r",
path: "main.rs",
selection: None,
},
BuildPermalinkParams::new("f00b4r", &repo_path("main.rs"), None),
);
let expected_url = "https://bitbucket.org/zed-industries/zed/src/f00b4r/main.rs";
@@ -200,11 +197,7 @@ mod tests {
owner: "zed-industries".into(),
repo: "zed".into(),
},
BuildPermalinkParams {
sha: "f00b4r",
path: "main.rs",
selection: Some(6..6),
},
BuildPermalinkParams::new("f00b4r", &repo_path("main.rs"), Some(6..6)),
);
let expected_url = "https://bitbucket.org/zed-industries/zed/src/f00b4r/main.rs#lines-7";
@@ -218,11 +211,7 @@ mod tests {
owner: "zed-industries".into(),
repo: "zed".into(),
},
BuildPermalinkParams {
sha: "f00b4r",
path: "main.rs",
selection: Some(23..47),
},
BuildPermalinkParams::new("f00b4r", &repo_path("main.rs"), Some(23..47)),
);
let expected_url =

View File

@@ -191,6 +191,7 @@ impl GitHostingProvider for Chromium {
#[cfg(test)]
mod tests {
use git::repository::repo_path;
use indoc::indoc;
use pretty_assertions::assert_eq;
@@ -218,11 +219,11 @@ mod tests {
owner: Arc::from(""),
repo: "chromium/src".into(),
},
BuildPermalinkParams {
sha: "fea5080b182fc92e3be0c01c5dece602fe70b588",
path: "ui/base/cursor/cursor.h",
selection: None,
},
BuildPermalinkParams::new(
"fea5080b182fc92e3be0c01c5dece602fe70b588",
&repo_path("ui/base/cursor/cursor.h"),
None,
),
);
let expected_url = "https://chromium.googlesource.com/chromium/src/+/fea5080b182fc92e3be0c01c5dece602fe70b588/ui/base/cursor/cursor.h";
@@ -236,11 +237,11 @@ mod tests {
owner: Arc::from(""),
repo: "chromium/src".into(),
},
BuildPermalinkParams {
sha: "fea5080b182fc92e3be0c01c5dece602fe70b588",
path: "ui/base/cursor/cursor.h",
selection: Some(18..18),
},
BuildPermalinkParams::new(
"fea5080b182fc92e3be0c01c5dece602fe70b588",
&repo_path("ui/base/cursor/cursor.h"),
Some(18..18),
),
);
let expected_url = "https://chromium.googlesource.com/chromium/src/+/fea5080b182fc92e3be0c01c5dece602fe70b588/ui/base/cursor/cursor.h#19";
@@ -254,11 +255,11 @@ mod tests {
owner: Arc::from(""),
repo: "chromium/src".into(),
},
BuildPermalinkParams {
sha: "fea5080b182fc92e3be0c01c5dece602fe70b588",
path: "ui/base/cursor/cursor.h",
selection: Some(18..30),
},
BuildPermalinkParams::new(
"fea5080b182fc92e3be0c01c5dece602fe70b588",
&repo_path("ui/base/cursor/cursor.h"),
Some(18..30),
),
);
let expected_url = "https://chromium.googlesource.com/chromium/src/+/fea5080b182fc92e3be0c01c5dece602fe70b588/ui/base/cursor/cursor.h#19";

View File

@@ -204,6 +204,7 @@ impl GitHostingProvider for Codeberg {
#[cfg(test)]
mod tests {
use git::repository::repo_path;
use pretty_assertions::assert_eq;
use super::*;
@@ -245,11 +246,11 @@ mod tests {
owner: "zed-industries".into(),
repo: "zed".into(),
},
BuildPermalinkParams {
sha: "faa6f979be417239b2e070dbbf6392b909224e0b",
path: "crates/editor/src/git/permalink.rs",
selection: None,
},
BuildPermalinkParams::new(
"faa6f979be417239b2e070dbbf6392b909224e0b",
&repo_path("crates/editor/src/git/permalink.rs"),
None,
),
);
let expected_url = "https://codeberg.org/zed-industries/zed/src/commit/faa6f979be417239b2e070dbbf6392b909224e0b/crates/editor/src/git/permalink.rs";
@@ -263,11 +264,11 @@ mod tests {
owner: "zed-industries".into(),
repo: "zed".into(),
},
BuildPermalinkParams {
sha: "faa6f979be417239b2e070dbbf6392b909224e0b",
path: "crates/editor/src/git/permalink.rs",
selection: Some(6..6),
},
BuildPermalinkParams::new(
"faa6f979be417239b2e070dbbf6392b909224e0b",
&repo_path("crates/editor/src/git/permalink.rs"),
Some(6..6),
),
);
let expected_url = "https://codeberg.org/zed-industries/zed/src/commit/faa6f979be417239b2e070dbbf6392b909224e0b/crates/editor/src/git/permalink.rs#L7";
@@ -281,11 +282,11 @@ mod tests {
owner: "zed-industries".into(),
repo: "zed".into(),
},
BuildPermalinkParams {
sha: "faa6f979be417239b2e070dbbf6392b909224e0b",
path: "crates/editor/src/git/permalink.rs",
selection: Some(23..47),
},
BuildPermalinkParams::new(
"faa6f979be417239b2e070dbbf6392b909224e0b",
&repo_path("crates/editor/src/git/permalink.rs"),
Some(23..47),
),
);
let expected_url = "https://codeberg.org/zed-industries/zed/src/commit/faa6f979be417239b2e070dbbf6392b909224e0b/crates/editor/src/git/permalink.rs#L24-L48";

View File

@@ -84,6 +84,7 @@ impl GitHostingProvider for Gitee {
#[cfg(test)]
mod tests {
use git::repository::repo_path;
use pretty_assertions::assert_eq;
use super::*;
@@ -125,11 +126,11 @@ mod tests {
owner: "zed-industries".into(),
repo: "zed".into(),
},
BuildPermalinkParams {
sha: "e5fe811d7ad0fc26934edd76f891d20bdc3bb194",
path: "crates/editor/src/git/permalink.rs",
selection: None,
},
BuildPermalinkParams::new(
"e5fe811d7ad0fc26934edd76f891d20bdc3bb194",
&repo_path("crates/editor/src/git/permalink.rs"),
None,
),
);
let expected_url = "https://gitee.com/zed-industries/zed/blob/e5fe811d7ad0fc26934edd76f891d20bdc3bb194/crates/editor/src/git/permalink.rs";
@@ -143,11 +144,11 @@ mod tests {
owner: "zed-industries".into(),
repo: "zed".into(),
},
BuildPermalinkParams {
sha: "e5fe811d7ad0fc26934edd76f891d20bdc3bb194",
path: "crates/editor/src/git/permalink.rs",
selection: Some(6..6),
},
BuildPermalinkParams::new(
"e5fe811d7ad0fc26934edd76f891d20bdc3bb194",
&repo_path("crates/editor/src/git/permalink.rs"),
Some(6..6),
),
);
let expected_url = "https://gitee.com/zed-industries/zed/blob/e5fe811d7ad0fc26934edd76f891d20bdc3bb194/crates/editor/src/git/permalink.rs#L7";
@@ -161,11 +162,11 @@ mod tests {
owner: "zed-industries".into(),
repo: "zed".into(),
},
BuildPermalinkParams {
sha: "e5fe811d7ad0fc26934edd76f891d20bdc3bb194",
path: "crates/editor/src/git/permalink.rs",
selection: Some(23..47),
},
BuildPermalinkParams::new(
"e5fe811d7ad0fc26934edd76f891d20bdc3bb194",
&repo_path("crates/editor/src/git/permalink.rs"),
Some(23..47),
),
);
let expected_url = "https://gitee.com/zed-industries/zed/blob/e5fe811d7ad0fc26934edd76f891d20bdc3bb194/crates/editor/src/git/permalink.rs#L24-48";

View File

@@ -259,6 +259,7 @@ impl GitHostingProvider for Github {
#[cfg(test)]
mod tests {
use git::repository::repo_path;
use indoc::indoc;
use pretty_assertions::assert_eq;
@@ -400,11 +401,11 @@ mod tests {
};
let permalink = Github::public_instance().build_permalink(
remote,
BuildPermalinkParams {
sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
path: "crates/editor/src/git/permalink.rs",
selection: None,
},
BuildPermalinkParams::new(
"e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
&repo_path("crates/editor/src/git/permalink.rs"),
None,
),
);
let expected_url = "https://github.com/zed-industries/zed/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs";
@@ -418,11 +419,11 @@ mod tests {
owner: "zed-industries".into(),
repo: "zed".into(),
},
BuildPermalinkParams {
sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
path: "crates/zed/src/main.rs",
selection: None,
},
BuildPermalinkParams::new(
"b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
&repo_path("crates/zed/src/main.rs"),
None,
),
);
let expected_url = "https://github.com/zed-industries/zed/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs";
@@ -436,11 +437,11 @@ mod tests {
owner: "zed-industries".into(),
repo: "zed".into(),
},
BuildPermalinkParams {
sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
path: "crates/editor/src/git/permalink.rs",
selection: Some(6..6),
},
BuildPermalinkParams::new(
"e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
&repo_path("crates/editor/src/git/permalink.rs"),
Some(6..6),
),
);
let expected_url = "https://github.com/zed-industries/zed/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs#L7";
@@ -454,11 +455,11 @@ mod tests {
owner: "zed-industries".into(),
repo: "zed".into(),
},
BuildPermalinkParams {
sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
path: "crates/editor/src/git/permalink.rs",
selection: Some(23..47),
},
BuildPermalinkParams::new(
"e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
&repo_path("crates/editor/src/git/permalink.rs"),
Some(23..47),
),
);
let expected_url = "https://github.com/zed-industries/zed/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs#L24-L48";
@@ -506,4 +507,23 @@ mod tests {
};
assert_eq!(github.extract_pull_request(&remote, message), None);
}
/// Regression test for issue #39875
#[test]
fn test_git_permalink_url_escaping() {
let permalink = Github::public_instance().build_permalink(
ParsedGitRemote {
owner: "zed-industries".into(),
repo: "nonexistent".into(),
},
BuildPermalinkParams::new(
"3ef1539900037dd3601be7149b2b39ed6d0ce3db",
&repo_path("app/blog/[slug]/page.tsx"),
Some(7..7),
),
);
let expected_url = "https://github.com/zed-industries/nonexistent/blob/3ef1539900037dd3601be7149b2b39ed6d0ce3db/app/blog/%5Bslug%5D/page.tsx#L8";
assert_eq!(permalink.to_string(), expected_url.to_string())
}
}

View File

@@ -126,6 +126,7 @@ impl GitHostingProvider for Gitlab {
#[cfg(test)]
mod tests {
use git::repository::repo_path;
use pretty_assertions::assert_eq;
use super::*;
@@ -209,11 +210,11 @@ mod tests {
owner: "zed-industries".into(),
repo: "zed".into(),
},
BuildPermalinkParams {
sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
path: "crates/editor/src/git/permalink.rs",
selection: None,
},
BuildPermalinkParams::new(
"e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
&repo_path("crates/editor/src/git/permalink.rs"),
None,
),
);
let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs";
@@ -227,11 +228,11 @@ mod tests {
owner: "zed-industries".into(),
repo: "zed".into(),
},
BuildPermalinkParams {
sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
path: "crates/editor/src/git/permalink.rs",
selection: Some(6..6),
},
BuildPermalinkParams::new(
"e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
&repo_path("crates/editor/src/git/permalink.rs"),
Some(6..6),
),
);
let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs#L7";
@@ -245,11 +246,11 @@ mod tests {
owner: "zed-industries".into(),
repo: "zed".into(),
},
BuildPermalinkParams {
sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
path: "crates/editor/src/git/permalink.rs",
selection: Some(23..47),
},
BuildPermalinkParams::new(
"e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
&repo_path("crates/editor/src/git/permalink.rs"),
Some(23..47),
),
);
let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs#L24-48";
@@ -266,11 +267,11 @@ mod tests {
owner: "zed-industries".into(),
repo: "zed".into(),
},
BuildPermalinkParams {
sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
path: "crates/editor/src/git/permalink.rs",
selection: None,
},
BuildPermalinkParams::new(
"e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
&repo_path("crates/editor/src/git/permalink.rs"),
None,
),
);
let expected_url = "https://gitlab.some-enterprise.com/zed-industries/zed/-/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs";
@@ -287,11 +288,11 @@ mod tests {
owner: "zed-industries".into(),
repo: "zed".into(),
},
BuildPermalinkParams {
sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
path: "crates/zed/src/main.rs",
selection: None,
},
BuildPermalinkParams::new(
"b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
&repo_path("crates/zed/src/main.rs"),
None,
),
);
let expected_url = "https://gitlab-instance.big-co.com/zed-industries/zed/-/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs";

View File

@@ -89,6 +89,7 @@ impl GitHostingProvider for Sourcehut {
#[cfg(test)]
mod tests {
use git::repository::repo_path;
use pretty_assertions::assert_eq;
use super::*;
@@ -145,11 +146,11 @@ mod tests {
owner: "zed-industries".into(),
repo: "zed".into(),
},
BuildPermalinkParams {
sha: "faa6f979be417239b2e070dbbf6392b909224e0b",
path: "crates/editor/src/git/permalink.rs",
selection: None,
},
BuildPermalinkParams::new(
"faa6f979be417239b2e070dbbf6392b909224e0b",
&repo_path("crates/editor/src/git/permalink.rs"),
None,
),
);
let expected_url = "https://git.sr.ht/~zed-industries/zed/tree/faa6f979be417239b2e070dbbf6392b909224e0b/item/crates/editor/src/git/permalink.rs";
@@ -163,11 +164,11 @@ mod tests {
owner: "zed-industries".into(),
repo: "zed.git".into(),
},
BuildPermalinkParams {
sha: "faa6f979be417239b2e070dbbf6392b909224e0b",
path: "crates/editor/src/git/permalink.rs",
selection: None,
},
BuildPermalinkParams::new(
"faa6f979be417239b2e070dbbf6392b909224e0b",
&repo_path("crates/editor/src/git/permalink.rs"),
None,
),
);
let expected_url = "https://git.sr.ht/~zed-industries/zed.git/tree/faa6f979be417239b2e070dbbf6392b909224e0b/item/crates/editor/src/git/permalink.rs";
@@ -181,11 +182,11 @@ mod tests {
owner: "zed-industries".into(),
repo: "zed".into(),
},
BuildPermalinkParams {
sha: "faa6f979be417239b2e070dbbf6392b909224e0b",
path: "crates/editor/src/git/permalink.rs",
selection: Some(6..6),
},
BuildPermalinkParams::new(
"faa6f979be417239b2e070dbbf6392b909224e0b",
&repo_path("crates/editor/src/git/permalink.rs"),
Some(6..6),
),
);
let expected_url = "https://git.sr.ht/~zed-industries/zed/tree/faa6f979be417239b2e070dbbf6392b909224e0b/item/crates/editor/src/git/permalink.rs#L7";
@@ -199,11 +200,11 @@ mod tests {
owner: "zed-industries".into(),
repo: "zed".into(),
},
BuildPermalinkParams {
sha: "faa6f979be417239b2e070dbbf6392b909224e0b",
path: "crates/editor/src/git/permalink.rs",
selection: Some(23..47),
},
BuildPermalinkParams::new(
"faa6f979be417239b2e070dbbf6392b909224e0b",
&repo_path("crates/editor/src/git/permalink.rs"),
Some(23..47),
),
);
let expected_url = "https://git.sr.ht/~zed-industries/zed/tree/faa6f979be417239b2e070dbbf6392b909224e0b/item/crates/editor/src/git/permalink.rs#L24-48";

View File

@@ -8,8 +8,8 @@ use git::{
repository::CommitSummary,
};
use gpui::{
ClipboardItem, Entity, Hsla, MouseButton, ScrollHandle, Subscription, TextStyle, WeakEntity,
prelude::*,
ClipboardItem, Entity, Hsla, MouseButton, ScrollHandle, Subscription, TextStyle,
TextStyleRefinement, UnderlineStyle, WeakEntity, prelude::*,
};
use markdown::{Markdown, MarkdownElement};
use project::{git_store::Repository, project_settings::ProjectSettings};
@@ -17,7 +17,7 @@ use settings::Settings as _;
use theme::ThemeSettings;
use time::OffsetDateTime;
use time_format::format_local_timestamp;
use ui::{ContextMenu, Divider, IconButtonShape, prelude::*, tooltip_container};
use ui::{ContextMenu, Divider, prelude::*, tooltip_container};
use workspace::Workspace;
const GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED: usize = 20;
@@ -61,16 +61,15 @@ impl BlameRenderer for GitBlameRenderer {
.mr_2()
.child(
h_flex()
.id(("blame", ix))
.w_full()
.gap_2()
.justify_between()
.font_family(style.font().family)
.line_height(style.line_height)
.id(("blame", ix))
.text_color(cx.theme().status().hint)
.gap_2()
.child(
h_flex()
.items_center()
.gap_2()
.child(div().text_color(sha_color).child(short_commit_id))
.children(avatar)
@@ -209,11 +208,21 @@ impl BlameRenderer for GitBlameRenderer {
OffsetDateTime::now_utc(),
time_format::TimestampFormat::MediumAbsolute,
);
let link_color = cx.theme().colors().text_accent;
let markdown_style = {
let mut style = hover_markdown_style(window, cx);
if let Some(code_block) = &style.code_block.text {
style.base_text_style.refine(code_block);
}
style.link.refine(&TextStyleRefinement {
color: Some(link_color),
underline: Some(UnderlineStyle {
color: Some(link_color.opacity(0.4)),
thickness: px(1.0),
..Default::default()
}),
..Default::default()
});
style
};
@@ -250,20 +259,21 @@ impl BlameRenderer for GitBlameRenderer {
};
Some(
tooltip_container(cx, |d, cx| {
d.occlude()
tooltip_container(cx, |this, cx| {
this.occlude()
.on_mouse_move(|_, _, cx| cx.stop_propagation())
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
.child(
v_flex()
.w(gpui::rems(30.))
.gap_4()
.child(
h_flex()
.pb_1p5()
.gap_x_2()
.pb_1()
.gap_2()
.overflow_x_hidden()
.flex_wrap()
.border_b_1()
.border_color(cx.theme().colors().border_variant)
.children(avatar)
.child(author)
.when(!author_email.is_empty(), |this| {
@@ -272,30 +282,29 @@ impl BlameRenderer for GitBlameRenderer {
.text_color(cx.theme().colors().text_muted)
.child(author_email.to_owned()),
)
})
.border_b_1()
.border_color(cx.theme().colors().border_variant),
}),
)
.child(
div()
.id("inline-blame-commit-message")
.child(message)
.track_scroll(&scroll_handle)
.py_1p5()
.max_h(message_max_height)
.overflow_y_scroll()
.track_scroll(&scroll_handle),
.child(message),
)
.child(
h_flex()
.text_color(cx.theme().colors().text_muted)
.w_full()
.justify_between()
.pt_1p5()
.pt_1()
.border_t_1()
.border_color(cx.theme().colors().border_variant)
.child(absolute_timestamp)
.child(
h_flex()
.gap_1p5()
.gap_1()
.when_some(pull_request, |this, pr| {
this.child(
Button::new(
@@ -306,24 +315,24 @@ impl BlameRenderer for GitBlameRenderer {
.icon(IconName::PullRequest)
.icon_color(Color::Muted)
.icon_position(IconPosition::Start)
.style(ButtonStyle::Subtle)
.icon_size(IconSize::Small)
.on_click(move |_, _, cx| {
cx.stop_propagation();
cx.open_url(pr.url.as_str())
}),
)
.child(Divider::vertical())
})
.child(Divider::vertical())
.child(
Button::new(
"commit-sha-button",
short_commit_id.clone(),
)
.style(ButtonStyle::Subtle)
.color(Color::Muted)
.icon(IconName::FileGit)
.icon_color(Color::Muted)
.icon_position(IconPosition::Start)
.icon_size(IconSize::Small)
.on_click(move |_, window, cx| {
CommitView::open(
commit_summary.clone(),
@@ -337,7 +346,6 @@ impl BlameRenderer for GitBlameRenderer {
)
.child(
IconButton::new("copy-sha-button", IconName::Copy)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.on_click(move |_, _, cx| {

View File

@@ -43,8 +43,8 @@ struct CommitMetadataFile {
worktree_id: WorktreeId,
}
const COMMIT_METADATA_NAMESPACE: u64 = 0;
const FILE_NAMESPACE: u64 = 1;
const COMMIT_METADATA_SORT_PREFIX: u64 = 0;
const FILE_NAMESPACE_SORT_PREFIX: u64 = 1;
impl CommitView {
pub fn open(
@@ -145,7 +145,7 @@ impl CommitView {
});
multibuffer.update(cx, |multibuffer, cx| {
multibuffer.set_excerpts_for_path(
PathKey::namespaced(COMMIT_METADATA_NAMESPACE, file.title.clone()),
PathKey::with_sort_prefix(COMMIT_METADATA_SORT_PREFIX, file.title.clone()),
buffer.clone(),
vec![Point::zero()..buffer.read(cx).max_point()],
0,
@@ -193,7 +193,7 @@ impl CommitView {
.collect::<Vec<_>>();
let path = snapshot.file().unwrap().path().clone();
let _is_newly_added = multibuffer.set_excerpts_for_path(
PathKey::namespaced(FILE_NAMESPACE, path),
PathKey::with_sort_prefix(FILE_NAMESPACE_SORT_PREFIX, path),
buffer,
diff_hunk_ranges,
multibuffer_context_lines(cx),

View File

@@ -4973,6 +4973,7 @@ mod tests {
use settings::SettingsStore;
use theme::LoadThemes;
use util::path;
use util::rel_path::rel_path;
use super::*;
@@ -5595,6 +5596,68 @@ mod tests {
});
}
#[gpui::test]
async fn test_open_diff(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.background_executor.clone());
fs.insert_tree(
path!("/project"),
json!({
".git": {},
"tracked": "tracked\n",
"untracked": "\n",
}),
)
.await;
fs.set_head_and_index_for_repo(
path!("/project/.git").as_ref(),
&[("tracked", "old tracked\n".into())],
);
let project = Project::test(fs.clone(), [Path::new(path!("/project"))], cx).await;
let workspace =
cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let panel = workspace.update(cx, GitPanel::new).unwrap();
// Enable the `sort_by_path` setting and wait for entries to be updated,
// as there should no longer be separators between Tracked and Untracked
// files.
cx.update(|_window, cx| {
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings(cx, |settings| {
settings.git_panel.get_or_insert_default().sort_by_path = Some(true);
})
});
});
cx.update_window_entity(&panel, |panel, _, _| {
std::mem::replace(&mut panel.update_visible_entries_task, Task::ready(()))
})
.await;
// Confirm that `Open Diff` still works for the untracked file, updating
// the Project Diff's active path.
panel.update_in(cx, |panel, window, cx| {
panel.selected_entry = Some(1);
panel.open_diff(&Confirm, window, cx);
});
cx.run_until_parked();
let _ = workspace.update(cx, |workspace, _window, cx| {
let active_path = workspace
.item_of_type::<ProjectDiff>(cx)
.expect("ProjectDiff should exist")
.read(cx)
.active_path(cx)
.expect("active_path should exist");
assert_eq!(active_path.path, rel_path("untracked").into_arc());
});
}
fn assert_entry_paths(entries: &[GitListEntry], expected_paths: &[Option<&str>]) {
assert_eq!(entries.len(), expected_paths.len());
for (entry, expected_path) in entries.iter().zip(expected_paths) {

View File

@@ -16,7 +16,7 @@ use editor::{
use futures::StreamExt;
use git::{
Commit, StageAll, StageAndNext, ToggleStaged, UnstageAll, UnstageAndNext,
repository::{Branch, Upstream, UpstreamTracking, UpstreamTrackingStatus},
repository::{Branch, RepoPath, Upstream, UpstreamTracking, UpstreamTrackingStatus},
status::FileStatus,
};
use gpui::{
@@ -27,7 +27,7 @@ use language::{Anchor, Buffer, Capability, OffsetRangeExt};
use multi_buffer::{MultiBuffer, PathKey};
use project::{
Project, ProjectPath,
git_store::{GitStore, GitStoreEvent},
git_store::{GitStore, GitStoreEvent, Repository},
};
use settings::{Settings, SettingsStore};
use std::any::{Any, TypeId};
@@ -73,9 +73,9 @@ struct DiffBuffer {
file_status: FileStatus,
}
const CONFLICT_NAMESPACE: u64 = 1;
const TRACKED_NAMESPACE: u64 = 2;
const NEW_NAMESPACE: u64 = 3;
const CONFLICT_SORT_PREFIX: u64 = 1;
const TRACKED_SORT_PREFIX: u64 = 2;
const NEW_SORT_PREFIX: u64 = 3;
impl ProjectDiff {
pub(crate) fn register(workspace: &mut Workspace, cx: &mut Context<Workspace>) {
@@ -234,16 +234,8 @@ impl ProjectDiff {
return;
};
let repo = git_repo.read(cx);
let namespace = if repo.had_conflict_on_last_merge_head_change(&entry.repo_path) {
CONFLICT_NAMESPACE
} else if entry.status.is_created() {
NEW_NAMESPACE
} else {
TRACKED_NAMESPACE
};
let path_key = PathKey::namespaced(namespace, entry.repo_path.0);
let sort_prefix = sort_prefix(repo, &entry.repo_path, entry.status, cx);
let path_key = PathKey::with_sort_prefix(sort_prefix, entry.repo_path.0);
self.move_to_path(path_key, window, cx)
}
@@ -388,16 +380,8 @@ impl ProjectDiff {
else {
continue;
};
let namespace = if GitPanelSettings::get_global(cx).sort_by_path {
TRACKED_NAMESPACE
} else if repo.had_conflict_on_last_merge_head_change(&entry.repo_path) {
CONFLICT_NAMESPACE
} else if entry.status.is_created() {
NEW_NAMESPACE
} else {
TRACKED_NAMESPACE
};
let path_key = PathKey::namespaced(namespace, entry.repo_path.0.clone());
let sort_prefix = sort_prefix(repo, &entry.repo_path, entry.status, cx);
let path_key = PathKey::with_sort_prefix(sort_prefix, entry.repo_path.0.clone());
previous_paths.remove(&path_key);
let load_buffer = self
@@ -541,6 +525,18 @@ impl ProjectDiff {
}
}
fn sort_prefix(repo: &Repository, repo_path: &RepoPath, status: FileStatus, cx: &App) -> u64 {
if GitPanelSettings::get_global(cx).sort_by_path {
TRACKED_SORT_PREFIX
} else if repo.had_conflict_on_last_merge_head_change(repo_path) {
CONFLICT_SORT_PREFIX
} else if status.is_created() {
NEW_SORT_PREFIX
} else {
TRACKED_SORT_PREFIX
}
}
impl EventEmitter<EditorEvent> for ProjectDiff {}
impl Focusable for ProjectDiff {
@@ -1463,7 +1459,7 @@ mod tests {
let editor = cx.update_window_entity(&diff, |diff, window, cx| {
diff.move_to_path(
PathKey::namespaced(TRACKED_NAMESPACE, rel_path("foo").into_arc()),
PathKey::with_sort_prefix(TRACKED_SORT_PREFIX, rel_path("foo").into_arc()),
window,
cx,
);
@@ -1484,7 +1480,7 @@ mod tests {
let editor = cx.update_window_entity(&diff, |diff, window, cx| {
diff.move_to_path(
PathKey::namespaced(TRACKED_NAMESPACE, rel_path("bar").into_arc()),
PathKey::with_sort_prefix(TRACKED_SORT_PREFIX, rel_path("bar").into_arc()),
window,
cx,
);
@@ -1623,14 +1619,13 @@ mod tests {
project_diff::{self, ProjectDiff},
};
#[cfg_attr(windows, ignore = "currently fails on windows")]
#[gpui::test]
async fn test_go_to_prev_hunk_multibuffer(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/a",
path!("/a"),
json!({
".git": {},
"a.txt": "created\n",
@@ -1641,7 +1636,7 @@ mod tests {
.await;
fs.set_head_and_index_for_repo(
Path::new("/a/.git"),
Path::new(path!("/a/.git")),
&[
("b.txt", "before\n".to_string()),
("c.txt", "unchanged\n".to_string()),
@@ -1649,7 +1644,7 @@ mod tests {
],
);
let project = Project::test(fs, [Path::new("/a")], cx).await;
let project = Project::test(fs, [Path::new(path!("/a"))], cx).await;
let (workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
@@ -1711,7 +1706,6 @@ mod tests {
));
}
#[cfg_attr(windows, ignore = "currently fails on windows")]
#[gpui::test]
async fn test_excerpts_splitting_after_restoring_the_middle_excerpt(cx: &mut TestAppContext) {
init_test(cx);
@@ -1751,7 +1745,7 @@ mod tests {
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/a",
path!("/a"),
json!({
".git": {},
"main.rs": buffer_contents,
@@ -1760,11 +1754,11 @@ mod tests {
.await;
fs.set_head_and_index_for_repo(
Path::new("/a/.git"),
Path::new(path!("/a/.git")),
&[("main.rs", git_contents.to_owned())],
);
let project = Project::test(fs, [Path::new("/a")], cx).await;
let project = Project::test(fs, [Path::new(path!("/a"))], cx).await;
let (workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
@@ -1929,6 +1923,7 @@ mod tests {
cx.run_until_parked();
let editor = diff.read_with(cx, |diff, _| diff.editor.clone());
assert_state_with_diff(
&editor,
cx,

View File

@@ -1,6 +1,6 @@
[package]
name = "gpui"
version = "0.2.0"
version = "0.2.1"
edition.workspace = true
authors = ["Nathan Sobo <nathan@zed.dev>"]
description = "Zed's GPU-accelerated UI framework"

View File

@@ -11,6 +11,8 @@ GPUI is still in active development as we work on the Zed code editor, and is st
gpui = { version = "*" }
```
- [Ownership and data flow](_ownership_and_data_flow)
Everything in GPUI starts with an `Application`. You can create one with `Application::new()`, and kick off your application by passing a callback to `Application::run()`. Inside this callback, you can create a new window with `App::open_window()`, and register your first root view. See [gpui.rs](https://www.gpui.rs/) for a complete example.
### Dependencies

View File

@@ -3,8 +3,8 @@ use std::time::Duration;
use anyhow::Result;
use gpui::{
Animation, AnimationExt as _, App, Application, AssetSource, Bounds, Context, SharedString,
Transformation, Window, WindowBounds, WindowOptions, black, bounce, div, ease_in_out,
percentage, prelude::*, px, rgb, size, svg,
Transformation, Window, WindowBounds, WindowOptions, bounce, div, ease_in_out, percentage,
prelude::*, px, size, svg,
};
struct Assets {}
@@ -37,37 +37,66 @@ struct AnimationExample {}
impl Render for AnimationExample {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div().flex().flex_col().size_full().justify_around().child(
div().flex().flex_row().w_full().justify_around().child(
div()
.flex()
.flex_col()
.size_full()
.bg(gpui::white())
.text_color(gpui::black())
.justify_around()
.child(
div()
.flex()
.bg(rgb(0x2e7d32))
.size(px(300.0))
.justify_center()
.items_center()
.shadow_lg()
.text_xl()
.text_color(black())
.child("hello")
.flex_col()
.size_full()
.justify_around()
.child(
svg()
.size_8()
.path(ARROW_CIRCLE_SVG)
.text_color(black())
.with_animation(
"image_circle",
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(bounce(ease_in_out)),
|svg, delta| {
svg.with_transformation(Transformation::rotate(percentage(
delta,
)))
},
div()
.id("content")
.flex()
.flex_col()
.h(px(150.))
.overflow_y_scroll()
.w_full()
.flex_1()
.justify_center()
.items_center()
.text_xl()
.gap_4()
.child("Hello Animation")
.child(
svg()
.size_20()
.overflow_hidden()
.path(ARROW_CIRCLE_SVG)
.text_color(gpui::black())
.with_animation(
"image_circle",
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(bounce(ease_in_out)),
|svg, delta| {
svg.with_transformation(Transformation::rotate(
percentage(delta),
))
},
),
),
)
.child(
div()
.flex()
.h(px(64.))
.w_full()
.p_2()
.justify_center()
.items_center()
.border_t_1()
.border_color(gpui::black().opacity(0.1))
.bg(gpui::black().opacity(0.05))
.child("Other Panel"),
),
),
)
)
}
}

View File

@@ -1,10 +1,9 @@
use std::{fs, path::PathBuf, time::Duration};
use std::{fs, path::PathBuf};
use anyhow::Result;
use gpui::{
App, Application, AssetSource, Bounds, BoxShadow, ClickEvent, Context, SharedString, Task,
Timer, Window, WindowBounds, WindowOptions, div, hsla, img, point, prelude::*, px, rgb, size,
svg,
Window, WindowBounds, WindowOptions, div, hsla, img, point, prelude::*, px, rgb, size, svg,
};
struct Assets {
@@ -37,6 +36,7 @@ impl AssetSource for Assets {
struct HelloWorld {
_task: Option<Task<()>>,
opacity: f32,
animating: bool,
}
impl HelloWorld {
@@ -44,39 +44,29 @@ impl HelloWorld {
Self {
_task: None,
opacity: 0.5,
animating: false,
}
}
fn change_opacity(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
fn start_animation(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context<Self>) {
self.opacity = 0.0;
self.animating = true;
cx.notify();
self._task = Some(cx.spawn_in(window, async move |view, cx| {
loop {
Timer::after(Duration::from_secs_f32(0.05)).await;
let mut stop = false;
let _ = cx.update(|_, cx| {
view.update(cx, |view, cx| {
if view.opacity >= 1.0 {
stop = true;
return;
}
view.opacity += 0.1;
cx.notify();
})
});
if stop {
break;
}
}
}));
}
}
impl Render for HelloWorld {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
if self.animating {
self.opacity += 0.005;
if self.opacity >= 1.0 {
self.animating = false;
self.opacity = 1.0;
} else {
window.request_animation_frame();
}
}
div()
.flex()
.flex_row()
@@ -96,7 +86,7 @@ impl Render for HelloWorld {
.child(
div()
.id("panel")
.on_click(cx.listener(Self::change_opacity))
.on_click(cx.listener(Self::start_animation))
.absolute()
.top_8()
.left_8()
@@ -150,7 +140,15 @@ impl Render for HelloWorld {
.text_2xl()
.size_8(),
)
.child("🎊✈️🎉🎈🎁🎂")
.child(
div()
.flex()
.children(["🎊", "✈️", "🎉", "🎈", "🎁", "🎂"].map(|emoji| {
div()
.child(emoji.to_string())
.hover(|style| style.opacity(0.5))
})),
)
.child(img("image/black-cat-typing.gif").size_12()),
),
)

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