Compare commits

...

263 Commits

Author SHA1 Message Date
Katie Geer
32b1576718 formatting 2025-10-13 11:59:38 -07:00
Katie Geer
e83508736e Checkpoint - past minimap 2025-10-13 11:58:27 -07:00
Katie Geer
3c2f320f0e Update shortcuts 2025-10-13 11:10:17 -07:00
Katie Geer
68bd7dae8f Made it to E section 2025-10-13 10:47:29 -07:00
Katie Geer
8c94e3e2a4 checkpoint for settings cleanup 2025-10-13 10:33:21 -07:00
Katie Geer
9980ce274e Updating individual settings layout 2025-10-13 10:18:59 -07:00
Katie Geer
93c506e405 Update "Accessing Settings" in Configuring Zed 2025-10-13 09:57:47 -07: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
Dino
3d4f488d46 vim: Update change surrounds to match vim's behavior (#38721)
These changes refactor the whitespace handling logic for Vim's change
surrounds command (`cs`), making its behavior closely match
[tpope/vim-surround](https://github.com/tpope/vim-surround), following
[this
discussion](https://github.com/zed-industries/zed/issues/38169#issuecomment-3304129461).

Zed's current implementation has two main differences when compared to
[tpope/vim-surround](https://github.com/tpope/vim-surround):

- It only considers whether a single space should be added or removed,
instead of all the space that is between the surrounding character and
the content
- It only takes into consideration the new surrounding characters in
order to determine whether to add or remove that space

A review of
[tpope/vim-surround](https://github.com/tpope/vim-surround)'s behavior
reveals these rules for whitespace:

* Quote to Quote
    * Whitespace is never changed
* Quote to Bracket
    * If opening bracket, add one space
    * If closing bracket, do not add space
* Bracket to Bracket
    * If opening to opening, keep only one space
    * If opening to closing, remove all space
    * If closing to opening, add one space
    * If closing to closing, do not change space
* Bracket to Quote
    * If opening, remove all space
    * If closing, preserve all space

Below is a table with examples for each scenario. A new test has also
been added to specifically check the scenarios outlined above,
`vim::surrounds::test::test_change_surrounds_vim`.

| Type              | Before      | Command | After         |
|-------------------|-------------|---------|---------------|
| Quote → Quote     | `'   a   '` | `cs'"`  | `"   a   "`   |
| Quote → Quote     | `"   a   "` | `cs"'`  | `'   a   '`   |
| Quote → Bracket   | `'   a   '` | `cs'{`  | `{    a    }` |
| Quote → Bracket   | `'   a   '` | `cs'}`  | `{   a   }`   |
| Bracket → Bracket | `[   a   ]` | `cs[{`  | `{ a }`       |
| Bracket → Bracket | `[   a   ]` | `cs[}`  | `{a}`         |
| Bracket → Bracket | `[   a   ]` | `cs]{`  | `{    a    }` |
| Bracket → Bracket | `[   a   ]` | `cs]}`  | `{   a   }`   |
| Bracket → Quote   | `[   a   ]` | `cs['`  | `'a'`         |
| Bracket → Quote   | `[   a   ]` | `cs]'`  | `'   a   '`   |

These changes diverge from
[tpope/vim-surround](https://github.com/tpope/vim-surround) when
handling newlines. For example, with the following snippet:

```rust
fn test_surround() {
    if 2 > 1 {
        println!("place cursor here");
    }
};
```

Placing the cursor inside the string and running any combination of
‎`cs{[`, ‎`cs{]`, ‎`cs}[`, or ‎`cs}]` would previously remove newline
characters. With these changes, using commands like ‎`cs}]` will now
preserve newlines.

Related to #38169
Closes #39334

Release Notes:

- Improved Vim’s change surround command to closely match
[tpope/vim-surround](https://github.com/tpope/vim-surround) behavior.

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-10-09 12:18:48 +01:00
Piotr Osiewicz
ba2337ffb9 project search: Reduce hangs on main thread (#39857)
This takes the idea that @RemcoSmitsDev started on in
https://github.com/zed-industries/zed/pull/39354. We did away with
grabbing a snapshot of the display map when buffer coordinates were
sufficient.
Closes #37267

Release Notes:

- Reduced micro-stutters in project search with large multi-buffer
contents.

---------

Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
2025-10-09 13:11:11 +02:00
Merlin04
37d676e2c6 Add support for xonsh shell (#39834)
Closes #39506

Release Notes:

- Fixed environment variable capture when login shell is
[xonsh](https://xon.sh/)

---------

Co-authored-by: Jakub Konka <kubkon@jakubkonka.com>
2025-10-09 12:00:22 +02:00
Mikayla Maki
1bb6752e3e gpui: Fix typo in publish script (#39836)
Release Notes:

- N/A
2025-10-09 05:11:11 +00:00
Mikayla Maki
8c9b42dda8 gpui 0.2.0 (#39835)
Release Notes:

- N/A
2025-10-09 04:58:59 +00:00
Mikayla Maki
15c4aadb57 Add bump gpui script (#39833)
Release Notes:

- N/A
2025-10-09 04:15:37 +00:00
Ben Kunkle
3d200a5466 settings_ui: Improve keyboard nav (#39819)
Closes #ISSUE

From notes:

```markdown
  - [x] Clicking on the disclsoure icon button in the root-level tree view item should steal focus and move it to the root item (not the icon button)
  - [x] [@ben] Allow left/right arrow keys to expand/collapse root tree view items in the nav
    - [x] With this, make enter/space work the same as clicking (activate page, don't expand root items, focus moves to the content and leaves nav — becomes consistent with mouse interaction)
  - [x] Smart cmd-shift-e: toggling focus should take you to the selected item
  - [x] [@ben] pageup + pagedown in nav -> jump between root items
  - [x] [@ben] home + end buttons should work
    - in nav:
      - home always goes to first section header
      - end always goes to last _visible_ item (does not expand)
```

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-10-08 23:22:02 -04:00
Matthijs Kok
e077b63915 settings_ui: Correct "File Icons" description (#39805)
Align with
cd656485c8/crates/settings/src/settings_content/workspace.rs (L490)

By the way, LOVE the settings UI! <3 Great job so far :)


Release Notes:

- N/A
2025-10-08 20:47:55 -03:00
John Tur
ef839cc207 Improve importing font-family settings from VS Code (#39736)
Closes https://github.com/zed-industries/zed/issues/39259

- Fixes import of `editor.fontFamily` (we were looking for the wrong
key)
- Adds basic support for the CSS font-family syntax used by VS Code,
including font fallback

Release Notes:

- N/A
2025-10-08 19:19:48 -04:00
Michael Sloan
3d0312f4c7 zeta2 inspector: Sort by scores and add score components tooltip (#39821)
Release Notes:

- N/A

Co-authored-by: Agus <agus@zed.dev>
2025-10-08 23:14:40 +00:00
Tom Planche
c1e3958c26 editor: Fix duplicate and copy line newlines (#39610)
Closes #34797 and its child #39508.


![zed-#34797-#39508](https://github.com/user-attachments/assets/48a0fe28-8b8a-480d-bffc-6abc7ff310ff)

Release Notes:

- Fixed `editor::DuplicateLineUp` duplicating the last line onto itself
when the line doesn't end with a newline (#39508)
- Fixed line copy not including a newline at end of buffer, causing
paste to occur on the same line (#34797)
2025-10-08 22:56:25 +00:00
Andrew Farkas
ba937d16e7 Onboarding refactor (#39724)
<img width="1648" height="976" alt="Screenshot 2025-10-07 at 6 57 20 PM"
src="https://github.com/user-attachments/assets/ae7289c0-8820-4fdf-ae28-84fb6bd64942"
/>

Fixes #39347

Release Notes:

- Improved onboarding UI by collapsing it to a single page

---------

Co-authored-by: dino <dinojoaocosta@gmail.com>
Co-authored-by: Lukas Wirth <me@lukaswirth.dev>
Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
Co-authored-by: Anthony Eid <hello@anthonyeid.me>
Co-authored-by: Mikayla Maki <mikayla@zed.dev>
2025-10-08 22:47:25 +00:00
Kirill Bulatov
4dbd186485 Do not deselect in terminal on copy by default (#39814)
Release Notes:

- Flips `terminal.keep_selection_on_copy` default to `true`
2025-10-08 18:01:44 -04:00
Cole Miller
88887fd292 debugger: Add support for remote browser debugging (#39248)
This PR adds support for browser debugging in SSH and WSL projects. We
use the vscode-js-debug-companion extension, repackaged as a standalone
CLI (https://github.com/zed-industries/js-debug-companion-cli).

Closes #38878

Release Notes:

- debugger: Browser debugging is now supported in SSH and WSL projects.

---------

Co-authored-by: Nia <nia@zed.dev>
2025-10-08 21:57:57 +00:00
ozer
31e75b2235 git_ui: Add repository search and alphabetical sorting (#39351)
Closes #38778

Release Notes:

- Added: Search functionality to repository selector
- Improved: Repositories now display in alphabetical order
2025-10-08 17:51:20 -04:00
robert7k
681c19899f Allow adding files to .gitignore (#38089)
This feature allows users to add a new, untracked file to `.gitignore`
by using the context menu in the git panel.

<img width="300" alt="Demo screen shot"
src="https://github.com/user-attachments/assets/3f2402fb-9337-42f8-939f-dac12ca09518"
/>

Release Notes:

- Added feature to add a new file to `.gitignore`
2025-10-08 17:49:06 -04:00
Jakub Konka
439add3d23 terminal: Clear shell after activating (#39798)
Two tweaks were required to ensure we correctly clear the shell after
running an activate script(s):
1. PowerShell upon receiving `\r\n` input, will enter the continuation
mode (>>). To avoid this, we send an "enter" key press instead `\x0d`.
2. In order to clear the terminal _after_ issuing all activation
commands, we need to take into account the asynchronous nature of the
activation process:
   - We write the command to run the script to PTY
- We send "enter" (It is now being processed by the shell) At this point
we need to wait for the shell to finish executing before we clear the
terminal. Otherwise we will create a race where we might clear the
terminal _before_ the shell finished executing the activation script(s).
   - Write `clear`/`cls` command to PTY
- Send "enter" This way we guarantee that we clear the terminal _after_
all scripts were executed.

Closes #38474 

Release Notes:

- N/A
2025-10-08 23:28:11 +02:00
Lev Zakharov
81b98cdd4d go: Add ability to run testable examples (#39390)
See related discussion #39381.

<img width="724" height="488"
src="https://github.com/user-attachments/assets/4a69e13e-783f-45d7-99f4-e23c0415a781"
/>

Release Notes:

- Added ability to run Go Testable Examples
2025-10-08 22:55:26 +02:00
Agus Zubiaga
ca89a40df2 zeta2 inspector: Plan prompt locally (#39811)
Plans and displays the prompt locally before the response arrives.
Helpful while debugging prompt planning.

Release Notes:

- N/A

---------

Co-authored-by: Michael Sloan <mgsloan@gmail.com>
2025-10-08 20:47:35 +00:00
Maksim Bondarenkov
f5884e99d0 audio: Move log::info into a global import (#39810)
I didn't find a commit, but it's now required for all platforms, I got
this compile error with 0.207.3 tag

``` 
  error: cannot find macro `info` in this scope
     --> crates\audio\src\audio.rs:121:13
      |
  121 |             info!("Output stream: {:?}", output_handle);
      |             ^^^^
      |
  help: consider importing this macro
      |
    1 + use log::info;
      |
  
  error: could not compile `audio` (lib) due to 1 previous error
```

Closes #ISSUE

Release Notes:

- N/A
2025-10-08 20:30:27 +00:00
Agus Zubiaga
fce931144e zeta2 inspector: Display prediction request immediately (#39809)
Release Notes:

- N/A

Co-authored-by: Michael Sloan <mgsloan@gmail.com>
2025-10-08 20:23:48 +00:00
Piotr Osiewicz
ef423148fc lsp: Serialize LSP notifications on background threads (#39403)
This should reduce hiccups when opening large files.

Release Notes:

- N/A
2025-10-08 19:48:40 +00:00
Danilo Leal
cd656485c8 settings ui: Fix some layout regressions (#39804)
Release Notes:

- N/A
2025-10-08 15:56:22 -03:00
Alvaro Parker
1e149b755f gpui: Add support for floating windows (#39702)
Closes #ISSUE

This allows new windows like the Rules library or the Settings UI window
to appear floating on window managers like hyprland:


https://github.com/user-attachments/assets/628db7f9-4459-4601-85f1-789923831182

Left is with `WindowKind::Floating` and right is with
`WindowKind::Normal`

Release Notes:

- Added support for floating windows on x11 and wayland
2025-10-08 20:48:17 +02:00
Bartosz Kaszubowski
e0eeda11ed inspector_ui: Align with title bar, other visual tweaks (#39697)
# How

Few tweaks for the GPUI Inspector panel, including toolbar align with
title bar, buffer font for source link, few other layout, spacing and
wording tweaks.

Release Notes:

- N/A

# Preview

### Before

<img width="1286" height="602" alt="Screenshot 2025-10-07 at 19 33 20"
src="https://github.com/user-attachments/assets/515ddcdf-a2c8-4f5f-b37e-b1668df2147f"
/>

### After

<img width="1286" height="542" alt="Screenshot 2025-10-07 at 19 09 24"
src="https://github.com/user-attachments/assets/3a777974-3427-4545-afda-37fabcb012ba"
/>
2025-10-08 12:10:53 -06:00
Michael Sloan
bcef3b5010 zeta2: Parse imports via Tree-sitter queries + improve zeta retrieval-stats (#39735)
Release Notes:

- N/A

---------

Co-authored-by: Max <max@zed.dev>
Co-authored-by: Agus <agus@zed.dev>
Co-authored-by: Oleksiy <oleksiy@zed.dev>
2025-10-08 12:04:06 -06:00
David
5fd187769d Add Codestral edit predictions provider (#34371)
Release Notes:

- Added Codestral edit predictions provider which can be enabled by adding an API key in the Mistral section of agent settings.

![2025-07-13 11 35
33](https://github.com/user-attachments/assets/8bf599d7-33c7-4556-b878-6c645d69661f)


## Config

Get API key from https://console.mistral.ai/codestral and add it in the Mistral section of the agent settings. 

```
  "features": {
    "edit_prediction_provider": "codestral"
  },
  "edit_predictions": {
    "codestral": {
      "model": "codestral-latest",
      "max_tokens": 150
    }
  },
```

---------

Co-authored-by: Michael Sloan <michael@zed.dev>
2025-10-08 12:02:21 -06:00
Munish Mummadi
096930817b Make FoldAtLevel commands discoverable in command palette (#39422)
## Description
Fixes #39376

Add individual FoldAtLevel1-9 actions so users can find fold commands in
the command palette while keeping existing keybindings.

Migrating user keymaps is necessary to have the keybinds show in the command palette.

Closes #39376 

### Changes
- `crates/editor/src/actions.rs` - Added FoldAtLevel1-9 action structs
- `crates/editor/src/editor.rs` - Implemented fold_at_level_1-9 handler
methods
- `crates/editor/src/element.rs` - Registered new actions
- `assets/keymaps/*.json` - Updated keybindings to use new individual
actions

### Other Approaches considered
- Adding #[serde(default)] to existing FoldAtLevel(u32) - wouldn't make
it discoverable
- Creating a single action with enumerated variants - idk about this
that well.

### Release Notes
Release Notes:
- Added Fold At Level 1-9 actions to the command palette

---------

Co-authored-by: HactarCE <6060305+HactarCE@users.noreply.github.com>
2025-10-08 17:28:46 +00:00
Xiaobo Liu
c7d5afedc5 docs: Add missing docs for CommandInterceptResult fields (#39676)
Document the `string` and `positions` fields to resolve TODO comments.

Release Notes:

- N/A

Signed-off-by: Xiaobo Liu <cppcoffee@gmail.com>
2025-10-08 20:20:50 +03:00
Bartosz Kaszubowski
d6b1801fb3 inspector_ui: Split out size from bounds string (#39703)
# How

Tweak the way in which inspected element bounds and size are printed to
improved readability of GPUI Inspector data.

> [!note]
> It looks like the only place in the workspace where bounds are used
within formatted print is GPUI Inspector panel, but I decided to do not
alter [GPUI `geometry.rs` default
format](a7e7f46020/crates/gpui/src/geometry.rs (L1579-L1587)),
since adding multiline output and additional labels in there does not
feel like the beast approach, but maybe I'm wrong?

Release Notes:

- N/A

# Preview

<img width="1168" height="224" alt="Screenshot 2025-10-07 at 20 08 35"
src="https://github.com/user-attachments/assets/97753fc1-68d7-4cf8-ad92-afe85319f3d8"
/>

<img width="1168" height="228" alt="Screenshot 2025-10-07 at 20 09 24"
src="https://github.com/user-attachments/assets/beed2a92-0817-4ed2-bb62-4d7b931e8709"
/>
2025-10-08 11:17:11 -06:00
Conrad Irwin
7c55f7181d Fix configuring shell in project settings (#39795)
I mistakenly broke this when refactoring settings

Closes #39479

Release Notes:

- Fixed a bug where you could no longer configure `terminal.shell` in
project settings
2025-10-08 16:49:44 +00:00
Jakub Konka
4684d6b50e terminal: Fix escaping arguments when using CMD as the shell (#39701)
A couple of caveats:
- We should not auto-escape arguments with Alacritty's `escape_args`
option if using CMD otherwise, the generated command will have way too
many escaped characters for CMD to parse correctly.
- When composing a full command for CMD, we need to put it in double
quotes manually: `cmd /C "activate.bat& pwsh.exe -C do_something"` so
that CMD executes the entire string as a sequence of commands.
- CMD requires `&` as a chaining operator for commands (`;` for other
shells).

Release Notes:

- N/A
2025-10-08 16:44:04 +00:00
Ben Kunkle
578e7e4cbd settings_ui: Focus content controls when opened from nav bar (#39792)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-10-08 12:42:52 -04:00
Danilo Leal
a960db6a43 keymap editor: Adjust the "edit in keymap.json" button (#39789)
Making its visuals and positioning more consistent with the same button
in the settings UI.

Release Notes:

- N/A
2025-10-08 13:03:15 -03:00
Marshall Bowers
5a0f796a44 agent2: Expand auto-retries for completion errors (#39787)
This PR expands our automatic retry behavior for certain classes of
completion errors (e.g., rate limit errors).

Previously this was only available when using burn mode.

We now auto-retry when:

- Using the Zed provider while on a token-based plan
- Using the Zed provider while on a legacy plan with burn mode enabled
- Using a non-Zed provider

Release Notes:

- Expanded automatic retry behavior for errors in the Agent. Errors
classified as "retryable" (such as rate limit errors) will now
automatically be retried when:
  - Using the Zed provider while on a token-based plan
  - Using the Zed provider while on a legacy plan with burn mode enabled
  - Using a non-Zed provider

---------

Co-authored-by: David Kleingeld <davidsk@zed.dev>
2025-10-08 15:52:06 +00:00
Dino
604d56659d file_finder: Fix path matching on starting slash (#39480)
These changes update the way the file finder decides wether to only look
for an absolute path or for a relative path too.

When the provided query started with a slash (`/`) the file finder would
assume this to be an absolute path so would always try to find an
absolute path and return no matches if none was found. This is meant to
support situtations where, for example, a CLI tool might output the
absolute path of a file and the user can copy and paste that in the file
finder.

However, it's should be possible to use slash (`/`) at the start of the
query to specify that only relative files inside a folder should be
matched, which would not work in this scenario.

With these changes, the file finder will first check if the path is
absolute and, if it is and no absolute matches were found, it'll still
try to find relative matches, otherwise it'll simply look for relative
matches.

Closes #39350

Release Notes:

- Fixed project files matches when using slash (`/`) at the start in
order to consider relative paths

---------

Co-authored-by: Piotr Osiewicz <piotr@zed.dev>
Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
2025-10-08 16:42:39 +01:00
Conrad Irwin
1d1c799b4b Reland "Remove cx from ThemeSettings" (#39720)
- **Reapply "Remove cx from ThemeSettings (#38836)" (#39691)**
- **Fix theme loading races**

Closes #ISSUE

Release Notes:

- N/A
2025-10-08 17:36:52 +02:00
Danilo Leal
70af11ef2a settings ui: Add a handful of design tweaks (#39784)
Release Notes:

- N/A
2025-10-08 12:27:22 -03:00
Piotr Osiewicz
5fa4b3bfe8 windows: Do not exit from app in dev builds when cli is not found (#39768)
Release Notes:

- N/A
2025-10-08 17:14:58 +02:00
Joseph T. Lyons
93a5dffea1 Bump Zed to v0.209 (#39781)
Release Notes:

- N/A
2025-10-08 15:14:54 +00:00
Finn Evers
9ac010043c settings_ui: Add fallback for agent_ui_font_size (#39782)
Closes https://github.com/zed-industries/zed/issues/39775

Release Notes:

- N/A
2025-10-08 15:08:39 +00:00
Ben Brandt
dd3b65f707 acp: Don't display failed terminal call on display only terminals (#39780)
We don't get an ExitStatus from a remote terminal, so this check was
failing.

Ideally we move all of this to just needing an exit code, but we will
have to revisit that later.

Release Notes:

- N/A
2025-10-08 14:17:37 +00:00
Dino
057b7b1543 vim: Fix % motion edge case (#39620)
Update Vim's `%` motion to first attempt finding the exact matching
bracket/tag under the cursor, then fall back to the previous
nearest-enclosing logic if none is found. This prevents accidentally
jumping to nested pairs in languages like TSX and Svelte where `<>`,
`</>`, and `/>` are also treated as brackets.

Closes #39368 

Release Notes:

- Fixed an edge case with the `%` motion in vim, where the cursor could
end up in a closing HTML tag instead of the matching bracket
2025-10-08 13:49:55 +01:00
Dino
a9455eb947 migrator: Avoid attempting to migrate empty content (#39771)
This commit fixes an issue where opening zed using `--user-data-dir`
with an empty directory would cause the first run to display a "Failed
to migrate settings" error.

This was caused by the migrator attempting to migrate an empty string,
so if that's the case, we'll simply return `Ok(None)` and avoid
attempting to migrate anything at all.

Relates to #39400

Release Notes:

- N/A

Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
2025-10-08 13:38:26 +01:00
Finn Evers
db3c186af0 language_model: Add image decoding support for BMP and TIFF image formats (#39767)
Related: #39745

Release Notes:

- Added support for pasting TIFF and BMP images in the agent panel.
2025-10-08 11:53:32 +00:00
Xiaobo Liu
71856706c7 agent2: Fix test_save_load_thread for Windows paths (#39753)
Use path! macro for platform-specific path formatting in test
assertions, fixing hardcoded Unix-style paths that failed on Windows.

Release Notes:

- N/A

Signed-off-by: Xiaobo Liu <cppcoffee@gmail.com>
2025-10-08 12:47:43 +02:00
Mikayla Maki
4ec24ebe01 Fix more settings UX problems (#39760)
And remove the feature flag for now.

Release Notes:

- N/A
2025-10-08 10:34:06 +00:00
Remco Smits
4152942a8e markdown: Add support for HTML block quotes (#39755)
This PR adds support for HTML block quotes, that also allows you to have
nested variant of it.

<img width="1441" height="804" alt="Screenshot 2025-10-08 at 10 25 57"
src="https://github.com/user-attachments/assets/4e1da766-fb54-4e87-8654-1ea14330bc97"
/>

Code example used in screenshot:

```html
<blockquote>
    <p>
        Words can be like X-rays, if you use them properly—they’ll go through
        anything. You read and you’re pierced.
    </p>
    <blockquote>
        <p>
            lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus. Suspendisse lectus tortor, dignissim sit amet, adipiscing nec, ultricies sed, dolor.
        </p>
    </blockquote>
</blockquote>
```

Release Notes:

- Markdown: Added support for `HTML` block quotes
2025-10-08 11:33:42 +02:00
Mikayla Maki
bbf4bfad6f Implement the unimplemented setting (#39747)
Release Notes:

- N/A
2025-10-08 07:15:40 +00:00
Mikayla Maki
989d172cfc Add edit JSON button (#39732)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-10-08 06:23:43 +00:00
Danilo Leal
1265b229a9 Update doc comments for agent_buffer_font_size (#39743)
Follow up to https://github.com/zed-industries/zed/pull/39468.

Unlike `agent_ui_font_size`, the `agent_buffer_font_size` setting does
have a default value, which means it does not fall back to the regular
UI font size, but rather to its default value.

Release Notes:

- N/A
2025-10-08 06:14:18 +00:00
Danilo Leal
294ca25f44 settings ui: Add another batch of UX fixes and improvements (#39742)
Release Notes:

- N/A
2025-10-08 06:11:34 +00:00
Ben Kunkle
5c7907ad2f settings_ui: Pre preview launch cleanup (#39733)
Closes #ISSUE

Release Notes:

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

---------

Co-authored-by: Anthony <anthony@zed.dev>
2025-10-07 22:41:48 -04:00
Ben Kunkle
f652c3a14d settings_ui: Filter to get project settings (#39730)
Closes #ISSUE

Release Notes:

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

---------

Co-authored-by: Danilo Leal <67129314+danilo-leal@users.noreply.github.com>
Co-authored-by: Mikayla Maki <mikayla@zed.dev>
2025-10-07 21:36:40 -04:00
Mikayla Maki
69ac003bc9 Add escape to settings window (#39699)
Release Notes:

- N/A
2025-10-08 00:36:33 +00:00
Danilo Leal
d615525771 ui: Rename and simplify NumberField component (#39731) 2025-10-07 21:35:51 -03:00
Danilo Leal
8bf37dd130 settings ui: Add more UX improvements (#39700)
Release Notes:

- N/A
2025-10-07 20:01:52 -03:00
Smit Barmase
8cb67ec91c remote: Fix opening a remote terminal failing on certain systems (#39715)
Closes #38538

Release Notes:

- Fixed an issue where opening a remote terminal failed on systems like
BusyBox, Alpine, Amazon Linux 2, some CentOS images, etc., due to an
invalid option 'C'.
2025-10-08 03:04:32 +05:30
Ben Kunkle
cd67941598 settings_ui: Preserve selected nav entry when changing files (#39721)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-10-07 21:28:47 +00:00
Anthony Eid
669db62e33 settings ui: Move selected nav bar entry on scroll (#39633)
This PR makes selecting a sub-entry in the settings UI nav bar scroll to
that section in the settings page. It also updates the selected
sub-entry when scrolling through a settings page to match what a user is
viewing on the page.

I also added a new helper method to `ScrollHandle` type called
`scroll_to_top_of_item` that scrolls until an item is the top element
visible.

Release Notes:

- N/A
2025-10-07 17:16:39 -04:00
Smit Barmase
41f1835bbe project_panel: Fix clicking away to create file or directory doesn't create it (#39716)
Closes #38919

Now, when unfocusing the filename editor while creating a file or
directory in the project panel, it will create it by default unless the
name is empty or already exists.

Release Notes:

- Improved behavior where unfocusing while creating a new file or
directory in the project panel now creates it instead of discarding it.
2025-10-08 02:23:44 +05:30
Ben Kunkle
791ba9ce4c settings_ui: Soft fail on no default & fix language default loading (#39709)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-10-07 20:27:42 +00:00
Marshall Bowers
e60a61f7e7 languages: Add comment injections for Rust (#39714)
This PR adds comment injections for Rust.

Release Notes:

- Rust: Added comment injections.
2025-10-07 20:26:05 +00:00
Ben Kunkle
b8a6180b82 settings_ui: Title Case Enums (#39711)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-10-07 19:44:45 +00:00
Bartosz Kaszubowski
dfce57c7f8 Remove unused blake3 dependency (#39677)
Did not found any code reference or direct dependants of this package in
the workspace.

Release Notes:

- N/A
2025-10-07 15:35:01 -04:00
Antal Szabó
15580a867b windows: Fix handling of AltGr to avoid conflicts (#38925)
The previous modifier detection treated `AltGr` presses as `Ctrl+Alt`,
which broke entering characters produced by AltGr. For example, on a
Hungarian layout `{` is typed with `AltGr+B`; our code saw that as
`Ctrl+Alt+B` and the keybind took precedence, so the character couldn’t
be entered.

On Windows, AltGr isn’t a first-class modifier. It’s emulated as a
combination of `Right Alt (VK_RMENU)` plus a synthetic `Left Ctrl
(VK_LCONTROL)` press. When users press AltGr, `GetKeyState` reports both
Ctrl and Alt as down, which makes AltGr indistinguishable from a real
`Ctrl+Alt` chord if we only look at aggregate modifier state.

Fix: detect the AltGr pattern by checking `VK_RMENU && VK_LCONTROL`.
When that pattern is present, treat it as text-entry intent and suppress
`control` and `alt` in `current_modifiers()`. This prevents
AltGr-produced characters from colliding with `Ctrl+Alt` keybinds while
keeping other modifiers intact.

Limitation: there is no Windows API to tell whether the active layout
actually has AltGr. As a result, on non-AltGr layouts (e.g. US),
pressing `Right Alt + Left Ctrl` will be interpreted as AltGr and will
not trigger `Ctrl+Alt` keybinds. This is an acceptable trade-off to
ensure AltGr layouts can reliably enter characters; users can still
invoke `Ctrl+Alt` keybinds using `Left Alt` or by choosing bindings that
avoid common AltGr pairs.

I based this on https://github.com/zed-industries/zed/pull/36115 after
trying other different approaches, but this one is a bit more specific.

Does this approach make sense, or is slightly breaking US input in favor
of fixing international input a no-go? I think the benefit - being able
to type certain characters _at all_ - outweighs the shortcomings.
Otherwise, there's a way to detect if the keyboard layout uses AltGr or
not, but it's quite hacky, and involves reading the registry to find the
current layout dll's name, opening that dll, manually declaring struct
layouts that it uses, then parsing out the AltGr flag from a function
call result. I don't think that's worth it, but if needed, I can give
that a shot, let me know.


Release Notes:

- windows: Fixed handling of AltGr to avoid keybinds preventing
character input
2025-10-07 21:28:50 +02:00
Anthony Eid
f7bb22fb83 settings ui: Add missing setting elements (#39644)
Added the following settings to the UI

Editor Page - Scrollbar Section (9 settings)
- Show
- Cursors
- Git Diff
- Search Results
- Selected Text
- Selected Symbol
- Diagnostics
- Horizontal Scrollbar
- Vertical Scrollbar

 Editor Page - Minimap Section (6 settings)
- Show
- Display In
- Thumb
- Thumb Border
- Current Line Highlight
- Max Width Columns

Editor Page - Editor Behavior Section (3 settings)
- Expand Excerpt Lines
- Excerpt Context Lines
- Minimum Contrast For Highlights

 Debugger Page (7 settings)
- Stepping Granularity
- Save Breakpoints
- Timeout
- Dock
- Log DAP Communications
- Format DAP Log Messages
- Button

 Panels Page - Git Panel Section (3 settings)
- Button
- Dock
- Default Width

Collaboration Page - Experimental Section (4 settings)
- Auto Microphone Volume
- Auto Speaker Volume
- Denoise
- Legacy Audio Compatible

Release Notes:

- N/A
2025-10-07 19:20:33 +00:00
Lukas Wirth
7db7ad93a2 Revert "gpui: Assert validity of text runs for StyleText" (#39708)
Reverts zed-industries/zed#39581

This has done its job uncovering incorrect constructions of the
highlight ranges pretty fast. Reverting this to prevent this from
spilling into preview until I can fix the call sites next week
2025-10-07 19:08:33 +00:00
Lukas Wirth
642643de01 language: Fix HighlightedText::first_line_preview creating incorrect highlight ranges (#39705)
Fixes ZED-1XW

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-10-07 18:52:59 +00:00
Ben Kunkle
391e304c9f settings_ui: Keyboard navigation (#39652)
Closes #ISSUE

Release Notes:

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

---------

Co-authored-by: Mikayla <mikayla@zed.dev>
2025-10-07 18:23:11 +00:00
Jakub Konka
3106472bf3 terminal: Escape strings with backticks rather than backslashes in PowerShell (#39657)
Closes #39007 

Strings should be escaped with backticks in PowerShell, so the following

```
\"pwsh.exe -C pytest -m \\\"some_test\\\"\"
```

becomes

```
\"pwsh.exe -C pytest -m `\"some_test`\"\"
```

Otherwise PowerShell will misinterpret the invocation resulting in
weirdness all-around such as the issue linked above.

Release Notes:

- N/A
2025-10-07 19:30:09 +02:00
Cole Miller
d04ac864b8 Don't construct an agent panel when disable_ai is set (#39689)
Follow-up to #39649, possible fix for #39669

This implements an alternate strategy for showing/hiding the agent panel
in response to `disable_ai`. We don't load the panel at all if AI is
disabled at startup, and when the value of `disable_ai` changes, we load
the panel or destroy it as needed.

Release Notes:

- N/A
2025-10-07 12:48:37 -04:00
Vinicius da Motta
f9a2724a8b Remove empty line when collapsing diagnostics (#39459)
Closes #39028

Fixed empty lines appearing when collapsing files with diagnostic
messages in the diagnostics panel.

Added a flag to track when processing a `FoldedBuffer` and skip
`Near/Below` blocks (diagnostic messages) that immediately follow it.
This prevents diagnostics from rendering as empty lines when their file
is collapsed.

Before:
<img width="1489" height="429" alt="before"
src="https://github.com/user-attachments/assets/5e233290-1f6e-403c-a6b3-a65107586d01"
/>

After:
<img width="981" height="270" alt="after"
src="https://github.com/user-attachments/assets/a877b651-6b7f-4441-805c-38ea41e73a18"
/>

Release Notes:
- Fixed empty lines when collapsing files with diagnostics in the
diagnostics panel
2025-10-07 16:38:35 +00:00
Finn Evers
ded73c9d56 Fix an issue where scrollbars would capture too many events (#39690)
This PR fixes an issue where scrollbars would overagressively capture
some events, which could lead to clicks being lost in the process. Also
improves how hovering of the parent is detected to lead to less false
positives.

Release Notes:

- Fixed a rare issue where scrollbars would react to and capture events
they should not react to.
2025-10-07 16:10:36 +00:00
Conrad Irwin
41cf114d8a Revert "Remove cx from ThemeSettings (#38836)" (#39691)
This reverts commit a2a7bd139a.

This caused themes to not load correctly on startup, you needed to edit
your settings.

Release Notes:

- N/A
2025-10-07 15:45:20 +00:00
Marshall Bowers
e765818487 agent: Remove some unused code from the Thread (#39688)
This PR removes some unused code from the Agent1 `Thread`.

Release Notes:

- N/A

Co-authored-by: David Kleingeld <davidsk@zed.dev>
2025-10-07 15:36:53 +00:00
Danilo Leal
84f488879c settings ui: Review available items ordering & writing (#39682)
Release Notes:

- N/A
2025-10-07 11:54:13 -03:00
Bennet Bo Fenner
85985fe960 git: Fix panic in git panel when sort_by_path is true (#39678)
Fixes ZED-1NX

This panic could occur when an `bulk_staging` was set to `Some(...)` and
`sort_by_path` was set to `true`.
When setting `sort_by_path: true`, we call `update_visible_entries(...)`
which then checks if `bulk_staging ` is `Some(...)` and calls
`entry_by_path`. That function accesses `entries`, which still consists
of both headers and entries. But the code
(`entry.status_entry().unwrap()`) assumes that there are no headers in
the entry list if `sort_by_path: true`.

```rust
if GitPanelSettings::get_global(cx).sort_by_path {
    return self
        .entries
        .binary_search_by(|entry| entry.status_entry().unwrap().repo_path.cmp(path)) //This unwrap() would panic
        .ok();
}
```

This has now been fixed by clearing all the entries when `sort_by_path`
changes, as this is the only case where our assumptions are invalid. I
also added a test which 1) actually tests the sort_by_path logic 2)
ensures that we do not re-introduce this panic in the future.


Release Notes:

- Fixed a panic that could occur when using `sort_by_path: true` in the
git panel
2025-10-07 13:05:13 +00:00
Piotr Osiewicz
3bec885536 relpaths: Fix repeated usages of RelPath::unix on static paths (#39675)
- **paths: Cache away results of static construction of RelPath**
- **agent: Cache away results of converting rules file names into
relpaths**

This PR fixed a regression from relpath PR where we've started doing
more work when working with static (Rel-)Paths.

Release Notes:

- N/A
2025-10-07 12:23:31 +00:00
Lukas Wirth
9a5034ea6d Improve command logging and log_err module paths (#39674)
Prior we only logged the crate in `log_err`, which is not too helpful.
We now assemble the module path from the file system path.

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-10-07 12:11:15 +00:00
Alvaro Parker
64eec67a81 Fix floating file chooser (#39154)
Closes #39117 

Some window managers (example: hyprland
https://github.com/hyprwm/Hyprland/issues/11229) still won't open a
floating file chooser because they don't support the XDG foreign
protocol yet: https://wayland.app/protocols/xdg-foreign-unstable-v2

Release Notes:

- Fixed file chooser not floating

---------

Co-authored-by: David Kleingeld <davidsk@zed.dev>
2025-10-07 14:06:48 +02:00
Lukas Wirth
ffff56f7fe Revert "search: Introduce more yield points in project search pending_search task" (#39672)
Reverts zed-industries/zed#39624

This seems to have had the opposite effect
2025-10-07 11:58:58 +00:00
Finn Evers
b02b130b7c extensions_ui: Fix uneven horizontal padding (#39627)
This fixes an issue where the horizontal padding on the extensions page
was uneven and where the padding on the right side would be much larger.

| Before | After |
| --- | --- |
| <img width="2550" height="1694" alt="Bildschirmfoto 2025-10-06 um 19
26 56"
src="https://github.com/user-attachments/assets/cf05b77b-4a9e-4ad9-8fa7-381f9b6b45af"
/> | <img width="2546" height="1694" alt="Bildschirmfoto 2025-10-06 um
19 25 49"
src="https://github.com/user-attachments/assets/493ba188-534a-4e7a-b2c1-2b1380be7150"
/> |

Release Notes:

- Improved the horizontal padding on the extensions tab.
2025-10-07 13:23:02 +02:00
Piotr Osiewicz
41ac6a8764 windows: Use nc-esque ssh askpass auth for remoting (#39646)
This lets us avoid storing user PW in ZED_ASKPASS_PASSWORD env var.
Release Notes:

- N/A
2025-10-07 09:48:03 +02:00
Danilo Leal
963204c99d settings ui: Add new batch of settings (#39650)
Release Notes:

- N/A
2025-10-07 00:27:58 -03:00
Cole Miller
f6f11eb544 Avoid spawning external agent process when AI is disabled at startup (#39649)
Closes #39645 

Release Notes:

- Fixed external agent servers sometimes being spawned when Zed started
even when AI was disabled.
2025-10-07 01:01:17 +00:00
Ben Kunkle
c1e917165d settings_ui: Language settings UI (#39640)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-10-06 19:56:23 -04:00
Conrad Irwin
a2a7bd139a Remove cx from ThemeSettings (#38836)
Before this change the active theme and icon theme were retrofitted onto
the ThemeSettings.

Now they're in their own new global (GlobalTheme::theme(cx) and
GlobalTheme::icon_theme(cx))

This lets us remove cx from the settings traits, and tidy up a few other
things along the way.

Release Notes:

- N/A
2025-10-06 23:06:50 +00:00
Marco Mihai Condrache
4de13e06ec askpass: Fix cli path when executed in a remote server (#39475)
Closes #39469
Closes #39438
Closes #39458

I'm not able to test it, i would appreciate if somebody could do it. I
think this bug was present also for SSH remote projects

Release Notes:

- Fixed an issue where zed bin was not found in remote servers for
askpass

---------

Signed-off-by: Marco Mihai Condrache <52580954+marcocondrache@users.noreply.github.com>
2025-10-07 00:49:06 +02:00
Cole Miller
e680dfb0a0 git_ui: Update project diff more aggressively (#39642)
This fixes a regression in #39557--for the project diff, we rely on
getting an event when a path inside a git repository changes, even if
the git state of the repository didn't change as a result (e.g. a new
modification to a file that already had the "modified" status).

I've also changed this code to send the `UpdateRepository` proto message
even when the git state didn't change, since otherwise we have the same
problem in SSH and collab projects.

Release Notes:

- N/A
2025-10-06 17:52:23 -04:00
Cole Miller
31544d294d ci: Show output of failed tests at the end too (#39643)
This makes it a bit easier to read GHA logs of failed CI runs.

Release Notes:

- N/A
2025-10-06 17:40:45 -04:00
Anthony Eid
4e932297a4 settings ui: Fix panic from reading BufferLineHeight custom variant (#39631)
The panic happened when a user had a settings file with a buffer line
height custom variant, because the drop-down renderer only took into
account the two named variants.

The fix for this will be creating a custom element that allows a user to
manually input a line height greater than one or select either
Comfortable or Standard.

Release Notes:

- N/A
2025-10-06 20:39:05 +00:00
John Tur
b2f0b1b168 Fix Ctrl+C not working when Zed is launched from CLI (#39482)
Closes https://github.com/zed-industries/zed/issues/38383
Closes https://github.com/zed-industries/zed/issues/39330

Release Notes:

- N/A
2025-10-06 20:35:44 +00:00
Ben Kunkle
94f1faffa7 settings_ui: Make unimplemented helper (#39639)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-10-06 19:35:40 +00:00
Anthony Eid
075104a529 settings ui: Move settings data out of settings window (#39638)
Moved `user_settings_data` and `project_settings_data` into their own
module because those functions just represent static data.

Release Notes:

- N/A
2025-10-06 19:29:36 +00:00
Andrew Farkas
c80d213227 Fix infinite loop when worktree is deleted (#39637)
Closes #39442

Release Notes:

- Fixed infinite loop when worktree is deleted

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-10-06 19:20:01 +00:00
Cole Miller
fe9895d112 node_runtime: Bump minimum version for system node to match copilot's requirement (#39632)
Copilot now requires 22.x. See the last min node version bump:
https://github.com/zed-industries/zed/pull/27912

Closes #39461

<img width="1040" height="97" alt="image"
src="https://github.com/user-attachments/assets/8f0490e3-b9b5-45fd-b7f1-321691b862f0"
/>

Release Notes:

- Zed will no longer use `node` from your `$PATH` if it's older than
22.x (previously, the minimum version was 20.x). Instead, it will fall
back to its bundled `node`. This fixes being unable to use Copilot if an
older `node` was installed system-wide.
2025-10-06 18:38:30 +00:00
Conrad Irwin
24bc52a15a Remove chat from docs (#39623)
Updates #37789

Release Notes:

- N/A
2025-10-06 18:33:54 +00:00
David Kleingeld
a65a8bea43 Revert YankEndOfLine default (part of PR #39143) (#39626)
Release Notes:

- N/A
2025-10-06 17:06:35 +00:00
Anthony Eid
ea60a7b172 settings ui: Use font picker element from onboarding instead of editor for font components (#39593)
The font picker from onboarding is a lot friendlier to interact with and
makes it impossible for a user to select an invalid font from the
settings ui.

I also moved the font picker from the onboarding crate to the ui_input
crate

## New Look
<img width="1136" height="812" alt="image"
src="https://github.com/user-attachments/assets/7436682c-6a41-4860-a18b-13e15b8f3f31"
/>

Release Notes:

- N/A
2025-10-06 13:04:43 -04:00
warrenjokinen
a67a55d81a docs: Fix Tree-sitter casing in vim.md (#39527)
The AI here in GitHub helped me find the creative ways that Tree-sitter
was incorrectly typed in the document vim.md

<img width="704" height="196" alt="Tree-sitter-bg"
src="https://github.com/user-attachments/assets/90924405-0961-4436-b6b8-2066de527ddc"
/>

Release Notes:

- N/A
2025-10-06 19:01:48 +02:00
Hexorg
1a9f9ccc29 Add note about inode/directory to Zed desktop entry (#39076)
Release Notes:

- N/A
2025-10-06 18:26:11 +02:00
localcc
6da5945cd2 Optimize fs_watcher to use less RAM by doing less work (#39602)
mac_watcher already does this so it would make more sense to also do
this on Windows and it saves ~500-600mb of ram on the chromium project.

This does not improve memory usage on linux because inotify cannot do
recursive directory monitoring

Release Notes:

- N/A
2025-10-06 18:24:28 +02:00
Lukas Wirth
354cc65daa search: Introduce more yield points in project search pending_search task (#39624)
This should help with project search lagging I believe

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-10-06 16:09:00 +00:00
Lukas Wirth
2c6a8634cc remote: Fix wsl failing to start on some setups (#39612)
Closes https://github.com/zed-industries/zed/issues/39433

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-10-06 17:40:05 +02:00
Danilo Leal
84ec865c44 agent: Fix gradient overlay in file list within the activity bar (#39619)
Release Notes:

- N/A
2025-10-06 15:32:08 +00:00
Smit Barmase
80727a03bf editor: Limit snippet query range instead of collecting from buffer start (#39617)
Fixes hang when computing query for snippet completions when working
with really large buffers.

Release Notes:

- N/A
2025-10-06 20:54:42 +05:30
ozer
e7339fbd42 project_panel: Focus project panel when clicking empty space (#39489)
Closes #39486

Release Notes:

- Fixed: Project Panel now properly focuses when clicking empty space,
allowing keyboard shortcuts to work as expected
2025-10-06 20:50:56 +05:30
Danilo Leal
db5b1a31b5 settings ui: Add some UX adjustments (#39615)
Release Notes:

- N/A
2025-10-06 12:12:35 -03:00
Ratazzi
bc39ed2575 editor: Preserve font features for vim block cursor (#39474)
## Summary

Fixes an issue where font features (like ligatures) were not applied to
text under the vim block cursor. The cursor would inherit the font
family from the character at the cursor
position, but would use default font features instead of the editor's
configured font features.

## Changes

- Make the font mutable when rendering the vim block cursor
- Apply the editor's text style font features to the cursor font

This ensures that text under the block cursor renders with the same
visual appearance as the rest of the editor content.

Closes #39471

Release Notes:

- Fixed vim block cursor not respecting font features (like ligatures)
2025-10-06 08:58:46 -06:00
Danilo Leal
1764337a5d agent: Fix plan summary text overflow in Claude Code threads (#39603)
| Before | After |
|--------|--------|
| <img width="700" height="764" alt="Screenshot 2025-10-06 at 9  43@2x"
src="https://github.com/user-attachments/assets/faf7e93f-f0d8-4bea-9f8d-272c83b41b18"
/> | <img width="700" height="394" alt="Screenshot 2025-10-06 at 9  43
2@2x"
src="https://github.com/user-attachments/assets/3f404e69-de3a-44c2-8111-0212d5d91199"
/> |

Release Notes:

- agent: Fixed a bug in Claude Code threads where the plan summary text
would overflow beyond its container.
2025-10-06 11:40:01 -03:00
Lev Zakharov
3707102702 title_bar: Show git status indicator icon in the title bar (#38029)
See related discussion #37046.

<details>

<summary>Screenshots</summary>

**No Changes**
<img
src="https://github.com/user-attachments/assets/e814da6e-bc9b-4edd-b37a-6bb4680d5bb3"
/>

**Added**
<img
src="https://github.com/user-attachments/assets/07ffdf90-08cb-43f4-b2bd-9966a21e08de"
/>

**Changed**
<img
src="https://github.com/user-attachments/assets/7e13b999-83b3-41ea-b2ab-baaa1541b169"
/>

**Deleted**
<img
src="https://github.com/user-attachments/assets/a77fc7e3-a026-419a-87bd-7146c3ca46a9"
/>

**Conflicts**
<img
src="https://github.com/user-attachments/assets/17e7e35c-d81b-4660-808d-08e12107ea2d"
/>

</details>

Release Notes:

- Show git status indicator icon in the title bar
2025-10-06 09:43:22 -04:00
Marco Mihai Condrache
5263f51432 terminal: Fix terminal cloning on WSL (#39552)
Should close #39428

The working directory of the `wsl.exe` program is set to a Linux path,
which is invalid on the Windows side, causing the terminal to crash. The
first spawn works because there is no active terminal view, allowing a
new shell (which checks for the remote) to be created. I cannot explain
why it works on SSH remote clients, but I may be missing something in
the remote connection implementation.

I don't have a Windows machine to test this, so I would appreciate
someone testing it. 🙏🏼

Release Notes:

- Fixed an issue where WSL terminals could not be splitted

---------

Signed-off-by: Marco Mihai Condrache <52580954+marcocondrache@users.noreply.github.com>
2025-10-06 13:18:27 +00:00
Lukas Wirth
93cd10aaa8 terminal: Re-enable activation scripts on windows (#39604)
Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-10-06 13:07:15 +00:00
Smit Barmase
2c1cc01b81 linux: Fix enter key triggering newline instead of commiting input (#39599)
Closes #31337 #35537

Release Notes:

- Fixed an issue on Linux X11 where pressing Enter added a new line
instead of confirming English input.
2025-10-06 18:22:02 +05:30
Lukas Wirth
81ada92306 editor: Fix clangd switch source header action failing on wsl (#39598)
Closes https://github.com/zed-industries/zed/issues/39180

Release Notes:

- Fixed clangd switch source header action failing on wsl
2025-10-06 12:36:12 +00:00
Lukas Wirth
4bd7ef8bad acp_thread: If available, use git bash over powershell in terminal tool (#39466)
Release Notes:

- When git bash is installed, agents will now use that over powershell
when invoking terminal commands
2025-10-06 13:39:19 +02:00
Lukas Wirth
d1e2a1f20c gpui: Assert validity of text runs for StyleText (#39581)
Should help with figuring out the char boundary panic in text shaping on
windows

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-10-06 13:04:45 +02:00
Danilo Leal
79a8986cb7 settings ui: Add scrollbar and other design details (#39504)
Release Notes:

- N/A
2025-10-06 08:00:47 -03:00
Anthony Eid
d2b91eb2bc settings ui: Add numeric steppers to settings UI (#39491)
This PR adds the numeric stepper component to the settings ui and
implements some settings that rely on this component as well.

I also switched {buffer/ui}_font_weight to the `gpui::FontWeight` type
and added a manual implementation of the Schemars trait. This allows Zed
to send min, max, and default information to the JSON LSP when a user is
manually editing the settings file.

The numeric stepper elements added to the settings ui are below:
- ui font size
- ui font weight
- Buffer font size
- Buffer font weight 
- Scroll sensitivity
- Fast scroll sensitivity
- Vertical scroll margin
- Horizontal scroll margin
- Inline blame padding 
- Inline blame delay
- Inline blame min column
- Unnecessary code fade
- Tab Size
- Hover popover delay

Release Notes:

- N/A
2025-10-06 10:06:33 +00:00
Bartosz Kaszubowski
c26937a848 zed: Show GPUI Inspector item in Dev build menus (#39287)
# Why

I have find out that this tool exists by browsing Keymap Editor. I think
it would be nice for its discoverability to show it in the app menus in
Dev builds.

# How

Add "GPUI Inspector" app menu item conditionally for Dev builds only.

Release Notes:

- N/A

# Preview

<img width="1014" height="948" alt="Screenshot 2025-10-01 at 14 36 48"
src="https://github.com/user-attachments/assets/c0409e67-1f4d-44f3-90b3-293ad4fe5c73"
/>
2025-10-06 12:21:48 +03:00
Lukas Wirth
da82eec4cb editor: Fix utf8 boundary panic in process_completion_for_edit (#39561)
Fixes ZED-1WH

Release Notes:

- Fixed panic when requesting completions after a multibyte character
2025-10-06 08:39:51 +00:00
Lukas Wirth
2bfcd60b88 editor: Shrink DisplayMapSnapshot from 824 to 256 bytes (#39568)
We have unnecessary clones for the fields here as most of the snapshots
contain the others hierarchically.

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-10-06 08:08:49 +00:00
Ratazzi
9c7369f54d terminal: Fix rendering of zero-width combining characters (#39526)
Add support for rendering Unicode combining characters (diacritics) in
the terminal's batched text runs.

- Add append_zero_width_chars() to handle combining marks
- Integrate zero-width chars into all batching code paths
- Update cell extras tracking logic
- Add test for combining character rendering

Fixes display of é, ñ, ô and other diacritics.

Closes #39525

Release Notes:

- Fixed: NFD/NFKD normalized text (e.g., é as e + ◌́) not rendering in
integrated terminal

Before:

<img width="874" height="688" alt="SCR-20251004-udnj"
src="https://github.com/user-attachments/assets/8d9f9c9f-dac4-4382-92c2-8b6c1d817abd"
/>

After:

<img width="873" height="686" alt="SCR-20251004-ulsw"
src="https://github.com/user-attachments/assets/fbd5cdc7-fdd6-44dc-8b05-cc425644f1a0"
/>
2025-10-06 10:02:27 +02:00
Anthony Eid
5160510ed0 chore: Remove unused settings ui module (#39580)
The editor settings control module was the first prototype of what a
settings UI could look like in Zed, but the code is outdated now and is
no longer used. So this PR removes it for cleanup.

Release Notes:

- N/A
2025-10-06 07:32:08 +00:00
Mikayla Maki
ee557fb7ea Add window close keybindings for Settings UI (#39578)
Closes #ISSUE

Release Notes:

- N/A
2025-10-06 07:27:54 +00:00
Mikayla Maki
f9919f9214 Swap the start building and login buttons (#39576)
New onboarding screen:

<img width="1027" height="700" alt="Screenshot 2025-10-05 at 10 38
57 PM"
src="https://github.com/user-attachments/assets/5dc49e53-68e7-4559-8ce0-1bada629781d"
/>


This PR also adds a new telemetry event: `Welcome Start Building
Clicked`

Release Notes:

- N/A
2025-10-06 05:55:57 +00:00
Mikayla Maki
0f0974f105 Add script to bump GPUI version (#39573)
This script successfully published the [0.2.0-test.4 GPUI
prerelease](https://crates.io/crates/gpui/0.2.0-test.4).

Release Notes:

- N/A
2025-10-06 01:42:17 +00:00
Mikayla Maki
e317d98915 Prep crates for GPUI on crates.io (#39543)
Release Notes:

- N/A
2025-10-05 13:44:31 -07:00
Kirill Bulatov
dada318be7 Remove iterations from the slow FS tests (#39564)
Follow-up to https://github.com/zed-industries/zed/pull/39557

Release Notes:

- N/A
2025-10-05 19:47:34 +00:00
Lukas Wirth
b53f9c8863 editor: Fix panic in delete_line with multibyte characters (#39560)
Fixes ZED-1TG

Release Notes:

- Fixed panic in `delete line` when following line contains multibyte
characters
2025-10-05 19:26:27 +00:00
Martin Pool
5b0a2f1ab6 Add more unit tests for Rope (#39426)
I was looking at the rope implementation and some of the existing bugs
that crash in there, and I ran cargo-mutants to inspect test coverage. I
was motivated by bugs like
https://github.com/zed-industries/zed/issues/38556 but this doesn't fix
it and the bug may well be at a higher layer.

This PR adds coverage for a few functions that aren't tested today. I
didn't find any actual bugs yet.

I can see this tree is pretty sparse on docstrings so if you think these
are too verbose I can take them out or drop the whole PR.

Release Notes:

- N/A
2025-10-05 21:42:27 +03:00
Lukas Wirth
d5a4890142 remote: Keep full shell path on wsl (#39555)
Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-10-05 20:29:58 +02:00
Be
cd61bfbd42 docs: Fix path of language extensions on Linux (#39425)
Release Notes:

- N/A
2025-10-05 18:21:48 +00:00
Kirill Bulatov
469ecfbe13 Emit less update events for odd FS events (#39557)
When running flycheck, I've noticed that scrolling starts to lag:


https://github.com/user-attachments/assets/b0bef0a3-ccbd-479d-a385-273398086d38

When checking the trace, it is notable that project panel updates its
entire tree multiple times during flycheck:

<img width="2032" height="1136" alt="image"
src="https://github.com/user-attachments/assets/d1935e77-3b00-4be5-a12a-8a17a9d64202"
/>


[scrolling.trace.zip](https://github.com/user-attachments/files/22710852/scrolling.trace.zip)

Turns out, `target/debug` directory is loaded by Zed (presumably,
reported by langserver as there are sources generated by bindgen and
proto that need to be loaded), and `target/debug/build` directory
received multiple events of a `None` kind for Zed, which trigger the
rescans.

Rework the logic to omit the `None`-kind events in Zed, and to avoid
excessive repo updates if not needed.


Release Notes:

- Improved worktree FS event emits in gitignored directories

---------

Co-authored-by: Cole Miller <cole@zed.dev>
2025-10-05 17:34:55 +00:00
Remco Smits
46b6adadf9 markdown: Add HTML table element support (#38605)
Follow-up: https://github.com/zed-industries/zed/pull/38590

**Note**: this PR contains changes from the [previous
PR](https://github.com/zed-industries/zed/pull/38590), when that PR gets
merged we should see the real changes.
This PR fixes 4 things in order to make:

1. Add html/markdown minifier to remove all the **\t** and **\n**
characters. This is needed as you cannot create new lines with markdown
by just adding an enter to the source file.
2. The event Event::HTML only contained a chunk of the real html for
multiline HTML code. I fixed this by storing the currently watched HTML
inside a buffer and at the end we parse it into the right elements.
Instead of trying to parse a chunck into multiple elements which would
always fail before.
3. Add support for html tables.
4. Fixed panic that occured when table does not have an header.

I also decided to keep the html minifier inside Zed, because making it a
dependency for just a few 100 lines seems to be an overkill. The
original crate had a few cve in their dependencies, so figured this
would be the best.

**Html table support**
<img width="1439" height="801" alt="Screenshot 2025-09-27 at 12 19 07"
src="https://github.com/user-attachments/assets/a884cc6f-cf47-45a2-81fa-91300c7bbf3f"
/>

**Before & after Zed's README (no changes)**
<img width="3440" height="1378" alt="Screenshot 2025-09-27 at 12 34 47"
src="https://github.com/user-attachments/assets/1273b094-fb24-4abd-bffa-56ef3b44670c"
/>

Release Notes:

- Markdown: Added support for html tables
2025-10-05 13:31:17 +02:00
Lukas Wirth
1a9e9c5faa workspace: Add Close Multibuffers pane context menu entry (#39199)
Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-10-05 10:50:36 +02:00
Lukas Wirth
eb64ca8758 askpass: Don't log error when user cancels askpass prompt (#39544)
Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-10-05 07:50:34 +00:00
Ngonidzashe Mangudya
68e6d55596 terminal: Fix terminal split pane opening in wrong directory (#39537)
## Problem
When splitting a terminal pane, the new pane opens in the root directory
(`/`) instead of preserving the current working directory of the
original terminal.

For example, when working in `/Users/modestnerd/Developer/Projects/zed`
(my pc) and splitting the terminal pane, the new pane would open in `/`
instead of staying in the current directory.

## Solution
Restructured the fallback logic in
`new_pane_with_cloned_active_terminal` (terminal_panel.rs:452-456) to
ensure `default_working_directory(workspace, cx)` is called as a
fallback even when a terminal view exists but its `working_directory()`
returns `None`.

The fix changes the nested `and_then` to use `or_else` for the fallback,
ensuring the working directory is always properly resolved before
entering the async block.

Release Notes:

- Fixed terminal split pane opening in wrong directory instead of
preserving the current working directory
2025-10-05 07:08:17 +00:00
Richard Feldman
bcd2d269e2 Fix CRLF handling in display-only terminals (#39538)
## Before

<img width="558" height="739" alt="Screenshot 2025-10-03 at 11 08 43 PM"
src="https://github.com/user-attachments/assets/5dae7f9d-03b6-48eb-826d-e2be60320546"
/>

## After

<img width="551" height="843" alt="Screenshot 2025-10-04 at 8 29 51 PM"
src="https://github.com/user-attachments/assets/2b06dcec-7758-42ad-acf0-c32a7f50f1b1"
/>

No release notes because we aren't using display-only terminals anywhere
yet (`codex-acp` will be the first to use them, and it's still
feature-flagged right now).

Release Notes:

- N/A
2025-10-05 02:43:46 +00:00
Richard Feldman
b32075cdcb Decouple agent reregistration from settings changes (#39528)
Fixes a `--release`-only bug in feature-flagged agents where the feature
flag isn't picked up in some situations (unless there was a settings
change to go with it - due to an early return when settings didn't
change).

Release Notes:

- N/A
2025-10-04 17:19:27 +00:00
Richard Feldman
21e75b8221 Pass through cwd from ACP extension (#39511)
If we get a `cwd` from ACP (because e.g. `codex-acp` is driving the
terminal rather than our own PTY) then use that to display the `cwd` of
the terminal process.

Release Notes:

- N/A
2025-10-04 00:50:14 -04:00
Richard Feldman
978951b79a Don't use PTY in the display-only terminal (#39510)
This only affects `codex-acp` for now.

Not using the PTY in display-only terminals means they don't display the
login prompt (or spurious `%`s) at the end of terminal output
renderings.

Release Notes:

- N/A
2025-10-04 04:49:33 +00:00
Ben Kunkle
6b980ecad3 settings_ui: Dynamic navbar filtering (#39494)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-10-04 03:29:28 +00:00
Mansoor Ahmed
d9c7f44b0b Add ability to hide status bar (#39430)
This pull request adds the ability to configure the setting to hide or
show the status bar, as described in discussion:
https://github.com/zed-industries/zed/discussions/38591

The original [PR
#38974](https://github.com/zed-industries/zed/pull/38974#issuecomment-3362020879)
was merged but reverted due to hidden conflicts. As per @ConradIrwin 's
[request](https://github.com/zed-industries/zed/pull/38974#issuecomment-3362020879),
I am recreating the PR on top of updated main branch.

Release Notes:

- Added an experimental setting `"status_bar": { "experimental.show":
false}` to hide the status bars.

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-10-03 20:11:21 -06:00
John Tur
55e68553a4 Fix caption buttons going off-screen (#39502)
https://github.com/user-attachments/assets/27bf58df-b8c4-4730-856b-d62ec639a552

Previously the caption buttons (minimize, maximize, close) would
disappear off the right side of the title bar.

Release Notes:

- N/A

Co-authored-by: Julia Ryan <juliaryan3.14@gmail.com>
2025-10-03 23:25:54 +00:00
John Tur
9fe46dc8d2 Fix double-clicking on non-empty title bar area (#39500)
Closes #38685 



Release Notes:

- N/A

---------

Co-authored-by: Julia Ryan <juliaryan3.14@gmail.com>
2025-10-03 19:25:15 -04:00
Cole Miller
aced13bc9f Fix ordering of multibuffer excerpts (#39476)
The ordering of path-based excerpts in multibuffers regressed with
#38744, because we changed the `path` field of `PathKey` to be a string
(from `std::path::Path`) and used the derived `Ord` implementation,
which doesn't agree with the path-based order of worktree traversals.
This PR fixes that by using `RelPath` for `PathKey`. Instead of using
`File::full_path`, which can be absolute, we always use `File::path` and
distinguish different worktrees using their ID.

Release Notes:

- N/A

---------

Co-authored-by: Lukas Wirth <me@lukaswirth.dev>
2025-10-03 22:17:31 +00:00
Lukas Wirth
2859cbdba9 Make ShellBuilder::new not branch on a remote shell (#39493)
Release Notes:

- Fixed claude code agent login on remotes

Co-authored-by: Max Brunsfeld <max@zed.dev>
Co-authored-by: Cole Miller <cole@zed.dev>
2025-10-03 23:23:09 +02:00
Marshall Bowers
4443f61c16 x_ai: Add support for Grok 4 Fast (#39492)
This PR adds support for Grok 4 Fast.

Release Notes:

- Added support for Grok 4 Fast models.

Co-authored-by: David Kleingeld <davidsk@zed.dev>
2025-10-03 16:00:09 -04:00
Ben Kunkle
f0f0beb42f settings_ui: Implement sub pages (#39484)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-10-03 19:59:46 +00:00
Finn Evers
6707ff3b50 Make outline modal work in channel notes (#39481)
This fixes an issue where the outline modal would not work in editors
that had no explicit workspace attached to them.

Release Notes:

- Enabled the outline modal to work in channel notes.
2025-10-03 19:20:51 +00:00
Finn Evers
93770e8314 Bring CI back up (#39485)
Release Notes:

- N/A
2025-10-03 19:01:30 +00:00
Max Brunsfeld
f8c617303a Build Windows installer for all releases (#39414)
Release Notes:

- N/A
2025-10-03 10:57:39 -07:00
Anthony Eid
e5f05a21ce settings ui: Improve numeric stepper component interface (#36513)
This is the first step to allowing users to type into a numeric stepper
to set its value. This PR makes the numeric stepper take in a generic
type `T` where T: `NumericStepperType`

```rust
pub trait NumericStepperType:
    Display
    + Add<Output = Self>
    + Sub<Output = Self>
    + Copy
    + Clone
    + Sized
    + PartialOrd
    + FromStr
    + 'static
{
    fn default_format(value: &Self) -> String {
        format!("{}", value)
    }
    fn default_step() -> Self;
    fn large_step() -> Self;
    fn small_step() -> Self;
    fn min_value() -> Self;
    fn max_value() -> Self;
}
```

This allows setting of step sizes and min/max values as well as making
the component easier to use.

cc @danilo-leal 

Release Notes:

- N/A

---------

Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
Co-authored-by: Gaauwe Rombouts <mail@grombouts.nl>
2025-10-03 17:35:30 +00:00
Danilo Leal
f499504b13 agent: Introduce agent_buffer_font_size setting (#39468)
Closes https://github.com/zed-industries/zed/issues/39406
Follow up to https://github.com/zed-industries/zed/pull/38726

This PR introduces the `agent_buffer_font_size` setting and renames
`agent_font_size` to `agent_ui_font_size`. This allows whoever wants
`buffer_font_size` and `agent_buffer_font_size` to match, as well as
folks who want a slightly smaller size only in the agent panel (which...
also looks just better by default!).

Release Notes:

- agent: Introduced the `agent_buffer_font_size` setting and renamed
`agent_font_size` to `agent_ui_font_size`, allowing for granular buffer
font size control in the agent panel vs. regular editors.
2025-10-03 14:23:23 -03:00
Marshall Bowers
504216cbbf settings: Fix JSON schema for ExtensionCapabilityContent (#39478)
This PR fixes the JSON schema for the `ExtensionCapabilityContent`.

Having the nested structs in the variants caused the `kind` property to
not be generated properly. Inlining the fields into the variants fixes
this.

Release Notes:

- N/A
2025-10-03 16:58:47 +00:00
Marshall Bowers
3bf71c690f extension_host: Load granted extension capabilities from settings (#39472)
This PR adds the ability to control the capabilities granted to
extensions by the extension host via the new
`granted_extension_capabilities` setting.

This setting is a list of the capabilities granted to any extension
running in Zed.

The currently available capabilities are:

- `process:exec` - Grants extensions the ability to invoke commands
using
[`zed_extension_api::process::Command`](https://docs.rs/zed_extension_api/latest/zed_extension_api/process/struct.Command.html)
- `download_file` - Grants extensions the ability to download files
using
[`zed_extension_api::download_file`](https://docs.rs/zed_extension_api/latest/zed_extension_api/fn.download_file.html)
- `npm:install` - Grants extensions the ability to install npm packages
using
[`zed_extension_api::npm_install_package`](https://docs.rs/zed_extension_api/latest/zed_extension_api/fn.npm_install_package.html)

Each of these capabilities has parameters that can be used to customize
the permissions.

For instance, to only allow downloads from GitHub, the `download_file`
capability can specify an allowed `host`:

```json
[
  { "kind": "download_file", "host": "github.com", "path": ["**"] }
]
```

The same capability can also be granted multiple times with different
parameters to build up an allowlist:

```json
[
  { "kind": "download_file", "host": "github.com", "path": ["**"] },
  { "kind": "download_file", "host": "gitlab.com", "path": ["**"] }
]
```

When an extension is not granted a capability, the associated extension
APIs protected by that capability will fail.

For instance, trying to use `zed_extension_api::download_file` when the
`download_file` capability is not granted will result in an error that
will be surfaced by the extension:

```
Language server phpactor:

from extension "PHP" version 0.4.3: failed to download file: capability for download_file https://github.com/phpactor/phpactor/releases/download/2025.07.25.0/phpactor.phar is not granted by the extension host
```

Release Notes:

- Added a `granted_extension_capabilities` setting to control the
capabilities granted to extensions.
2025-10-03 15:55:01 +00:00
Smit Barmase
456ba32ea7 macOS: Fix keyboards shortcuts does not work until mouse clicked inside Zed (#39467)
Closes #38258

Regressed in https://github.com/zed-industries/zed/pull/33334
 
Release Notes:

- Fixed an issue on macOS where keyboard shortcuts wouldn’t work until
you clicked inside Zed.
2025-10-03 20:38:29 +05:30
Andrew Farkas
9aeb617a89 Keep folds at cursor open for "fold at level" (#39396)
Closes #39308

Also fixes a possible bug in `apply_selected_diff_hunks()` caused by
reversed selections.

Release Notes:

- Fixed "editor: fold at level" closing regions containing selections

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-10-03 15:06:46 +00:00
Ben Kunkle
fd8bae9b72 docs: Document ctrl-b to toggle left dock not working in Vim mode on Linux and Windows (#39464)
Closes #39370

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-10-03 10:26:04 -04:00
Ben Kunkle
f71c9122ca settings_ui: Write local settings files (#39408)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-10-03 10:05:44 -04:00
Dino
8441aa49b2 vim: Fix visual block handling of wrapped lines (#39355)
These changes fix an issue with vim's visual block mode when soft
wrapping is enabled. In this situation, if one was to move the cursor
either up or down, the selection would be updated to include visual
(wrapped) rows, instead of only the buffer rows. For example, take the
following contents:

```
1 | And here's a very long line that is wrapping
    at this exact point.
2 | And another very long line that is will also
    wrap at this exact point.
```

If one was to place the cursor at the start of the first line, character
`A`, trigger visual block mode with `ctrl-v` and then move down one line
with `j`, the selection would end up as (with [X] representing the
selected characters):

```
1 | [A]nd here's a very long line that is wrapping
    [a]t this exact point.
2 | [A]nd another very long line that is will also
    wrap at this exact point.
```

Instead of the expected:

```
1 | [A]nd here's a very long line that is wrapping
    at this exact point.
2 | [A]nd another very long line that is will also
    wrap at this exact point.
```

With the changes in this commit, `Vim.visual_block_motion` will now
leverage buffer rows in order to navigate to the next or previous row.

Release Notes:

- Fixed handling of soft wrapped lines in vim's visual block mode
2025-10-03 15:58:34 +02:00
Danilo Leal
7b96e1cf1a agent: Add profile description in docs aside (#39412)
This improves the design of the profile picker a bit by making every
item on it have the same height; it also makes it more consistent with
the model selector.

Release Notes:

- N/A
2025-10-03 10:16:33 -03:00
Lukas Wirth
86322a186f worktree: Prevent background scanner from trying to scan file worktrees (#39277)
Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-10-03 13:12:24 +00:00
Joseph T. Lyons
1b94d74dc3 Clarify extension license detection in docs (#39456)
Release Notes:

- N/A
2025-10-03 12:32:24 +00:00
Lukas Wirth
db825c1141 remote: Do not allocate pseudo terminal for ssh commands (#39451)
Closes https://github.com/zed-industries/zed/issues/25382

Release Notes:

- Fixed ssh remote not working if the default shell profile prints to
stdout
2025-10-03 11:33:44 +00:00
522 changed files with 28507 additions and 14036 deletions

View File

@@ -24,7 +24,7 @@ workspace-members = [
third-party = [
{ name = "reqwest", version = "0.11.27" },
# build of remote_server should not include scap / its x11 dependency
{ name = "scap", git = "https://github.com/zed-industries/scap", rev = "808aa5c45b41e8f44729d02e38fd00a2fe2722e7" },
{ name = "zed-scap", git = "https://github.com/zed-industries/scap", rev = "4afea48c3b002197176fb19cd0f9b180dd36eaac", version = "0.0.8-zed" },
# build of remote_server should not need to include on libalsa through rodio
{ name = "rodio", git = "https://github.com/RustAudio/rodio" },
]

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

@@ -20,4 +20,4 @@ runs:
- name: Run tests
shell: bash -euxo pipefail {0}
run: cargo nextest run --workspace --no-fail-fast
run: cargo nextest run --workspace --no-fail-fast --failure-output immediate-final

View File

@@ -24,4 +24,4 @@ runs:
shell: powershell
working-directory: ${{ inputs.working-directory }}
run: |
cargo nextest run --workspace --no-fail-fast
cargo nextest run --workspace --no-fail-fast --failure-output immediate-final

View File

@@ -826,8 +826,9 @@ jobs:
timeout-minutes: 120
name: Create a Windows installer
runs-on: [self-32vcpu-windows-2022]
if: contains(github.event.pull_request.labels.*.name, 'run-bundling')
# if: (startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling'))
if: |
( startsWith(github.ref, 'refs/tags/v')
|| contains(github.event.pull_request.labels.*.name, 'run-bundling') )
needs: [windows_tests]
env:
AZURE_TENANT_ID: ${{ secrets.AZURE_SIGNING_TENANT_ID }}
@@ -865,13 +866,12 @@ jobs:
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
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
# Re-enable when we are ready to publish windows preview releases
if: ${{ !(contains(github.event.pull_request.labels.*.name, 'run-bundling')) && env.RELEASE_CHANNEL == 'preview' }} # upload only preview
if: ${{ !(contains(github.event.pull_request.labels.*.name, 'run-bundling')) }}
with:
draft: true
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}

1381
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -164,6 +164,7 @@ members = [
"crates/sum_tree",
"crates/supermaven",
"crates/supermaven_api",
"crates/codestral",
"crates/svg_preview",
"crates/system_specs",
"crates/tab_switcher",
@@ -273,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" }
collections = { path = "crates/collections", package = "zed-collections", version = "0.1.0" }
command_palette = { path = "crates/command_palette" }
command_palette_hooks = { path = "crates/command_palette_hooks" }
component = { path = "crates/component" }
@@ -289,6 +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" }
diagnostics = { path = "crates/diagnostics" }
editor = { path = "crates/editor" }
extension = { path = "crates/extension" }
@@ -307,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" }
gpui_macros = { path = "crates/gpui_macros", package = "gpui-macros", version = "0.1.0" }
gpui_tokio = { path = "crates/gpui_tokio" }
html_to_markdown = { path = "crates/html_to_markdown" }
http_client = { path = "crates/http_client" }
http_client = { path = "crates/http_client", package = "zed-http-client", version = "0.1.0" }
http_client_tls = { path = "crates/http_client_tls" }
icons = { path = "crates/icons" }
image_viewer = { path = "crates/image_viewer" }
@@ -339,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" }
media = { path = "crates/media", package = "zed-media", version = "0.1.0" }
menu = { path = "crates/menu" }
migrator = { path = "crates/migrator" }
mistral = { path = "crates/mistral" }
@@ -356,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" }
perf = { path = "tooling/perf", package = "zed-perf", version = "0.1.0" }
picker = { path = "crates/picker" }
plugin = { path = "crates/plugin" }
plugin_macros = { path = "crates/plugin_macros" }
@@ -368,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" }
refineable = { path = "crates/refineable", package = "zed-refineable", version = "0.1.0" }
release_channel = { path = "crates/release_channel" }
scheduler = { path = "crates/scheduler" }
remote = { path = "crates/remote" }
@@ -381,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" }
semantic_version = { path = "crates/semantic_version", package = "zed-semantic-version", version = "0.1.0" }
session = { path = "crates/session" }
settings = { path = "crates/settings" }
settings_macros = { path = "crates/settings_macros" }
@@ -394,9 +396,10 @@ 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" }
sum_tree = { path = "crates/sum_tree", package = "zed-sum-tree", version = "0.1.0" }
supermaven = { path = "crates/supermaven" }
supermaven_api = { path = "crates/supermaven_api" }
codestral = { path = "crates/codestral" }
system_specs = { path = "crates/system_specs" }
tab_switcher = { path = "crates/tab_switcher" }
task = { path = "crates/task" }
@@ -417,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" }
util_macros = { path = "crates/util_macros" }
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" }
vercel = { path = "crates/vercel" }
vim = { path = "crates/vim" }
vim_mode_setting = { path = "crates/vim_mode_setting" }
@@ -475,7 +478,6 @@ bitflags = "2.6.0"
blade-graphics = { version = "0.7.0" }
blade-macros = { version = "0.3.0" }
blade-util = { version = "0.3.0" }
blake3 = "1.5.3"
bytes = "1.0"
cargo_metadata = "0.19"
cargo_toml = "0.21"
@@ -605,7 +607,8 @@ rand = "0.9"
rayon = "1.8"
ref-cast = "1.0.24"
regex = "1.5"
reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "951c770a32f1998d6e999cef3e59e0013e6c4415", default-features = false, features = [
# WARNING: If you change this, you must also publish a new version of zed-reqwest to crates.io
reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "c15662463bda39148ba154100dd44d3fba5873a4", default-features = false, features = [
"charset",
"http2",
"macos-system-configuration",
@@ -613,7 +616,7 @@ reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "951c77
"rustls-tls-native-roots",
"socks",
"stream",
] }
], package = "zed-reqwest", version = "0.12.15-zed" }
rsa = "0.9.6"
runtimelib = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734", default-features = false, features = [
"async-dispatcher-runtime",
@@ -622,7 +625,8 @@ rust-embed = { version = "8.4", features = ["include-exclude"] }
rustc-hash = "2.1.0"
rustls = { version = "0.23.26" }
rustls-platform-verifier = "0.5.0"
scap = { git = "https://github.com/zed-industries/scap", rev = "808aa5c45b41e8f44729d02e38fd00a2fe2722e7", default-features = false }
# WARNING: If you change this, you must also publish a new version of zed-scap to crates.io
scap = { git = "https://github.com/zed-industries/scap", rev = "4afea48c3b002197176fb19cd0f9b180dd36eaac", default-features = false, package = "zed-scap", version = "0.0.8-zed" }
schemars = { version = "1.0", features = ["indexmap2"] }
semver = "1.0"
serde = { version = "1.0.221", features = ["derive", "rc"] }
@@ -648,9 +652,9 @@ streaming-iterator = "0.1"
strsim = "0.11"
strum = { version = "0.27.0", features = ["derive"] }
subtle = "2.5.0"
syn = { version = "2.0.101", features = ["full", "extra-traits"] }
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"
@@ -666,6 +670,7 @@ tiny_http = "0.8"
tokio = { version = "1" }
tokio-tungstenite = { version = "0.26", features = ["__rustls-tls"] }
toml = "0.8"
toml_edit = { version = "0.22", default-features = false, features = ["display", "parse", "serde"] }
tower-http = "0.4.4"
tree-sitter = { version = "0.25.10", features = ["wasm"] }
tree-sitter-bash = "0.25.0"
@@ -688,7 +693,7 @@ tree-sitter-python = "0.25"
tree-sitter-regex = "0.24"
tree-sitter-ruby = "0.23"
tree-sitter-rust = "0.24"
tree-sitter-typescript = "0.23"
tree-sitter-typescript = { git = "https://github.com/zed-industries/tree-sitter-typescript", rev = "e2c53597d6a5d9cf7bbe8dccde576fe1e46c5899" } # https://github.com/tree-sitter/tree-sitter-typescript/pull/347
tree-sitter-yaml = { git = "https://github.com/zed-industries/tree-sitter-yaml", rev = "baff0b51c64ef6a1fb1f8390f3ad6015b83ec13a" }
unicase = "2.6"
unicode-script = "0.5.7"
@@ -800,7 +805,7 @@ wasmtime = { opt-level = 3 }
activity_indicator = { codegen-units = 1 }
assets = { codegen-units = 1 }
breadcrumbs = { codegen-units = 1 }
collections = { codegen-units = 1 }
zed-collections = { codegen-units = 1 }
command_palette = { codegen-units = 1 }
command_palette_hooks = { codegen-units = 1 }
extension_cli = { codegen-units = 1 }
@@ -820,11 +825,11 @@ outline = { codegen-units = 1 }
paths = { codegen-units = 1 }
prettier = { codegen-units = 1 }
project_symbols = { codegen-units = 1 }
refineable = { codegen-units = 1 }
zed-refineable = { codegen-units = 1 }
release_channel = { codegen-units = 1 }
reqwest_client = { codegen-units = 1 }
rich_text = { codegen-units = 1 }
semantic_version = { codegen-units = 1 }
zed-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

@@ -31,6 +31,7 @@
"ctrl--": ["zed::DecreaseBufferFontSize", { "persist": false }],
"ctrl-0": ["zed::ResetBufferFontSize", { "persist": false }],
"ctrl-,": "zed::OpenSettings",
"ctrl-alt-,": "zed::OpenSettingsFile",
"ctrl-q": "zed::Quit",
"f4": "debugger::Start",
"shift-f5": "debugger::Stop",
@@ -369,7 +370,8 @@
"bindings": {
"new": "rules_library::NewRule",
"ctrl-n": "rules_library::NewRule",
"ctrl-shift-s": "rules_library::ToggleDefaultRule"
"ctrl-shift-s": "rules_library::ToggleDefaultRule",
"ctrl-w": "workspace::CloseWindow"
}
},
{
@@ -525,15 +527,15 @@
"ctrl-k ctrl-l": "editor::ToggleFold",
"ctrl-k ctrl-[": "editor::FoldRecursive",
"ctrl-k ctrl-]": "editor::UnfoldRecursive",
"ctrl-k ctrl-1": ["editor::FoldAtLevel", 1],
"ctrl-k ctrl-2": ["editor::FoldAtLevel", 2],
"ctrl-k ctrl-3": ["editor::FoldAtLevel", 3],
"ctrl-k ctrl-4": ["editor::FoldAtLevel", 4],
"ctrl-k ctrl-5": ["editor::FoldAtLevel", 5],
"ctrl-k ctrl-6": ["editor::FoldAtLevel", 6],
"ctrl-k ctrl-7": ["editor::FoldAtLevel", 7],
"ctrl-k ctrl-8": ["editor::FoldAtLevel", 8],
"ctrl-k ctrl-9": ["editor::FoldAtLevel", 9],
"ctrl-k ctrl-1": "editor::FoldAtLevel_1",
"ctrl-k ctrl-2": "editor::FoldAtLevel_2",
"ctrl-k ctrl-3": "editor::FoldAtLevel_3",
"ctrl-k ctrl-4": "editor::FoldAtLevel_4",
"ctrl-k ctrl-5": "editor::FoldAtLevel_5",
"ctrl-k ctrl-6": "editor::FoldAtLevel_6",
"ctrl-k ctrl-7": "editor::FoldAtLevel_7",
"ctrl-k ctrl-8": "editor::FoldAtLevel_8",
"ctrl-k ctrl-9": "editor::FoldAtLevel_9",
"ctrl-k ctrl-0": "editor::FoldAll",
"ctrl-k ctrl-j": "editor::UnfoldAll",
"ctrl-space": "editor::ShowCompletions",
@@ -619,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",
@@ -1227,9 +1229,6 @@
"context": "Onboarding",
"use_key_equivalents": true,
"bindings": {
"ctrl-1": "onboarding::ActivateBasicsPage",
"ctrl-2": "onboarding::ActivateEditingPage",
"ctrl-3": "onboarding::ActivateAISetupPage",
"ctrl-enter": "onboarding::Finish",
"alt-shift-l": "onboarding::SignIn",
"alt-shift-a": "onboarding::OpenAccount"
@@ -1241,5 +1240,44 @@
"bindings": {
"ctrl-shift-enter": "workspace::OpenWithSystem"
}
},
{
"context": "SettingsWindow",
"use_key_equivalents": true,
"bindings": {
"ctrl-w": "workspace::CloseWindow",
"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],
"ctrl-2": ["settings_editor::FocusFile", 1],
"ctrl-3": ["settings_editor::FocusFile", 2],
"ctrl-4": ["settings_editor::FocusFile", 3],
"ctrl-5": ["settings_editor::FocusFile", 4],
"ctrl-6": ["settings_editor::FocusFile", 5],
"ctrl-7": ["settings_editor::FocusFile", 6],
"ctrl-8": ["settings_editor::FocusFile", 7],
"ctrl-9": ["settings_editor::FocusFile", 8],
"ctrl-0": ["settings_editor::FocusFile", 9],
"ctrl-pageup": "settings_editor::FocusPreviousFile",
"ctrl-pagedown": "settings_editor::FocusNextFile"
}
},
{
"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",
"pagedown": "settings_editor::FocusNextRootNavEntry",
"home": "settings_editor::FocusFirstNavEntry",
"end": "settings_editor::FocusLastNavEntry"
}
}
]

View File

@@ -40,6 +40,7 @@
"cmd--": ["zed::DecreaseBufferFontSize", { "persist": false }],
"cmd-0": ["zed::ResetBufferFontSize", { "persist": false }],
"cmd-,": "zed::OpenSettings",
"cmd-alt-,": "zed::OpenSettingsFile",
"cmd-q": "zed::Quit",
"cmd-h": "zed::Hide",
"alt-cmd-h": "zed::HideOthers",
@@ -581,15 +582,15 @@
"cmd-k cmd-l": "editor::ToggleFold",
"cmd-k cmd-[": "editor::FoldRecursive",
"cmd-k cmd-]": "editor::UnfoldRecursive",
"cmd-k cmd-1": ["editor::FoldAtLevel", 1],
"cmd-k cmd-2": ["editor::FoldAtLevel", 2],
"cmd-k cmd-3": ["editor::FoldAtLevel", 3],
"cmd-k cmd-4": ["editor::FoldAtLevel", 4],
"cmd-k cmd-5": ["editor::FoldAtLevel", 5],
"cmd-k cmd-6": ["editor::FoldAtLevel", 6],
"cmd-k cmd-7": ["editor::FoldAtLevel", 7],
"cmd-k cmd-8": ["editor::FoldAtLevel", 8],
"cmd-k cmd-9": ["editor::FoldAtLevel", 9],
"cmd-k cmd-1": "editor::FoldAtLevel_1",
"cmd-k cmd-2": "editor::FoldAtLevel_2",
"cmd-k cmd-3": "editor::FoldAtLevel_3",
"cmd-k cmd-4": "editor::FoldAtLevel_4",
"cmd-k cmd-5": "editor::FoldAtLevel_5",
"cmd-k cmd-6": "editor::FoldAtLevel_6",
"cmd-k cmd-7": "editor::FoldAtLevel_7",
"cmd-k cmd-8": "editor::FoldAtLevel_8",
"cmd-k cmd-9": "editor::FoldAtLevel_9",
"cmd-k cmd-0": "editor::FoldAll",
"cmd-k cmd-j": "editor::UnfoldAll",
// Using `ctrl-space` / `ctrl-shift-space` in Zed requires disabling the macOS global shortcut.
@@ -689,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",
@@ -1333,10 +1334,7 @@
"context": "Onboarding",
"use_key_equivalents": true,
"bindings": {
"cmd-1": "onboarding::ActivateBasicsPage",
"cmd-2": "onboarding::ActivateEditingPage",
"cmd-3": "onboarding::ActivateAISetupPage",
"cmd-escape": "onboarding::Finish",
"cmd-enter": "onboarding::Finish",
"alt-tab": "onboarding::SignIn",
"alt-shift-a": "onboarding::OpenAccount"
}
@@ -1347,5 +1345,44 @@
"bindings": {
"ctrl-shift-enter": "workspace::OpenWithSystem"
}
},
{
"context": "SettingsWindow",
"use_key_equivalents": true,
"bindings": {
"cmd-w": "workspace::CloseWindow",
"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],
"ctrl-2": ["settings_editor::FocusFile", 1],
"ctrl-3": ["settings_editor::FocusFile", 2],
"ctrl-4": ["settings_editor::FocusFile", 3],
"ctrl-5": ["settings_editor::FocusFile", 4],
"ctrl-6": ["settings_editor::FocusFile", 5],
"ctrl-7": ["settings_editor::FocusFile", 6],
"ctrl-8": ["settings_editor::FocusFile", 7],
"ctrl-9": ["settings_editor::FocusFile", 8],
"ctrl-0": ["settings_editor::FocusFile", 9],
"cmd-{": "settings_editor::FocusPreviousFile",
"cmd-}": "settings_editor::FocusNextFile"
}
},
{
"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",
"pagedown": "settings_editor::FocusNextRootNavEntry",
"home": "settings_editor::FocusFirstNavEntry",
"end": "settings_editor::FocusLastNavEntry"
}
}
]

View File

@@ -30,6 +30,7 @@
"ctrl--": ["zed::DecreaseBufferFontSize", { "persist": false }],
"ctrl-0": ["zed::ResetBufferFontSize", { "persist": false }],
"ctrl-,": "zed::OpenSettings",
"ctrl-alt-,": "zed::OpenSettingsFile",
"ctrl-q": "zed::Quit",
"f4": "debugger::Start",
"shift-f5": "debugger::Stop",
@@ -133,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",
@@ -243,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",
@@ -378,7 +379,8 @@
"use_key_equivalents": true,
"bindings": {
"ctrl-n": "rules_library::NewRule",
"ctrl-shift-s": "rules_library::ToggleDefaultRule"
"ctrl-shift-s": "rules_library::ToggleDefaultRule",
"ctrl-w": "workspace::CloseWindow"
}
},
{
@@ -534,15 +536,15 @@
"ctrl-k ctrl-l": "editor::ToggleFold",
"ctrl-k ctrl-[": "editor::FoldRecursive",
"ctrl-k ctrl-]": "editor::UnfoldRecursive",
"ctrl-k ctrl-1": ["editor::FoldAtLevel", 1],
"ctrl-k ctrl-2": ["editor::FoldAtLevel", 2],
"ctrl-k ctrl-3": ["editor::FoldAtLevel", 3],
"ctrl-k ctrl-4": ["editor::FoldAtLevel", 4],
"ctrl-k ctrl-5": ["editor::FoldAtLevel", 5],
"ctrl-k ctrl-6": ["editor::FoldAtLevel", 6],
"ctrl-k ctrl-7": ["editor::FoldAtLevel", 7],
"ctrl-k ctrl-8": ["editor::FoldAtLevel", 8],
"ctrl-k ctrl-9": ["editor::FoldAtLevel", 9],
"ctrl-k ctrl-1": "editor::FoldAtLevel_1",
"ctrl-k ctrl-2": "editor::FoldAtLevel_2",
"ctrl-k ctrl-3": "editor::FoldAtLevel_3",
"ctrl-k ctrl-4": "editor::FoldAtLevel_4",
"ctrl-k ctrl-5": "editor::FoldAtLevel_5",
"ctrl-k ctrl-6": "editor::FoldAtLevel_6",
"ctrl-k ctrl-7": "editor::FoldAtLevel_7",
"ctrl-k ctrl-8": "editor::FoldAtLevel_8",
"ctrl-k ctrl-9": "editor::FoldAtLevel_9",
"ctrl-k ctrl-0": "editor::FoldAll",
"ctrl-k ctrl-j": "editor::UnfoldAll",
"ctrl-space": "editor::ShowCompletions",
@@ -621,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",
@@ -1255,12 +1257,48 @@
"context": "Onboarding",
"use_key_equivalents": true,
"bindings": {
"ctrl-1": "onboarding::ActivateBasicsPage",
"ctrl-2": "onboarding::ActivateEditingPage",
"ctrl-3": "onboarding::ActivateAISetupPage",
"ctrl-enter": "onboarding::Finish",
"alt-shift-l": "onboarding::SignIn",
"shift-alt-a": "onboarding::OpenAccount"
}
},
{
"context": "SettingsWindow",
"use_key_equivalents": true,
"bindings": {
"ctrl-w": "workspace::CloseWindow",
"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],
"ctrl-2": ["settings_editor::FocusFile", 1],
"ctrl-3": ["settings_editor::FocusFile", 2],
"ctrl-4": ["settings_editor::FocusFile", 3],
"ctrl-5": ["settings_editor::FocusFile", 4],
"ctrl-6": ["settings_editor::FocusFile", 5],
"ctrl-7": ["settings_editor::FocusFile", 6],
"ctrl-8": ["settings_editor::FocusFile", 7],
"ctrl-9": ["settings_editor::FocusFile", 8],
"ctrl-0": ["settings_editor::FocusFile", 9],
"ctrl-pageup": "settings_editor::FocusPreviousFile",
"ctrl-pagedown": "settings_editor::FocusNextFile"
}
},
{
"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",
"pagedown": "settings_editor::FocusNextRootNavEntry",
"home": "settings_editor::FocusFirstNavEntry",
"end": "settings_editor::FocusLastNavEntry"
}
}
]

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

@@ -240,7 +240,7 @@
"delete": "vim::DeleteRight",
"g shift-j": "vim::JoinLinesNoWhitespace",
"y": "vim::PushYank",
"shift-y": "vim::YankToEndOfLine",
"shift-y": "vim::YankLine",
"x": "vim::DeleteRight",
"shift-x": "vim::DeleteLeft",
"ctrl-a": "vim::Increment",
@@ -393,7 +393,7 @@
"escape": "editor::Cancel",
"shift-d": "vim::DeleteToEndOfLine",
"shift-j": "vim::JoinLines",
"shift-y": "vim::YankToEndOfLine",
"shift-y": "vim::YankLine",
"shift-i": "vim::InsertFirstNonWhitespace",
"shift-a": "vim::InsertEndOfLine",
"o": "vim::InsertLineBelow",
@@ -580,18 +580,18 @@
// "q": "vim::AnyQuotes",
"q": "vim::MiniQuotes",
"|": "vim::VerticalBars",
"(": "vim::Parentheses",
"(": ["vim::Parentheses", { "opening": true }],
")": "vim::Parentheses",
"b": "vim::Parentheses",
// "b": "vim::AnyBrackets",
// "b": "vim::MiniBrackets",
"[": "vim::SquareBrackets",
"[": ["vim::SquareBrackets", { "opening": true }],
"]": "vim::SquareBrackets",
"r": "vim::SquareBrackets",
"{": "vim::CurlyBrackets",
"{": ["vim::CurlyBrackets", { "opening": true }],
"}": "vim::CurlyBrackets",
"shift-b": "vim::CurlyBrackets",
"<": "vim::AngleBrackets",
"<": ["vim::AngleBrackets", { "opening": true }],
">": "vim::AngleBrackets",
"a": "vim::Argument",
"i": "vim::IndentObj",

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": "",
@@ -74,8 +75,10 @@
"ui_font_weight": 400,
// The default font size for text in the UI
"ui_font_size": 16,
// The default font size for text in the agent panel. Falls back to the UI font size if unset.
"agent_font_size": null,
// The default font size for agent responses in the agent panel. Falls back to the UI font size if unset.
"agent_ui_font_size": null,
// The default font size for user messages in the agent panel.
"agent_buffer_font_size": 12,
// How much to fade out unused code.
"unnecessary_code_fade": 0.3,
// Active pane styling settings.
@@ -719,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
@@ -1099,7 +1104,7 @@
// 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:
@@ -1229,6 +1234,10 @@
// 2. Hide the gutter
// "git_gutter": "hide"
"git_gutter": "tracked_files",
/// Sets the debounce threshold (in milliseconds) after which changes are reflected in the git gutter.
///
/// Default: 0
"gutter_debounce": 0,
// Control whether the git blame information is shown inline,
// in the currently focused line.
"inline_blame": {
@@ -1305,15 +1314,18 @@
// "proxy": "",
// "proxy_no_verify": false
// },
// Whether edit predictions are enabled when editing text threads.
// This setting has no effect if globally disabled.
"enabled_in_text_threads": true,
"copilot": {
"enterprise_uri": null,
"proxy": null,
"proxy_no_verify": null
}
},
"codestral": {
"model": null,
"max_tokens": null
},
// Whether edit predictions are enabled when editing text threads.
// This setting has no effect if globally disabled.
"enabled_in_text_threads": true
},
// Settings specific to journaling
"journal": {
@@ -1327,6 +1339,8 @@
},
// Status bar-related settings.
"status_bar": {
// Whether to show the status bar.
"experimental.show": true,
// Whether to show the active language button in the status bar.
"active_language_button": true,
// Whether to show the cursor position button in the status bar.
@@ -1393,8 +1407,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
@@ -1416,8 +1430,8 @@
// Whether or not selecting text in the terminal will automatically
// copy to the system clipboard.
"copy_on_select": false,
// Whether to keep the text selection after copying it to the clipboard
"keep_selection_on_copy": false,
// Whether to keep the text selection after copying it to the clipboard.
"keep_selection_on_copy": true,
// Whether to show the terminal button in the status bar
"button": true,
// Any key-value pairs added to this list will be added to the terminal's
@@ -1507,7 +1521,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": {},
@@ -1562,6 +1575,14 @@
"auto_install_extensions": {
"html": true
},
// The capabilities granted to extensions.
//
// This list can be customized to restrict what extensions are able to do.
"granted_extension_capabilities": [
{ "kind": "process:exec", "command": "*", "args": ["**"] },
{ "kind": "download_file", "host": "*", "path": ["**"] },
{ "kind": "npm:install", "package": "*" }
],
// Controls how completions are processed for this language.
"completions": {
// Controls how words are completed.
@@ -1669,9 +1690,7 @@
"preferred_line_length": 72
},
"Go": {
"code_actions_on_format": {
"source.organizeImports": true
},
"formatter": [{ "code_action": "source.organizeImports" }, { "language_server": {} }],
"debuggers": ["Delve"]
},
"GraphQL": {
@@ -1860,21 +1879,19 @@
// Allows to enable/disable formatting with Prettier
// and configure default Prettier, used when no project-level Prettier installation is found.
"prettier": {
// // Whether to consider prettier formatter or not when attempting to format a file.
"allowed": false
//
// // Use regular Prettier json configuration.
// // If Prettier is allowed, Zed will use this for its Prettier instance for any applicable file, if
// // the project has no other Prettier installed.
// "plugins": [],
//
// // Use regular Prettier json configuration.
// // If Prettier is allowed, Zed will use this for its Prettier instance for any applicable file, if
// // the project has no other Prettier installed.
// Enables or disables formatting with Prettier for any given language.
"allowed": false,
// Forces Prettier integration to use a specific parser name when formatting files with the language.
"plugins": [],
// Default Prettier options, in the format as in package.json section for Prettier.
// If project installs Prettier via its package.json, these options will be ignored.
// "trailingComma": "es5",
// "tabWidth": 4,
// "semi": false,
// "singleQuote": true
// Forces Prettier integration to use a specific parser name when formatting files with the language
// when set to a non-empty string.
"parser": ""
},
// Settings for auto-closing of JSX tags.
"jsx_tag_auto_close": {
@@ -2024,7 +2041,7 @@
// Examples:
// "profiles": {
// "Presenting": {
// "agent_font_size": 20.0,
// "agent_ui_font_size": 20.0,
// "buffer_font_size": 20.0,
// "theme": "One Light",
// "ui_font_size": 20.0
@@ -2037,7 +2054,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

@@ -12,7 +12,7 @@ use language::language_settings::FormatOnSave;
pub use mention::*;
use project::lsp_store::{FormatTrigger, LspFormatTarget};
use serde::{Deserialize, Serialize};
use settings::Settings as _;
use settings::{Settings as _, SettingsLocation};
use task::{Shell, ShellBuilder};
pub use terminal::*;
@@ -35,7 +35,7 @@ use std::rc::Rc;
use std::time::{Duration, Instant};
use std::{fmt::Display, mem, path::PathBuf, sync::Arc};
use ui::App;
use util::{ResultExt, get_default_system_shell};
use util::{ResultExt, get_default_system_shell_preferring_bash};
use uuid::Uuid;
#[derive(Debug)]
@@ -2086,7 +2086,16 @@ impl AcpThread {
) -> Task<Result<Entity<Terminal>>> {
let env = match &cwd {
Some(dir) => self.project.update(cx, |project, cx| {
let shell = TerminalSettings::get_global(cx).shell.clone();
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.as_path().into(), cx)
}),
None => Task::ready(None).shared(),
@@ -2103,24 +2112,24 @@ 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({
let terminal_id = terminal_id.clone();
async move |_this, cx| {
let env = env.await;
let (task_command, task_args) = ShellBuilder::new(
project
.update(cx, |project, cx| {
project
.remote_client()
.and_then(|r| r.read(cx).default_system_shell())
})?
.as_deref(),
&Shell::Program(get_default_system_shell()),
)
.redirect_stdin_to_dev_null()
.build(Some(command.clone()), &args);
let shell = project
.update(cx, |project, cx| {
project
.remote_client()
.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), is_windows)
.redirect_stdin_to_dev_null()
.build(Some(command.clone()), &args);
let terminal = project
.update(cx, |project, cx| {
project.create_terminal_task(
@@ -2333,12 +2342,10 @@ mod tests {
// Create a display-only terminal and then send Created
let lower = cx.new(|cx| {
let builder = ::terminal::TerminalBuilder::new_display_only(
None,
::terminal::terminal_settings::CursorShape::default(),
::terminal::terminal_settings::AlternateScroll::On,
None,
0,
cx,
)
.unwrap();
builder.subscribe(cx)
@@ -2412,12 +2419,10 @@ mod tests {
// Now create a display-only lower-level terminal and send Created
let lower = cx.new(|cx| {
let builder = ::terminal::TerminalBuilder::new_display_only(
None,
::terminal::terminal_settings::CursorShape::default(),
::terminal::terminal_settings::AlternateScroll::On,
None,
0,
cx,
)
.unwrap();
builder.subscribe(cx)

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

@@ -1276,62 +1276,6 @@ impl Thread {
);
}
pub fn retry_last_completion(
&mut self,
window: Option<AnyWindowHandle>,
cx: &mut Context<Self>,
) {
// Clear any existing error state
self.retry_state = None;
// Use the last error context if available, otherwise fall back to configured model
let (model, intent) = if let Some((model, intent)) = self.last_error_context.take() {
(model, intent)
} else if let Some(configured_model) = self.configured_model.as_ref() {
let model = configured_model.model.clone();
let intent = if self.has_pending_tool_uses() {
CompletionIntent::ToolResults
} else {
CompletionIntent::UserPrompt
};
(model, intent)
} else if let Some(configured_model) = self.get_or_init_configured_model(cx) {
let model = configured_model.model.clone();
let intent = if self.has_pending_tool_uses() {
CompletionIntent::ToolResults
} else {
CompletionIntent::UserPrompt
};
(model, intent)
} else {
return;
};
self.send_to_model(model, intent, window, cx);
}
pub fn enable_burn_mode_and_retry(
&mut self,
window: Option<AnyWindowHandle>,
cx: &mut Context<Self>,
) {
self.completion_mode = CompletionMode::Burn;
cx.emit(ThreadEvent::ProfileChanged);
self.retry_last_completion(window, cx);
}
pub fn used_tools_since_last_user_message(&self) -> bool {
for message in self.messages.iter().rev() {
if self.tool_use.message_has_tool_results(message.id) {
return true;
} else if message.role == Role::User {
return false;
}
}
false
}
pub fn to_completion_request(
&self,
model: Arc<dyn LanguageModel>,
@@ -3276,7 +3220,6 @@ mod tests {
use settings::{LanguageModelParameters, Settings, SettingsStore};
use std::sync::Arc;
use std::time::Duration;
use theme::ThemeSettings;
use util::path;
use workspace::Workspace;
@@ -5337,7 +5280,7 @@ fn main() {{
thread_store::init(fs.clone(), cx);
workspace::init_settings(cx);
language_model::init_settings(cx);
ThemeSettings::register(cx);
theme::init(theme::LoadThemes::JustBase, cx);
ToolRegistry::default_global(cx);
assistant_tool::init(cx);

View File

@@ -38,7 +38,7 @@ use std::{
cell::{Ref, RefCell},
path::{Path, PathBuf},
rc::Rc,
sync::{Arc, Mutex},
sync::{Arc, LazyLock, Mutex},
};
use util::{ResultExt as _, rel_path::RelPath};
@@ -74,17 +74,19 @@ impl Column for DataType {
}
}
const RULES_FILE_NAMES: [&str; 9] = [
".rules",
".cursorrules",
".windsurfrules",
".clinerules",
".github/copilot-instructions.md",
"CLAUDE.md",
"AGENT.md",
"AGENTS.md",
"GEMINI.md",
];
static RULES_FILE_NAMES: LazyLock<[&RelPath; 9]> = LazyLock::new(|| {
[
RelPath::unix(".rules").unwrap(),
RelPath::unix(".cursorrules").unwrap(),
RelPath::unix(".windsurfrules").unwrap(),
RelPath::unix(".clinerules").unwrap(),
RelPath::unix(".github/copilot-instructions.md").unwrap(),
RelPath::unix("CLAUDE.md").unwrap(),
RelPath::unix("AGENT.md").unwrap(),
RelPath::unix("AGENTS.md").unwrap(),
RelPath::unix("GEMINI.md").unwrap(),
]
});
pub fn init(fs: Arc<dyn Fs>, cx: &mut App) {
ThreadsDatabase::init(fs, cx);
@@ -232,11 +234,10 @@ impl ThreadStore {
self.enqueue_system_prompt_reload();
}
project::Event::WorktreeUpdatedEntries(_, items) => {
if items.iter().any(|(path, _, _)| {
RULES_FILE_NAMES
.iter()
.any(|name| path.as_ref() == RelPath::unix(name).unwrap())
}) {
if items
.iter()
.any(|(path, _, _)| RULES_FILE_NAMES.iter().any(|name| path.as_ref() == *name))
{
self.enqueue_system_prompt_reload();
}
}
@@ -368,7 +369,7 @@ impl ThreadStore {
.into_iter()
.filter_map(|name| {
worktree
.entry_for_path(RelPath::unix(name).unwrap())
.entry_for_path(name)
.filter(|entry| entry.is_file())
.map(|entry| entry.path.clone())
})

View File

@@ -1418,7 +1418,6 @@ mod tests {
}
#[gpui::test]
#[cfg_attr(target_os = "windows", ignore)] // TODO: Fix this test on Windows
async fn test_save_load_thread(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
@@ -1498,7 +1497,8 @@ mod tests {
model.send_last_completion_stream_text_chunk("Lorem.");
model.end_last_completion_stream();
cx.run_until_parked();
summary_model.send_last_completion_stream_text_chunk("Explaining /a/b.md");
summary_model
.send_last_completion_stream_text_chunk(&format!("Explaining {}", path!("/a/b.md")));
summary_model.end_last_completion_stream();
send.await.unwrap();
@@ -1538,7 +1538,7 @@ mod tests {
history_entries(&history_store, cx),
vec![(
HistoryEntryId::AcpThread(session_id.clone()),
"Explaining /a/b.md".into()
format!("Explaining {}", path!("/a/b.md"))
)]
);
let acp_thread = agent

View File

@@ -15,10 +15,11 @@ use agent_settings::{
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::adapt_schema_to_format;
use chrono::{DateTime, Utc};
use client::{ModelRequestUsage, RequestUsage};
use cloud_llm_client::{CompletionIntent, CompletionRequestStatus, UsageLimit};
use client::{ModelRequestUsage, RequestUsage, UserStore};
use cloud_llm_client::{CompletionIntent, CompletionRequestStatus, Plan, UsageLimit};
use collections::{HashMap, HashSet, IndexMap};
use fs::Fs;
use futures::stream;
use futures::{
FutureExt,
channel::{mpsc, oneshot},
@@ -34,7 +35,7 @@ use language_model::{
LanguageModelImage, LanguageModelProviderId, LanguageModelRegistry, LanguageModelRequest,
LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolResult,
LanguageModelToolResultContent, LanguageModelToolSchemaFormat, LanguageModelToolUse,
LanguageModelToolUseId, Role, SelectedModel, StopReason, TokenUsage,
LanguageModelToolUseId, Role, SelectedModel, StopReason, TokenUsage, ZED_CLOUD_PROVIDER_ID,
};
use project::{
Project,
@@ -585,6 +586,7 @@ pub struct Thread {
pending_title_generation: Option<Task<()>>,
summary: Option<SharedString>,
messages: Vec<Message>,
user_store: Entity<UserStore>,
completion_mode: CompletionMode,
/// Holds the task that handles agent interaction until the end of the turn.
/// Survives across multiple requests as the model performs tool calls and
@@ -641,6 +643,7 @@ impl Thread {
pending_title_generation: None,
summary: None,
messages: Vec::new(),
user_store: project.read(cx).user_store(),
completion_mode: AgentSettings::get_global(cx).preferred_completion_mode,
running_turn: None,
pending_message: None,
@@ -820,6 +823,7 @@ impl Thread {
pending_title_generation: None,
summary: db_thread.detailed_summary,
messages: db_thread.messages,
user_store: project.read(cx).user_store(),
completion_mode: db_thread.completion_mode.unwrap_or_default(),
running_turn: None,
pending_message: None,
@@ -1249,12 +1253,12 @@ impl Thread {
);
log::debug!("Calling model.stream_completion, attempt {}", attempt);
let mut events = model
.stream_completion(request, cx)
.await
.map_err(|error| anyhow!(error))?;
let (mut events, mut error) = match model.stream_completion(request, cx).await {
Ok(events) => (events, None),
Err(err) => (stream::empty().boxed(), Some(err)),
};
let mut tool_results = FuturesUnordered::new();
let mut error = None;
while let Some(event) = events.next().await {
log::trace!("Received completion event: {:?}", event);
match event {
@@ -1302,8 +1306,10 @@ impl Thread {
if let Some(error) = error {
attempt += 1;
let retry =
this.update(cx, |this, _| this.handle_completion_error(error, attempt))??;
let retry = this.update(cx, |this, cx| {
let user_store = this.user_store.read(cx);
this.handle_completion_error(error, attempt, user_store.plan())
})??;
let timer = cx.background_executor().timer(retry.duration);
event_stream.send_retry(retry);
timer.await;
@@ -1330,8 +1336,23 @@ impl Thread {
&mut self,
error: LanguageModelCompletionError,
attempt: u8,
plan: Option<Plan>,
) -> Result<acp_thread::RetryStatus> {
if self.completion_mode == CompletionMode::Normal {
let Some(model) = self.model.as_ref() else {
return Err(anyhow!(error));
};
let auto_retry = if model.provider_id() == ZED_CLOUD_PROVIDER_ID {
match plan {
Some(Plan::V2(_)) => true,
Some(Plan::V1(_)) => self.completion_mode == CompletionMode::Burn,
None => false,
}
} else {
true
};
if !auto_retry {
return Err(anyhow!(error));
}

View File

@@ -9,6 +9,7 @@ use futures::io::BufReader;
use project::Project;
use project::agent_server_store::AgentServerCommand;
use serde::Deserialize;
use task::Shell;
use util::ResultExt as _;
use std::path::PathBuf;
@@ -81,7 +82,7 @@ impl AcpConnection {
is_remote: bool,
cx: &mut AsyncApp,
) -> Result<Self> {
let mut child = util::command::new_smol_command(command.path);
let mut child = util::command::new_smol_command(&command.path);
child
.args(command.args.iter().map(|arg| arg.as_str()))
.envs(command.env.iter().flatten())
@@ -96,6 +97,11 @@ impl AcpConnection {
let stdout = child.stdout.take().context("Failed to take stdout")?;
let stdin = child.stdin.take().context("Failed to take stdin")?;
let stderr = child.stderr.take().context("Failed to take stderr")?;
log::info!(
"Spawning external agent server: {:?}, {:?}",
command.path,
command.args
);
log::trace!("Spawned (pid: {})", child.id());
let sessions = Rc::new(RefCell::new(HashMap::default()));
@@ -712,23 +718,24 @@ impl acp::Client for ClientDelegate {
if let Some(id_str) = terminal_info.get("terminal_id").and_then(|v| v.as_str())
{
let terminal_id = acp::TerminalId(id_str.into());
let cwd = terminal_info
.get("cwd")
.and_then(|v| v.as_str().map(PathBuf::from));
// Create a minimal display-only lower-level terminal and register it.
let _ = session.thread.update(&mut self.cx.clone(), |thread, cx| {
let builder = TerminalBuilder::new_display_only(
None,
CursorShape::default(),
AlternateScroll::On,
None,
0,
cx,
)?;
let lower = cx.new(|cx| builder.subscribe(cx));
thread.on_terminal_provider_event(
TerminalProviderEvent::Created {
terminal_id: terminal_id.clone(),
label: tc.title.clone(),
cwd: None,
cwd,
output_byte_limit: None,
terminal: lower,
},
@@ -820,15 +827,20 @@ impl acp::Client for ClientDelegate {
}
// Use remote shell or default system shell, as appropriate
let remote_shell = project.update(&mut self.cx.clone(), |project, cx| {
project
.remote_client()
.and_then(|r| r.read(cx).default_system_shell())
})?;
let (task_command, task_args) =
task::ShellBuilder::new(remote_shell.as_deref(), &task::Shell::System)
.redirect_stdin_to_dev_null()
.build(Some(args.command.clone()), &args.args);
let shell = project
.update(&mut self.cx.clone(), |project, cx| {
project
.remote_client()
.and_then(|r| r.read(cx).default_system_shell())
.map(Shell::Program)
})?
.unwrap_or(task::Shell::System);
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);
let terminal_entity = project
.update(&mut self.cx.clone(), |project, cx| {

View File

@@ -151,7 +151,7 @@ impl Default for AgentProfileId {
}
impl Settings for AgentSettings {
fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
fn from_settings(content: &settings::SettingsContent) -> Self {
let agent = content.agent.clone().unwrap();
Self {
enabled: agent.enabled.unwrap(),

View File

@@ -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;
@@ -759,13 +759,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 => {}

View File

@@ -203,7 +203,7 @@ impl EntryViewState {
self.entries.drain(range);
}
pub fn agent_font_size_changed(&mut self, cx: &mut App) {
pub fn agent_ui_font_size_changed(&mut self, cx: &mut App) {
for entry in self.entries.iter() {
match entry {
Entry::UserMessage { .. } | Entry::AssistantMessage { .. } => {}
@@ -387,7 +387,7 @@ fn diff_editor_text_style_refinement(cx: &mut App) -> TextStyleRefinement {
font_size: Some(
TextSize::Small
.rems(cx)
.to_pixels(ThemeSettings::get_global(cx).agent_font_size(cx))
.to_pixels(ThemeSettings::get_global(cx).agent_ui_font_size(cx))
.into(),
),
..Default::default()
@@ -414,7 +414,6 @@ mod tests {
use project::Project;
use serde_json::json;
use settings::{Settings as _, SettingsStore};
use theme::ThemeSettings;
use util::path;
use workspace::Workspace;
@@ -544,7 +543,7 @@ mod tests {
Project::init_settings(cx);
AgentSettings::register(cx);
workspace::init_settings(cx);
ThemeSettings::register(cx);
theme::init(theme::LoadThemes::JustBase, cx);
release_channel::init(SemanticVersion::default(), cx);
EditorSettings::register(cx);
});

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)
@@ -290,18 +292,18 @@ impl MessageEditor {
let snapshot = self
.editor
.update(cx, |editor, cx| editor.snapshot(window, cx));
let Some((excerpt_id, _, _)) = snapshot.buffer_snapshot.as_singleton() else {
let Some((excerpt_id, _, _)) = snapshot.buffer_snapshot().as_singleton() else {
return Task::ready(());
};
let Some(start_anchor) = snapshot
.buffer_snapshot
.buffer_snapshot()
.anchor_in_excerpt(*excerpt_id, start)
else {
return Task::ready(());
};
let end_anchor = snapshot
.buffer_snapshot
.anchor_before(start_anchor.to_offset(&snapshot.buffer_snapshot) + content_len + 1);
.buffer_snapshot()
.anchor_before(start_anchor.to_offset(&snapshot.buffer_snapshot()) + content_len + 1);
let crease = if let MentionUri::File { abs_path } = &mention_uri
&& let Some(extension) = abs_path.extension()
@@ -718,7 +720,7 @@ impl MessageEditor {
continue;
};
let crease_range = crease.range().to_offset(&snapshot.buffer_snapshot);
let crease_range = crease.range().to_offset(&snapshot.buffer_snapshot());
if crease_range.start > ix {
//todo(): Custom slash command ContentBlock?
// let chunk = if prevent_slash_commands
@@ -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)
}
@@ -865,11 +874,11 @@ impl MessageEditor {
self.editor.update(cx, |message_editor, cx| {
let snapshot = message_editor.snapshot(window, cx);
let (excerpt_id, _, buffer_snapshot) =
snapshot.buffer_snapshot.as_singleton().unwrap();
snapshot.buffer_snapshot().as_singleton().unwrap();
let text_anchor = buffer_snapshot.anchor_before(buffer_snapshot.len());
let multibuffer_anchor = snapshot
.buffer_snapshot
.buffer_snapshot()
.anchor_in_excerpt(*excerpt_id, text_anchor);
message_editor.edit(
[(
@@ -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()
@@ -1299,7 +1309,7 @@ impl Render for MessageEditor {
font_family: settings.buffer_font.family.clone(),
font_fallbacks: settings.buffer_font.fallbacks.clone(),
font_features: settings.buffer_font.features.clone(),
font_size: settings.buffer_font_size(cx).into(),
font_size: settings.agent_buffer_font_size(cx).into(),
line_height: relative(settings.buffer_line_height.value()),
..Default::default()
};
@@ -1550,7 +1560,7 @@ impl MentionSet {
fn remove_invalid(&mut self, snapshot: EditorSnapshot) {
for (crease_id, crease) in snapshot.crease_snapshot.creases() {
if !crease.range().start.is_valid(&snapshot.buffer_snapshot) {
if !crease.range().start.is_valid(&snapshot.buffer_snapshot()) {
self.mentions.remove(&crease_id);
}
}

View File

@@ -26,7 +26,7 @@ use gpui::{
CursorStyle, EdgesRefinement, ElementId, Empty, Entity, FocusHandle, Focusable, Hsla, Length,
ListOffset, ListState, PlatformDisplay, SharedString, StyleRefinement, Subscription, Task,
TextStyle, TextStyleRefinement, UnderlineStyle, WeakEntity, Window, WindowHandle, div,
ease_in_out, linear_color_stop, linear_gradient, list, point, prelude::*, pulsating_between,
ease_in_out, linear_color_stop, linear_gradient, list, point, pulsating_between,
};
use language::Buffer;
@@ -289,8 +289,9 @@ pub struct AcpThreadView {
available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
is_loading_contents: bool,
new_server_version_available: Option<SharedString>,
resume_thread_metadata: Option<DbThreadMetadata>,
_cancel_task: Option<Task<()>>,
_subscriptions: [Subscription; 4],
_subscriptions: [Subscription; 5],
}
enum ThreadState {
@@ -380,11 +381,17 @@ impl AcpThreadView {
)
});
let agent_server_store = project.read(cx).agent_server_store().clone();
let subscriptions = [
cx.observe_global_in::<SettingsStore>(window, Self::agent_font_size_changed),
cx.observe_global_in::<AgentFontSize>(window, Self::agent_font_size_changed),
cx.observe_global_in::<SettingsStore>(window, Self::agent_ui_font_size_changed),
cx.observe_global_in::<AgentFontSize>(window, Self::agent_ui_font_size_changed),
cx.subscribe_in(&message_editor, window, Self::handle_message_editor_event),
cx.subscribe_in(&entry_view_state, window, Self::handle_entry_view_event),
cx.subscribe_in(
&agent_server_store,
window,
Self::handle_agent_servers_updated,
),
];
Self {
@@ -392,7 +399,14 @@ impl AcpThreadView {
workspace: workspace.clone(),
project: project.clone(),
entry_view_state,
thread_state: Self::initial_state(agent, resume_thread, workspace, project, window, cx),
thread_state: Self::initial_state(
agent.clone(),
resume_thread.clone(),
workspace.clone(),
project.clone(),
window,
cx,
),
login: None,
message_editor,
model_selector: None,
@@ -421,13 +435,14 @@ impl AcpThreadView {
_cancel_task: None,
focus_handle: cx.focus_handle(),
new_server_version_available: None,
resume_thread_metadata: resume_thread,
}
}
fn reset(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.thread_state = Self::initial_state(
self.agent.clone(),
None,
self.resume_thread_metadata.clone(),
self.workspace.clone(),
self.project.clone(),
window,
@@ -775,6 +790,25 @@ impl AcpThreadView {
cx.notify();
}
fn handle_agent_servers_updated(
&mut self,
_agent_server_store: &Entity<project::AgentServerStore>,
_event: &project::AgentServersUpdated,
window: &mut Window,
cx: &mut Context<Self>,
) {
// If we're in a LoadError state OR have a thread_error set (which can happen
// when agent.connect() fails during loading), retry loading the thread.
// This handles the case where a thread is restored before authentication completes.
let should_retry =
matches!(&self.thread_state, ThreadState::LoadError(_)) || self.thread_error.is_some();
if should_retry {
self.thread_error = None;
self.reset(window, cx);
}
}
pub fn workspace(&self) -> &WeakEntity<Workspace> {
&self.workspace
}
@@ -1011,33 +1045,37 @@ impl AcpThreadView {
return;
};
self.message_editor
.update(cx, |editor, cx| editor.clear(window, cx));
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 {
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)
@@ -2693,7 +2731,7 @@ impl AcpThreadView {
let output_line_count = output.map(|output| output.content_line_count).unwrap_or(0);
let command_failed = command_finished
&& output.is_some_and(|o| o.exit_status.is_none_or(|status| !status.success()));
&& output.is_some_and(|o| o.exit_status.is_some_and(|status| !status.success()));
let time_elapsed = if let Some(output) = output {
output.ended_at.duration_since(started_at)
@@ -2716,7 +2754,7 @@ impl AcpThreadView {
let working_dir = working_dir
.as_ref()
.map(|path| format!("{}", path.display()))
.map(|path| path.display().to_string())
.unwrap_or_else(|| "current directory".to_string());
let is_expanded = self.expanded_tool_calls.contains(&tool_call.id);
@@ -3248,6 +3286,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!(
@@ -3354,6 +3398,12 @@ impl AcpThreadView {
.into_any_element()
}
fn activity_bar_bg(&self, cx: &Context<Self>) -> Hsla {
let editor_bg_color = cx.theme().colors().editor_background;
let active_color = cx.theme().colors().element_selected;
editor_bg_color.blend(active_color.opacity(0.3))
}
fn render_activity_bar(
&self,
thread_entity: &Entity<AcpThread>,
@@ -3369,10 +3419,6 @@ impl AcpThreadView {
return None;
}
let editor_bg_color = cx.theme().colors().editor_background;
let active_color = cx.theme().colors().element_selected;
let bg_edit_files_disclosure = editor_bg_color.blend(active_color.opacity(0.3));
// Temporarily always enable ACP edit controls. This is temporary, to lessen the
// impact of a nasty bug that causes them to sometimes be disabled when they shouldn't
// be, which blocks you from being able to accept or reject edits. This switches the
@@ -3383,7 +3429,7 @@ impl AcpThreadView {
v_flex()
.mt_1()
.mx_2()
.bg(bg_edit_files_disclosure)
.bg(self.activity_bar_bg(cx))
.border_1()
.border_b_0()
.border_color(cx.theme().colors().border)
@@ -3424,27 +3470,33 @@ impl AcpThreadView {
.into()
}
fn render_plan_summary(&self, plan: &Plan, window: &mut Window, cx: &Context<Self>) -> Div {
fn render_plan_summary(
&self,
plan: &Plan,
window: &mut Window,
cx: &Context<Self>,
) -> impl IntoElement {
let stats = plan.stats();
let title = if let Some(entry) = stats.in_progress_entry
&& !self.plan_expanded
{
h_flex()
.w_full()
.cursor_default()
.relative()
.w_full()
.gap_1()
.text_xs()
.text_color(cx.theme().colors().text_muted)
.justify_between()
.truncate()
.child(
h_flex()
.gap_1()
.child(
Label::new("Current:")
.size(LabelSize::Small)
.color(Color::Muted),
)
Label::new("Current:")
.size(LabelSize::Small)
.color(Color::Muted),
)
.child(
div()
.text_xs()
.text_color(cx.theme().colors().text_muted)
.line_clamp(1)
.child(MarkdownElement::new(
entry.content.clone(),
plan_label_markdown_style(&entry.status, window, cx),
@@ -3452,10 +3504,23 @@ impl AcpThreadView {
)
.when(stats.pending > 0, |this| {
this.child(
Label::new(format!("{} left", stats.pending))
.size(LabelSize::Small)
.color(Color::Muted)
.mr_1(),
h_flex()
.absolute()
.top_0()
.right_0()
.h_full()
.child(div().min_w_8().h_full().bg(linear_gradient(
90.,
linear_color_stop(self.activity_bar_bg(cx), 1.),
linear_color_stop(self.activity_bar_bg(cx).opacity(0.2), 0.),
)))
.child(
div().pr_0p5().bg(self.activity_bar_bg(cx)).child(
Label::new(format!("{} left", stats.pending))
.size(LabelSize::Small)
.color(Color::Muted),
),
),
)
})
} else {
@@ -3485,23 +3550,19 @@ impl AcpThreadView {
};
h_flex()
.id("plan_summary")
.p_1()
.justify_between()
.w_full()
.gap_1()
.when(self.plan_expanded, |this| {
this.border_b_1().border_color(cx.theme().colors().border)
})
.child(
h_flex()
.id("plan_summary")
.w_full()
.gap_1()
.child(Disclosure::new("plan_disclosure", self.plan_expanded))
.child(title)
.on_click(cx.listener(|this, _, _, cx| {
this.plan_expanded = !this.plan_expanded;
cx.notify();
})),
)
.child(Disclosure::new("plan_disclosure", self.plan_expanded))
.child(title)
.on_click(cx.listener(|this, _, _, cx| {
this.plan_expanded = !this.plan_expanded;
cx.notify();
}))
}
fn render_plan_entries(&self, plan: &Plan, window: &mut Window, cx: &Context<Self>) -> Div {
@@ -3750,7 +3811,7 @@ impl AcpThreadView {
.id(("file-name", index))
.pr_8()
.gap_1p5()
.max_w_full()
.w_full()
.overflow_x_scroll()
.child(file_icon)
.child(h_flex().gap_0p5().children(file_name).children(file_path))
@@ -4902,9 +4963,9 @@ impl AcpThreadView {
)
}
fn agent_font_size_changed(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
fn agent_ui_font_size_changed(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
self.entry_view_state.update(cx, |entry_view_state, cx| {
entry_view_state.agent_font_size_changed(cx);
entry_view_state.agent_ui_font_size_changed(cx);
});
}
@@ -4920,10 +4981,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(
@@ -5334,6 +5397,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 {
@@ -5348,7 +5428,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()
@@ -6035,7 +6115,7 @@ pub(crate) mod tests {
Project::init_settings(cx);
AgentSettings::register(cx);
workspace::init_settings(cx);
ThemeSettings::register(cx);
theme::init(theme::LoadThemes::JustBase, cx);
release_channel::init(SemanticVersion::default(), cx);
EditorSettings::register(cx);
prompt_store::init(cx)
@@ -6609,4 +6689,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",
"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",
"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",
"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",
"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

@@ -619,10 +619,10 @@ mod tests {
cx.update(|_window, cx| {
LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
registry.register_provider(
FakeLanguageModelProvider::new(
Arc::new(FakeLanguageModelProvider::new(
LanguageModelProviderId::new("someprovider"),
LanguageModelProviderName::new("Some Provider"),
),
)),
cx,
);
});

View File

@@ -562,10 +562,6 @@ impl Item for AgentDiffPane {
self.editor.for_each_project_item(cx, f)
}
fn is_singleton(&self, _: &App) -> bool {
false
}
fn set_nav_history(
&mut self,
nav_history: ItemNavHistory,
@@ -850,7 +846,7 @@ fn render_diff_hunk_controls(
editor.update(cx, |editor, cx| {
let snapshot = editor.snapshot(window, cx);
let position =
hunk_range.end.to_point(&snapshot.buffer_snapshot);
hunk_range.end.to_point(&snapshot.buffer_snapshot());
editor.go_to_hunk_before_or_after_position(
&snapshot,
position,
@@ -886,7 +882,7 @@ fn render_diff_hunk_controls(
editor.update(cx, |editor, cx| {
let snapshot = editor.snapshot(window, cx);
let point =
hunk_range.start.to_point(&snapshot.buffer_snapshot);
hunk_range.start.to_point(&snapshot.buffer_snapshot());
editor.go_to_hunk_before_or_after_position(
&snapshot,
point,
@@ -1818,7 +1814,6 @@ mod tests {
use serde_json::json;
use settings::{Settings, SettingsStore};
use std::{path::Path, rc::Rc};
use theme::ThemeSettings;
use util::path;
#[gpui::test]
@@ -1831,7 +1826,7 @@ mod tests {
AgentSettings::register(cx);
prompt_store::init(cx);
workspace::init_settings(cx);
ThemeSettings::register(cx);
theme::init(theme::LoadThemes::JustBase, cx);
EditorSettings::register(cx);
language_model::init_settings(cx);
});
@@ -1983,7 +1978,7 @@ mod tests {
AgentSettings::register(cx);
prompt_store::init(cx);
workspace::init_settings(cx);
ThemeSettings::register(cx);
theme::init(theme::LoadThemes::JustBase, cx);
EditorSettings::register(cx);
language_model::init_settings(cx);
workspace::register_project_item::<Editor>(cx);

View File

@@ -53,7 +53,7 @@ use gpui::{
};
use language::LanguageRegistry;
use language_model::{ConfigurationError, LanguageModelRegistry};
use project::{DisableAiSettings, Project, ProjectPath, Worktree};
use project::{Project, ProjectPath, Worktree};
use prompt_store::{PromptBuilder, PromptStore, UserPromptId};
use rules_library::{RulesLibrary, open_rules_library};
use search::{BufferSearchBar, buffer_search};
@@ -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)]
@@ -519,6 +518,7 @@ impl AgentPanel {
cx,
)
});
panel.as_mut(cx).loading = true;
if let Some(serialized_panel) = serialized_panel {
panel.update(cx, |panel, cx| {
@@ -670,43 +670,6 @@ impl AgentPanel {
)
});
let mut old_disable_ai = false;
cx.observe_global_in::<SettingsStore>(window, move |panel, window, cx| {
let disable_ai = DisableAiSettings::get_global(cx).disable_ai;
if old_disable_ai != disable_ai {
let agent_panel_id = cx.entity_id();
let agent_panel_visible = panel
.workspace
.update(cx, |workspace, cx| {
let agent_dock_position = panel.position(window, cx);
let agent_dock = workspace.dock_at_position(agent_dock_position);
let agent_panel_focused = agent_dock
.read(cx)
.active_panel()
.is_some_and(|panel| panel.panel_id() == agent_panel_id);
let active_panel_visible = agent_dock
.read(cx)
.visible_panel()
.is_some_and(|panel| panel.panel_id() == agent_panel_id);
if agent_panel_focused {
cx.dispatch_action(&ToggleFocus);
}
active_panel_visible
})
.unwrap_or_default();
if agent_panel_visible {
cx.emit(PanelEvent::Close);
}
old_disable_ai = disable_ai;
}
})
.detach();
Self {
active_view,
workspace,
@@ -1108,15 +1071,15 @@ impl AgentPanel {
WhichFontSize::AgentFont => {
if persist {
update_settings_file(self.fs.clone(), cx, move |settings, cx| {
let agent_font_size =
ThemeSettings::get_global(cx).agent_font_size(cx) + delta;
let agent_ui_font_size =
ThemeSettings::get_global(cx).agent_ui_font_size(cx) + delta;
let _ = settings
.theme
.agent_font_size
.insert(theme::clamp_font_size(agent_font_size).into());
.agent_ui_font_size
.insert(theme::clamp_font_size(agent_ui_font_size).into());
});
} else {
theme::adjust_agent_font_size(cx, |size| size + delta);
theme::adjust_agent_ui_font_size(cx, |size| size + delta);
}
}
WhichFontSize::BufferFont => {
@@ -1136,10 +1099,10 @@ impl AgentPanel {
) {
if action.persist {
update_settings_file(self.fs.clone(), cx, move |settings, _| {
settings.theme.agent_font_size = None;
settings.theme.agent_ui_font_size = None;
});
} else {
theme::reset_agent_font_size(cx);
theme::reset_agent_ui_font_size(cx);
}
}
@@ -1975,34 +1938,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)
@@ -2570,7 +2531,7 @@ impl Render for AgentPanel {
match self.active_view.which_font_size_used() {
WhichFontSize::AgentFont => {
WithRemSize::new(ThemeSettings::get_global(cx).agent_font_size(cx))
WithRemSize::new(ThemeSettings::get_global(cx).agent_ui_font_size(cx))
.size_full()
.child(content)
.into_any()

View File

@@ -382,7 +382,7 @@ impl InlineAssistant {
if let Some(editor_assists) = self.assists_by_editor.get(&editor.downgrade()) {
for assist_id in &editor_assists.assist_ids {
let assist = &self.assists[assist_id];
let range = assist.range.to_point(&snapshot.buffer_snapshot);
let range = assist.range.to_point(&snapshot.buffer_snapshot());
if range.start.row <= newest_selection.start.row
&& newest_selection.end.row <= range.end.row
{
@@ -402,16 +402,16 @@ impl InlineAssistant {
selection.end.row -= 1;
}
selection.end.column = snapshot
.buffer_snapshot
.buffer_snapshot()
.line_len(MultiBufferRow(selection.end.row));
} else if let Some(fold) =
snapshot.crease_for_buffer_row(MultiBufferRow(selection.end.row))
{
selection.start = fold.range().start;
selection.end = fold.range().end;
if MultiBufferRow(selection.end.row) < snapshot.buffer_snapshot.max_row() {
if MultiBufferRow(selection.end.row) < snapshot.buffer_snapshot().max_row() {
let chars = snapshot
.buffer_snapshot
.buffer_snapshot()
.chars_at(Point::new(selection.end.row + 1, 0));
for c in chars {
@@ -427,7 +427,7 @@ impl InlineAssistant {
{
selection.end.row += 1;
selection.end.column = snapshot
.buffer_snapshot
.buffer_snapshot()
.line_len(MultiBufferRow(selection.end.row));
}
}
@@ -447,7 +447,7 @@ impl InlineAssistant {
}
selections.push(selection);
}
let snapshot = &snapshot.buffer_snapshot;
let snapshot = &snapshot.buffer_snapshot();
let newest_selection = newest_selection.unwrap();
let mut codegen_ranges = Vec::new();

View File

@@ -15,7 +15,8 @@ use std::{
sync::{Arc, atomic::AtomicBool},
};
use ui::{
HighlightedLabel, ListItem, ListItemSpacing, PopoverMenuHandle, TintColor, Tooltip, prelude::*,
DocumentationAside, DocumentationEdge, DocumentationSide, HighlightedLabel, LabelSize,
ListItem, ListItemSpacing, PopoverMenuHandle, TintColor, Tooltip, prelude::*,
};
/// Trait for types that can provide and manage agent profiles
@@ -86,8 +87,8 @@ impl ProfileSelector {
let picker = cx.new(|cx| {
Picker::list(delegate, window, cx)
.show_scrollbar(true)
.width(rems(20.))
.max_height(Some(rems(16.).into()))
.width(rems(18.))
.max_height(Some(rems(20.).into()))
});
self.picker = Some(picker);
@@ -536,18 +537,10 @@ impl PickerDelegate for ProfilePickerDelegate {
.inset(true)
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected)
.child(
v_flex()
.child(HighlightedLabel::new(
candidate.name.clone(),
entry.positions.clone(),
))
.when_some(Self::documentation(candidate), |this, doc| {
this.child(
Label::new(doc).size(LabelSize::Small).color(Color::Muted),
)
}),
)
.child(HighlightedLabel::new(
candidate.name.clone(),
entry.positions.clone(),
))
.when(is_active, |this| {
this.end_slot(
div()
@@ -561,6 +554,36 @@ impl PickerDelegate for ProfilePickerDelegate {
}
}
fn documentation_aside(
&self,
_window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<DocumentationAside> {
use std::rc::Rc;
let entry = match self.filtered_entries.get(self.selected_index)? {
ProfilePickerEntry::Profile(entry) => entry,
ProfilePickerEntry::Header(_) => return None,
};
let candidate = self.candidates.get(entry.candidate_index)?;
let docs_aside = Self::documentation(candidate)?.to_string();
let settings = AgentSettings::get_global(cx);
let side = match settings.dock {
settings::DockPosition::Left => DocumentationSide::Right,
settings::DockPosition::Bottom | settings::DockPosition::Right => {
DocumentationSide::Left
}
};
Some(DocumentationAside {
side,
edge: DocumentationEdge::Top,
render: Rc::new(move |_| Label::new(docs_aside.clone()).into_any_element()),
})
}
fn render_footer(
&self,
_: &mut Window,

View File

@@ -16,8 +16,8 @@ anyhow.workspace = true
futures.workspace = true
gpui.workspace = true
net.workspace = true
proto.workspace = true
smol.workspace = true
log.workspace = true
tempfile.workspace = true
util.workspace = true
workspace-hack.workspace = true
@@ -25,3 +25,6 @@ zeroize.workspace = true
[target.'cfg(target_os = "windows")'.dependencies]
windows.workspace = true
[package.metadata.cargo-machete]
ignored = ["log"]

View File

@@ -1,10 +1,16 @@
mod encrypted_password;
pub use encrypted_password::{EncryptedPassword, ProcessExt};
pub use encrypted_password::{EncryptedPassword, IKnowWhatIAmDoingAndIHaveReadTheDocs};
#[cfg(target_os = "windows")]
use net::async_net::UnixListener;
use smol::lock::Mutex;
use util::fs::make_file_executable;
use std::ffi::OsStr;
use std::ops::ControlFlow;
use std::sync::Arc;
use std::sync::OnceLock;
use std::{ffi::OsStr, time::Duration};
use std::time::Duration;
use anyhow::{Context as _, Result};
use futures::channel::{mpsc, oneshot};
@@ -14,9 +20,13 @@ use futures::{
};
use gpui::{AsyncApp, BackgroundExecutor, Task};
use smol::fs;
use util::ResultExt as _;
use util::{ResultExt as _, debug_panic, maybe, paths::PathExt};
use crate::encrypted_password::decrypt;
/// Path to the program used for askpass
///
/// On Unix and remote servers, this defaults to the current executable
/// On Windows, this is set to the CLI variant of zed
static ASKPASS_PROGRAM: OnceLock<std::path::PathBuf> = OnceLock::new();
#[derive(PartialEq, Eq)]
pub enum AskPassResult {
@@ -26,6 +36,7 @@ pub enum AskPassResult {
pub struct AskPassDelegate {
tx: mpsc::UnboundedSender<(String, oneshot::Sender<EncryptedPassword>)>,
executor: BackgroundExecutor,
_task: Task<()>,
}
@@ -43,24 +54,27 @@ impl AskPassDelegate {
password_prompt(prompt, channel, cx);
}
});
Self { tx, _task: task }
Self {
tx,
_task: task,
executor: cx.background_executor().clone(),
}
}
pub async fn ask_password(&mut self, prompt: String) -> Result<EncryptedPassword> {
let (tx, rx) = oneshot::channel();
self.tx.send((prompt, tx)).await?;
Ok(rx.await?)
pub fn ask_password(&mut self, prompt: String) -> Task<Option<EncryptedPassword>> {
let mut this_tx = self.tx.clone();
self.executor.spawn(async move {
let (tx, rx) = oneshot::channel();
this_tx.send((prompt, tx)).await.ok()?;
rx.await.ok()
})
}
}
pub struct AskPassSession {
#[cfg(not(target_os = "windows"))]
script_path: std::path::PathBuf,
#[cfg(target_os = "windows")]
askpass_helper: String,
#[cfg(target_os = "windows")]
secret: std::sync::Arc<OnceLock<EncryptedPassword>>,
_askpass_task: Task<()>,
askpass_task: PasswordProxy,
askpass_opened_rx: Option<oneshot::Receiver<()>>,
askpass_kill_master_rx: Option<oneshot::Receiver<()>>,
}
@@ -75,101 +89,57 @@ impl AskPassSession {
/// You must retain this session until the master process exits.
#[must_use]
pub async fn new(executor: &BackgroundExecutor, mut delegate: AskPassDelegate) -> Result<Self> {
use net::async_net::UnixListener;
use util::fs::make_file_executable;
#[cfg(target_os = "windows")]
let secret = std::sync::Arc::new(OnceLock::new());
let temp_dir = tempfile::Builder::new().prefix("zed-askpass").tempdir()?;
let askpass_socket = temp_dir.path().join("askpass.sock");
let askpass_script_path = temp_dir.path().join(ASKPASS_SCRIPT_NAME);
let (askpass_opened_tx, askpass_opened_rx) = oneshot::channel::<()>();
let listener = UnixListener::bind(&askpass_socket).context("creating askpass socket")?;
let zed_cli_path =
util::get_shell_safe_zed_cli_path().context("getting zed-cli path for askpass")?;
let askpass_opened_tx = Arc::new(Mutex::new(Some(askpass_opened_tx)));
let (askpass_kill_master_tx, askpass_kill_master_rx) = oneshot::channel::<()>();
let mut kill_tx = Some(askpass_kill_master_tx);
let kill_tx = Arc::new(Mutex::new(Some(askpass_kill_master_tx)));
#[cfg(target_os = "windows")]
let askpass_secret = secret.clone();
let askpass_task = executor.spawn(async move {
let mut askpass_opened_tx = Some(askpass_opened_tx);
let get_password = {
let executor = executor.clone();
while let Ok((mut stream, _)) = listener.accept().await {
if let Some(askpass_opened_tx) = askpass_opened_tx.take() {
askpass_opened_tx.send(()).ok();
}
let mut buffer = Vec::new();
let mut reader = BufReader::new(&mut stream);
if reader.read_until(b'\0', &mut buffer).await.is_err() {
buffer.clear();
}
let prompt = String::from_utf8_lossy(&buffer);
if let Some(password) = delegate
.ask_password(prompt.to_string())
.await
.context("getting askpass password")
.log_err()
{
#[cfg(target_os = "windows")]
{
askpass_secret.get_or_init(|| password.clone());
move |prompt| {
let prompt = delegate.ask_password(prompt);
let kill_tx = kill_tx.clone();
let askpass_opened_tx = askpass_opened_tx.clone();
#[cfg(target_os = "windows")]
let askpass_secret = askpass_secret.clone();
executor.spawn(async move {
if let Some(askpass_opened_tx) = askpass_opened_tx.lock().await.take() {
askpass_opened_tx.send(()).ok();
}
if let Ok(decrypted) = decrypt(password) {
stream.write_all(decrypted.as_bytes()).await.log_err();
if let Some(password) = prompt.await {
#[cfg(target_os = "windows")]
{
_ = askpass_secret.set(password.clone());
}
ControlFlow::Continue(Ok(password))
} else {
if let Some(kill_tx) = kill_tx.lock().await.take() {
kill_tx.send(()).log_err();
}
ControlFlow::Break(())
}
} else {
if let Some(kill_tx) = kill_tx.take() {
kill_tx.send(()).log_err();
}
// note: we expect the caller to drop this task when it's done.
// We need to keep the stream open until the caller is done to avoid
// spurious errors from ssh.
std::future::pending::<()>().await;
drop(stream);
}
})
}
drop(temp_dir)
});
// Create an askpass script that communicates back to this process.
let askpass_script = generate_askpass_script(&zed_cli_path, &askpass_socket);
fs::write(&askpass_script_path, askpass_script)
.await
.with_context(|| format!("creating askpass script at {askpass_script_path:?}"))?;
make_file_executable(&askpass_script_path).await?;
#[cfg(target_os = "windows")]
let askpass_helper = format!(
"powershell.exe -ExecutionPolicy Bypass -File {}",
askpass_script_path.display()
);
};
let askpass_task = PasswordProxy::new(get_password, executor.clone()).await?;
Ok(Self {
#[cfg(not(target_os = "windows"))]
script_path: askpass_script_path,
#[cfg(target_os = "windows")]
secret,
#[cfg(target_os = "windows")]
askpass_helper,
_askpass_task: askpass_task,
askpass_task,
askpass_kill_master_rx: Some(askpass_kill_master_rx),
askpass_opened_rx: Some(askpass_opened_rx),
})
}
#[cfg(not(target_os = "windows"))]
pub fn script_path(&self) -> impl AsRef<OsStr> {
&self.script_path
}
#[cfg(target_os = "windows")]
pub fn script_path(&self) -> impl AsRef<OsStr> {
&self.askpass_helper
}
// This will run the askpass task forever, resolving as many authentication requests as needed.
// The caller is responsible for examining the result of their own commands and cancelling this
// future when this is no longer needed. Note that this can only be called once, but due to the
@@ -201,8 +171,109 @@ impl AskPassSession {
pub fn get_password(&self) -> Option<EncryptedPassword> {
self.secret.get().cloned()
}
pub fn script_path(&self) -> impl AsRef<OsStr> {
self.askpass_task.script_path()
}
}
pub struct PasswordProxy {
_task: Task<()>,
#[cfg(not(target_os = "windows"))]
askpass_script_path: std::path::PathBuf,
#[cfg(target_os = "windows")]
askpass_helper: String,
}
impl PasswordProxy {
pub async fn new(
mut get_password: impl FnMut(String) -> Task<ControlFlow<(), Result<EncryptedPassword>>>
+ 'static
+ Send
+ Sync,
executor: BackgroundExecutor,
) -> Result<Self> {
let temp_dir = tempfile::Builder::new().prefix("zed-askpass").tempdir()?;
let askpass_socket = temp_dir.path().join("askpass.sock");
let askpass_script_path = temp_dir.path().join(ASKPASS_SCRIPT_NAME);
let current_exec =
std::env::current_exe().context("Failed to determine current zed executable path.")?;
let askpass_program = ASKPASS_PROGRAM
.get_or_init(|| current_exec)
.try_shell_safe()
.context("Failed to shell-escape Askpass program path.")?
.to_string();
// Create an askpass script that communicates back to this process.
let askpass_script = generate_askpass_script(&askpass_program, &askpass_socket);
let _task = executor.spawn(async move {
maybe!(async move {
let listener =
UnixListener::bind(&askpass_socket).context("creating askpass socket")?;
while let Ok((mut stream, _)) = listener.accept().await {
let mut buffer = Vec::new();
let mut reader = BufReader::new(&mut stream);
if reader.read_until(b'\0', &mut buffer).await.is_err() {
buffer.clear();
}
let prompt = String::from_utf8_lossy(&buffer).into_owned();
let password = get_password(prompt).await;
match password {
ControlFlow::Continue(password) => {
if let Ok(password) = password
&& let Ok(decrypted) =
password.decrypt(IKnowWhatIAmDoingAndIHaveReadTheDocs)
{
stream.write_all(decrypted.as_bytes()).await.log_err();
}
}
ControlFlow::Break(()) => {
// note: we expect the caller to drop this task when it's done.
// We need to keep the stream open until the caller is done to avoid
// spurious errors from ssh.
std::future::pending::<()>().await;
drop(stream);
}
}
}
drop(temp_dir);
Result::<_, anyhow::Error>::Ok(())
})
.await
.log_err();
});
fs::write(&askpass_script_path, askpass_script)
.await
.with_context(|| format!("creating askpass script at {askpass_script_path:?}"))?;
make_file_executable(&askpass_script_path).await?;
#[cfg(target_os = "windows")]
let askpass_helper = format!(
"powershell.exe -ExecutionPolicy Bypass -File {}",
askpass_script_path.display()
);
Ok(Self {
_task,
#[cfg(not(target_os = "windows"))]
askpass_script_path,
#[cfg(target_os = "windows")]
askpass_helper,
})
}
pub fn script_path(&self) -> impl AsRef<OsStr> {
#[cfg(not(target_os = "windows"))]
{
&self.askpass_script_path
}
#[cfg(target_os = "windows")]
{
&self.askpass_helper
}
}
}
/// The main function for when Zed is running in netcat mode for use in askpass.
/// Called from both the remote server binary and the zed binary in their respective main functions.
pub fn main(socket: &str) {
@@ -249,12 +320,17 @@ pub fn main(socket: &str) {
}
}
pub fn set_askpass_program(path: std::path::PathBuf) {
if ASKPASS_PROGRAM.set(path).is_err() {
debug_panic!("askpass program has already been set");
}
}
#[inline]
#[cfg(not(target_os = "windows"))]
fn generate_askpass_script(zed_cli_path: &str, askpass_socket: &std::path::Path) -> String {
fn generate_askpass_script(askpass_program: &str, askpass_socket: &std::path::Path) -> String {
format!(
"{shebang}\n{print_args} | {zed_cli} --askpass={askpass_socket} 2> /dev/null \n",
zed_cli = zed_cli_path,
"{shebang}\n{print_args} | {askpass_program} --askpass={askpass_socket} 2> /dev/null \n",
askpass_socket = askpass_socket.display(),
print_args = "printf '%s\\0' \"$@\"",
shebang = "#!/bin/sh",
@@ -263,13 +339,12 @@ fn generate_askpass_script(zed_cli_path: &str, askpass_socket: &std::path::Path)
#[inline]
#[cfg(target_os = "windows")]
fn generate_askpass_script(zed_cli_path: &str, askpass_socket: &std::path::Path) -> String {
fn generate_askpass_script(askpass_program: &str, askpass_socket: &std::path::Path) -> String {
format!(
r#"
$ErrorActionPreference = 'Stop';
($args -join [char]0) | & "{zed_cli}" --askpass={askpass_socket} 2> $null
($args -join [char]0) | & "{askpass_program}" --askpass={askpass_socket} 2> $null
"#,
zed_cli = zed_cli_path,
askpass_socket = askpass_socket.display(),
)
}

View File

@@ -21,27 +21,6 @@ type LengthWithoutPadding = u32;
#[derive(Clone)]
pub struct EncryptedPassword(Vec<u8>, LengthWithoutPadding);
pub trait ProcessExt {
fn encrypted_env(&mut self, name: &str, value: EncryptedPassword) -> &mut Self;
}
impl ProcessExt for smol::process::Command {
fn encrypted_env(&mut self, name: &str, value: EncryptedPassword) -> &mut Self {
if let Ok(password) = decrypt(value) {
self.env(name, password);
}
self
}
}
impl TryFrom<EncryptedPassword> for proto::AskPassResponse {
type Error = anyhow::Error;
fn try_from(pw: EncryptedPassword) -> Result<Self, Self::Error> {
let pw = decrypt(pw)?;
Ok(Self { response: pw })
}
}
impl Drop for EncryptedPassword {
fn drop(&mut self) {
self.0.zeroize();
@@ -79,38 +58,45 @@ impl TryFrom<&str> for EncryptedPassword {
}
}
pub(crate) fn decrypt(mut password: EncryptedPassword) -> Result<String> {
#[cfg(windows)]
{
use anyhow::Context;
use windows::Win32::Security::Cryptography::{
CRYPTPROTECTMEMORY_BLOCK_SIZE, CRYPTPROTECTMEMORY_SAME_PROCESS, CryptUnprotectMemory,
};
assert_eq!(
password.0.len() % CRYPTPROTECTMEMORY_BLOCK_SIZE as usize,
0,
"Violated pre-condition (buffer size <{}> must be a multiple of CRYPTPROTECTMEMORY_BLOCK_SIZE <{}>) for CryptUnprotectMemory.",
password.0.len(),
CRYPTPROTECTMEMORY_BLOCK_SIZE
);
if password.1 != 0 {
unsafe {
CryptUnprotectMemory(
password.0.as_mut_ptr() as _,
password.0.len().try_into()?,
CRYPTPROTECTMEMORY_SAME_PROCESS,
)
.context("while decrypting a SSH password")?
/// Read the docs for [EncryptedPassword]; please take care of not storing the plaintext string in memory for extended
/// periods of time.
pub struct IKnowWhatIAmDoingAndIHaveReadTheDocs;
impl EncryptedPassword {
pub fn decrypt(mut self, _: IKnowWhatIAmDoingAndIHaveReadTheDocs) -> Result<String> {
#[cfg(windows)]
{
use anyhow::Context;
use windows::Win32::Security::Cryptography::{
CRYPTPROTECTMEMORY_BLOCK_SIZE, CRYPTPROTECTMEMORY_SAME_PROCESS,
CryptUnprotectMemory,
};
assert_eq!(
self.0.len() % CRYPTPROTECTMEMORY_BLOCK_SIZE as usize,
0,
"Violated pre-condition (buffer size <{}> must be a multiple of CRYPTPROTECTMEMORY_BLOCK_SIZE <{}>) for CryptUnprotectMemory.",
self.0.len(),
CRYPTPROTECTMEMORY_BLOCK_SIZE
);
if self.1 != 0 {
unsafe {
CryptUnprotectMemory(
self.0.as_mut_ptr() as _,
self.0.len().try_into()?,
CRYPTPROTECTMEMORY_SAME_PROCESS,
)
.context("while decrypting a SSH password")?
};
{
// Remove padding
_ = password.0.drain(password.1 as usize..);
{
// Remove padding
_ = self.0.drain(self.1 as usize..);
}
}
}
Ok(String::from_utf8(std::mem::take(&mut password.0))?)
Ok(String::from_utf8(std::mem::take(&mut self.0))?)
}
#[cfg(not(windows))]
Ok(String::from_utf8(std::mem::take(&mut self.0))?)
}
#[cfg(not(windows))]
Ok(String::from_utf8(std::mem::take(&mut password.0))?)
}

View File

@@ -1003,7 +1003,7 @@ impl ToolCard for EditFileToolCard {
font_size: Some(
TextSize::Small
.rems(cx)
.to_pixels(ThemeSettings::get_global(cx).agent_font_size(cx))
.to_pixels(ThemeSettings::get_global(cx).agent_ui_font_size(cx))
.into(),
),
..TextStyleRefinement::default()

View File

@@ -18,7 +18,7 @@ use portable_pty::{CommandBuilder, PtySize, native_pty_system};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::Settings;
use settings::{Settings, SettingsLocation};
use std::{
env,
path::{Path, PathBuf},
@@ -32,8 +32,8 @@ use terminal_view::TerminalView;
use theme::ThemeSettings;
use ui::{CommonAnimationExt, Disclosure, Tooltip, prelude::*};
use util::{
ResultExt, get_default_system_shell, markdown::MarkdownInlineCode, size::format_file_size,
time::duration_alt_display,
ResultExt, get_default_system_shell_preferring_bash, markdown::MarkdownInlineCode,
size::format_file_size, time::duration_alt_display,
};
use workspace::Workspace;
@@ -122,16 +122,28 @@ impl Tool for TerminalTool {
let cwd = working_dir.clone();
let env = match &cwd {
Some(dir) => project.update(cx, |project, cx| {
let shell = TerminalSettings::get_global(cx).shell.clone();
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.as_path().into(), cx)
}),
None => Task::ready(None).shared(),
};
let remote_shell = project.update(cx, |project, cx| {
project
.remote_client()
.and_then(|r| r.read(cx).default_system_shell())
});
let is_windows = project.read(cx).path_style(cx).is_windows();
let shell = project
.update(cx, |project, cx| {
project
.remote_client()
.and_then(|r| r.read(cx).default_system_shell())
})
.unwrap_or_else(|| get_default_system_shell_preferring_bash());
let env = cx.spawn(async move |_| {
let mut env = env.await.unwrap_or_default();
@@ -144,12 +156,9 @@ impl Tool for TerminalTool {
let build_cmd = {
let input_command = input.command.clone();
move || {
ShellBuilder::new(
remote_shell.as_deref(),
&Shell::Program(get_default_system_shell()),
)
.redirect_stdin_to_dev_null()
.build(Some(input_command.clone()), &[])
ShellBuilder::new(&Shell::Program(shell), is_windows)
.redirect_stdin_to_dev_null()
.build(Some(input_command), &[])
}
};
@@ -478,7 +487,7 @@ impl ToolCard for TerminalToolCard {
.as_ref()
.cloned()
.or_else(|| env::current_dir().ok())
.map(|path| format!("{}", path.display()))
.map(|path| path.display().to_string())
.unwrap_or_else(|| "current directory".to_string());
let header = h_flex()
@@ -696,7 +705,6 @@ mod tests {
use serde_json::json;
use settings::{Settings, SettingsStore};
use terminal::terminal_settings::TerminalSettings;
use theme::ThemeSettings;
use util::{ResultExt as _, test::TempTree};
use super::*;
@@ -711,7 +719,7 @@ mod tests {
language::init(cx);
Project::init_settings(cx);
workspace::init_settings(cx);
ThemeSettings::register(cx);
theme::init(theme::LoadThemes::JustBase, cx);
TerminalSettings::register(cx);
EditorSettings::register(cx);
});

View File

@@ -1,12 +1,12 @@
use anyhow::{Context as _, Result};
use collections::HashMap;
use gpui::{App, BackgroundExecutor, BorrowAppContext, Global};
use log::info;
#[cfg(not(any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd")))]
mod non_windows_and_freebsd_deps {
pub(super) use gpui::AsyncApp;
pub(super) use libwebrtc::native::apm;
pub(super) use log::info;
pub(super) use parking_lot::Mutex;
pub(super) use rodio::cpal::Sample;
pub(super) use rodio::source::LimitSettings;

View File

@@ -42,7 +42,7 @@ pub struct AudioSettings {
/// Configuration of audio in Zed
impl Settings for AudioSettings {
fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
fn from_settings(content: &settings::SettingsContent) -> Self {
let audio = &content.audio.as_ref().unwrap();
AudioSettings {
rodio_audio: audio.rodio_audio.unwrap(),

View File

@@ -127,7 +127,7 @@ struct AutoUpdateSetting(bool);
///
/// Default: true
impl Settings for AutoUpdateSetting {
fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
fn from_settings(content: &settings::SettingsContent) -> Self {
Self(content.auto_update.unwrap())
}
}
@@ -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

@@ -1,5 +1,4 @@
use std::{
os::windows::process::CommandExt,
path::Path,
time::{Duration, Instant},
};
@@ -7,7 +6,6 @@ use std::{
use anyhow::{Context as _, Result};
use windows::Win32::{
Foundation::{HWND, LPARAM, WPARAM},
System::Threading::CREATE_NEW_PROCESS_GROUP,
UI::WindowsAndMessaging::PostMessageW,
};
@@ -207,9 +205,7 @@ pub(crate) fn perform_update(app_dir: &Path, hwnd: Option<isize>, launch: bool)
}
if launch {
#[allow(clippy::disallowed_methods, reason = "doesn't run in the main binary")]
let _ = std::process::Command::new(app_dir.join("Zed.exe"))
.creation_flags(CREATE_NEW_PROCESS_GROUP.0)
.spawn();
let _ = std::process::Command::new(app_dir.join("Zed.exe")).spawn();
}
log::info!("Update completed successfully");
Ok(())

View File

@@ -1,4 +1,3 @@
use gpui::App;
use settings::Settings;
#[derive(Debug)]
@@ -8,17 +7,11 @@ pub struct CallSettings {
}
impl Settings for CallSettings {
fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
fn from_settings(content: &settings::SettingsContent) -> Self {
let call = content.calls.clone().unwrap();
CallSettings {
mute_on_join: call.mute_on_join.unwrap(),
share_on_join: call.share_on_join.unwrap(),
}
}
fn import_from_vscode(
_vscode: &settings::VsCodeSettings,
_current: &mut settings::SettingsContent,
) {
}
}

View File

@@ -731,15 +731,15 @@ mod windows {
Storage::FileSystem::{
CreateFileW, FILE_FLAGS_AND_ATTRIBUTES, FILE_SHARE_MODE, OPEN_EXISTING, WriteFile,
},
System::Threading::{CREATE_NEW_PROCESS_GROUP, CreateMutexW},
System::Threading::CreateMutexW,
},
core::HSTRING,
};
use crate::{Detect, InstalledApp};
use std::io;
use std::path::{Path, PathBuf};
use std::process::ExitStatus;
use std::{io, os::windows::process::CommandExt};
fn check_single_instance() -> bool {
let mutex = unsafe {
@@ -778,7 +778,6 @@ mod windows {
fn launch(&self, ipc_url: String) -> anyhow::Result<()> {
if check_single_instance() {
std::process::Command::new(self.0.clone())
.creation_flags(CREATE_NEW_PROCESS_GROUP.0)
.arg(ipc_url)
.spawn()?;
} else {

View File

@@ -101,7 +101,7 @@ pub struct ClientSettings {
}
impl Settings for ClientSettings {
fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
fn from_settings(content: &settings::SettingsContent) -> Self {
if let Some(server_url) = &*ZED_SERVER_URL {
return Self {
server_url: server_url.clone(),
@@ -133,7 +133,7 @@ impl ProxySettings {
}
impl Settings for ProxySettings {
fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
fn from_settings(content: &settings::SettingsContent) -> Self {
Self {
proxy: content.proxy.clone(),
}
@@ -519,7 +519,7 @@ pub struct TelemetrySettings {
}
impl settings::Settings for TelemetrySettings {
fn from_settings(content: &SettingsContent, _cx: &mut App) -> Self {
fn from_settings(content: &SettingsContent) -> Self {
Self {
diagnostics: content.telemetry.as_ref().unwrap().diagnostics.unwrap(),
metrics: content.telemetry.as_ref().unwrap().metrics.unwrap(),

View File

@@ -127,7 +127,6 @@ pub struct DeclarationScoreComponents {
pub declaration_count: usize,
pub reference_line_distance: u32,
pub declaration_line_distance: u32,
pub declaration_line_distance_rank: usize,
pub excerpt_vs_item_jaccard: f32,
pub excerpt_vs_signature_jaccard: f32,
pub adjacent_vs_item_jaccard: f32,
@@ -136,6 +135,15 @@ pub struct DeclarationScoreComponents {
pub excerpt_vs_signature_weighted_overlap: f32,
pub adjacent_vs_item_weighted_overlap: f32,
pub adjacent_vs_signature_weighted_overlap: f32,
pub path_import_match_count: usize,
pub wildcard_path_import_match_count: usize,
pub import_similarity: f32,
pub max_import_similarity: f32,
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)]

View File

@@ -0,0 +1,28 @@
[package]
name = "codestral"
version = "0.1.0"
edition = "2021"
publish = false
license = "GPL-3.0-or-later"
[lib]
path = "src/codestral.rs"
[dependencies]
anyhow.workspace = true
edit_prediction.workspace = true
edit_prediction_context.workspace = true
futures.workspace = true
gpui.workspace = true
http_client.workspace = true
language.workspace = true
language_models.workspace = true
log.workspace = true
mistral.workspace = true
serde.workspace = true
serde_json.workspace = true
smol.workspace = true
text.workspace = true
workspace-hack.workspace = true
[dev-dependencies]

View File

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

View File

@@ -0,0 +1,381 @@
use anyhow::{Context as _, Result};
use edit_prediction::{Direction, EditPrediction, EditPredictionProvider};
use edit_prediction_context::{EditPredictionExcerpt, EditPredictionExcerptOptions};
use futures::AsyncReadExt;
use gpui::{App, Context, Entity, Task};
use http_client::HttpClient;
use language::{
language_settings::all_language_settings, Anchor, Buffer, BufferSnapshot, EditPreview, ToPoint,
};
use language_models::MistralLanguageModelProvider;
use mistral::CODESTRAL_API_URL;
use serde::{Deserialize, Serialize};
use std::{
ops::Range,
sync::Arc,
time::{Duration, Instant},
};
use text::ToOffset;
pub const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(150);
const EXCERPT_OPTIONS: EditPredictionExcerptOptions = EditPredictionExcerptOptions {
max_bytes: 1050,
min_bytes: 525,
target_before_cursor_over_total_bytes: 0.66,
};
/// Represents a completion that has been received and processed from Codestral.
/// This struct maintains the state needed to interpolate the completion as the user types.
#[derive(Clone)]
struct CurrentCompletion {
/// The buffer snapshot at the time the completion was generated.
/// Used to detect changes and interpolate edits.
snapshot: BufferSnapshot,
/// The edits that should be applied to transform the original text into the predicted text.
/// Each edit is a range in the buffer and the text to replace it with.
edits: Arc<[(Range<Anchor>, String)]>,
/// Preview of how the buffer will look after applying the edits.
edit_preview: EditPreview,
}
impl CurrentCompletion {
/// Attempts to adjust the edits based on changes made to the buffer since the completion was generated.
/// Returns None if the user's edits conflict with the predicted edits.
fn interpolate(&self, new_snapshot: &BufferSnapshot) -> Option<Vec<(Range<Anchor>, String)>> {
edit_prediction::interpolate_edits(&self.snapshot, new_snapshot, &self.edits)
}
}
pub struct CodestralCompletionProvider {
http_client: Arc<dyn HttpClient>,
pending_request: Option<Task<Result<()>>>,
current_completion: Option<CurrentCompletion>,
}
impl CodestralCompletionProvider {
pub fn new(http_client: Arc<dyn HttpClient>) -> Self {
Self {
http_client,
pending_request: None,
current_completion: None,
}
}
pub fn has_api_key(cx: &App) -> bool {
Self::api_key(cx).is_some()
}
fn api_key(cx: &App) -> Option<Arc<str>> {
MistralLanguageModelProvider::try_global(cx)
.and_then(|provider| provider.codestral_api_key(CODESTRAL_API_URL, cx))
}
/// Uses Codestral's Fill-in-the-Middle API for code completion.
async fn fetch_completion(
http_client: Arc<dyn HttpClient>,
api_key: &str,
prompt: String,
suffix: String,
model: String,
max_tokens: Option<u32>,
) -> Result<String> {
let start_time = Instant::now();
log::debug!(
"Codestral: Requesting completion (model: {}, max_tokens: {:?})",
model,
max_tokens
);
let request = CodestralRequest {
model,
prompt,
suffix: if suffix.is_empty() {
None
} else {
Some(suffix)
},
max_tokens: max_tokens.or(Some(350)),
temperature: Some(0.2),
top_p: Some(1.0),
stream: Some(false),
stop: None,
random_seed: None,
min_tokens: None,
};
let request_body = serde_json::to_string(&request)?;
log::debug!("Codestral: Sending FIM request");
let http_request = http_client::Request::builder()
.method(http_client::Method::POST)
.uri(format!("{}/v1/fim/completions", CODESTRAL_API_URL))
.header("Content-Type", "application/json")
.header("Authorization", format!("Bearer {}", api_key))
.body(http_client::AsyncBody::from(request_body))?;
let mut response = http_client.send(http_request).await?;
let status = response.status();
log::debug!("Codestral: Response status: {}", status);
if !status.is_success() {
let mut body = String::new();
response.body_mut().read_to_string(&mut body).await?;
return Err(anyhow::anyhow!(
"Codestral API error: {} - {}",
status,
body
));
}
let mut body = String::new();
response.body_mut().read_to_string(&mut body).await?;
let codestral_response: CodestralResponse = serde_json::from_str(&body)?;
let elapsed = start_time.elapsed();
if let Some(choice) = codestral_response.choices.first() {
let completion = &choice.message.content;
log::debug!(
"Codestral: Completion received ({} tokens, {:.2}s)",
codestral_response.usage.completion_tokens,
elapsed.as_secs_f64()
);
// Return just the completion text for insertion at cursor
Ok(completion.clone())
} else {
log::error!("Codestral: No completion returned in response");
Err(anyhow::anyhow!("No completion returned from Codestral"))
}
}
}
impl EditPredictionProvider for CodestralCompletionProvider {
fn name() -> &'static str {
"codestral"
}
fn display_name() -> &'static str {
"Codestral"
}
fn show_completions_in_menu() -> bool {
true
}
fn is_enabled(&self, _buffer: &Entity<Buffer>, _cursor_position: Anchor, cx: &App) -> bool {
Self::api_key(cx).is_some()
}
fn is_refreshing(&self) -> bool {
self.pending_request.is_some()
}
fn refresh(
&mut self,
buffer: Entity<Buffer>,
cursor_position: language::Anchor,
debounce: bool,
cx: &mut Context<Self>,
) {
log::debug!("Codestral: Refresh called (debounce: {})", debounce);
let Some(api_key) = Self::api_key(cx) else {
log::warn!("Codestral: No API key configured, skipping refresh");
return;
};
let snapshot = buffer.read(cx).snapshot();
// Check if current completion is still valid
if let Some(current_completion) = self.current_completion.as_ref() {
if current_completion.interpolate(&snapshot).is_some() {
return;
}
}
let http_client = self.http_client.clone();
// Get settings
let settings = all_language_settings(None, cx);
let model = settings
.edit_predictions
.codestral
.model
.clone()
.unwrap_or_else(|| "codestral-latest".to_string());
let max_tokens = settings.edit_predictions.codestral.max_tokens;
self.pending_request = Some(cx.spawn(async move |this, cx| {
if debounce {
log::debug!("Codestral: Debouncing for {:?}", DEBOUNCE_TIMEOUT);
smol::Timer::after(DEBOUNCE_TIMEOUT).await;
}
let cursor_offset = cursor_position.to_offset(&snapshot);
let cursor_point = cursor_offset.to_point(&snapshot);
let excerpt = EditPredictionExcerpt::select_from_buffer(
cursor_point,
&snapshot,
&EXCERPT_OPTIONS,
None,
)
.context("Line containing cursor doesn't fit in excerpt max bytes")?;
let excerpt_text = excerpt.text(&snapshot);
let cursor_within_excerpt = cursor_offset
.saturating_sub(excerpt.range.start)
.min(excerpt_text.body.len());
let prompt = excerpt_text.body[..cursor_within_excerpt].to_string();
let suffix = excerpt_text.body[cursor_within_excerpt..].to_string();
let completion_text = match Self::fetch_completion(
http_client,
&api_key,
prompt,
suffix,
model,
max_tokens,
)
.await
{
Ok(completion) => completion,
Err(e) => {
log::error!("Codestral: Failed to fetch completion: {}", e);
this.update(cx, |this, cx| {
this.pending_request = None;
cx.notify();
})?;
return Err(e);
}
};
if completion_text.trim().is_empty() {
log::debug!("Codestral: Completion was empty after trimming; ignoring");
this.update(cx, |this, cx| {
this.pending_request = None;
cx.notify();
})?;
return Ok(());
}
let edits: Arc<[(Range<Anchor>, String)]> =
vec![(cursor_position..cursor_position, completion_text)].into();
let edit_preview = buffer
.read_with(cx, |buffer, cx| buffer.preview_edits(edits.clone(), cx))?
.await;
this.update(cx, |this, cx| {
this.current_completion = Some(CurrentCompletion {
snapshot,
edits,
edit_preview,
});
this.pending_request = None;
cx.notify();
})?;
Ok(())
}));
}
fn cycle(
&mut self,
_buffer: Entity<Buffer>,
_cursor_position: Anchor,
_direction: Direction,
_cx: &mut Context<Self>,
) {
// Codestral doesn't support multiple completions, so cycling does nothing
}
fn accept(&mut self, _cx: &mut Context<Self>) {
log::debug!("Codestral: Completion accepted");
self.pending_request = None;
self.current_completion = None;
}
fn discard(&mut self, _cx: &mut Context<Self>) {
log::debug!("Codestral: Completion discarded");
self.pending_request = None;
self.current_completion = None;
}
/// Returns the completion suggestion, adjusted or invalidated based on user edits
fn suggest(
&mut self,
buffer: &Entity<Buffer>,
_cursor_position: Anchor,
cx: &mut Context<Self>,
) -> Option<EditPrediction> {
let current_completion = self.current_completion.as_ref()?;
let buffer = buffer.read(cx);
let edits = current_completion.interpolate(&buffer.snapshot())?;
if edits.is_empty() {
return None;
}
Some(EditPrediction::Local {
id: None,
edits,
edit_preview: Some(current_completion.edit_preview.clone()),
})
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct CodestralRequest {
pub model: String,
pub prompt: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub suffix: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_tokens: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub temperature: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub top_p: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stream: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stop: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub random_seed: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub min_tokens: Option<u32>,
}
#[derive(Debug, Deserialize)]
pub struct CodestralResponse {
pub id: String,
pub object: String,
pub model: String,
pub usage: Usage,
pub created: u64,
pub choices: Vec<Choice>,
}
#[derive(Debug, Deserialize)]
pub struct Usage {
pub prompt_tokens: u32,
pub completion_tokens: u32,
pub total_tokens: u32,
}
#[derive(Debug, Deserialize)]
pub struct Choice {
pub index: u32,
pub message: Message,
pub finish_reason: String,
}
#[derive(Debug, Deserialize)]
pub struct Message {
pub content: String,
pub role: String,
}

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

@@ -323,8 +323,8 @@ fn assert_remote_selections(
let CollaboratorId::PeerId(peer_id) = s.collaborator_id else {
panic!("unexpected collaborator id");
};
let start = s.selection.start.to_offset(&snapshot.buffer_snapshot);
let end = s.selection.end.to_offset(&snapshot.buffer_snapshot);
let start = s.selection.start.to_offset(snapshot.buffer_snapshot());
let end = s.selection.end.to_offset(snapshot.buffer_snapshot());
let user_id = collaborators.get(&peer_id).unwrap().user_id;
let participant_index = hub.user_participant_indices(cx).get(&user_id).copied();
(participant_index, start..end)

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,
@@ -1272,7 +1272,7 @@ async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut Tes
fake_language_server.start_progress("the-token").await;
executor.advance_clock(SERVER_PROGRESS_THROTTLE_TIMEOUT);
fake_language_server.notify::<lsp::notification::Progress>(&lsp::ProgressParams {
fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
token: lsp::NumberOrString::String("the-token".to_string()),
value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
lsp::WorkDoneProgressReport {
@@ -1306,7 +1306,7 @@ async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut Tes
});
executor.advance_clock(SERVER_PROGRESS_THROTTLE_TIMEOUT);
fake_language_server.notify::<lsp::notification::Progress>(&lsp::ProgressParams {
fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
token: lsp::NumberOrString::String("the-token".to_string()),
value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
lsp::WorkDoneProgressReport {
@@ -2041,6 +2041,10 @@ async fn test_mutual_editor_inlay_hint_cache_update(
});
}
// This test started hanging on seed 2 after the theme settings
// PR. The hypothesis is that it's been buggy for a while, but got lucky
// on seeds.
#[ignore]
#[gpui::test(iterations = 10)]
async fn test_inlay_hint_refresh_is_forwarded(
cx_a: &mut TestAppContext,
@@ -2405,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!(
@@ -2844,7 +2849,7 @@ async fn test_lsp_pull_diagnostics(
});
fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
&lsp::PublishDiagnosticsParams {
lsp::PublishDiagnosticsParams {
uri: lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
diagnostics: vec![lsp::Diagnostic {
range: lsp::Range {
@@ -2865,7 +2870,7 @@ async fn test_lsp_pull_diagnostics(
},
);
fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
&lsp::PublishDiagnosticsParams {
lsp::PublishDiagnosticsParams {
uri: lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap(),
diagnostics: vec![lsp::Diagnostic {
range: lsp::Range {
@@ -2887,7 +2892,7 @@ async fn test_lsp_pull_diagnostics(
);
if should_stream_workspace_diagnostic {
fake_language_server.notify::<lsp::notification::Progress>(&lsp::ProgressParams {
fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
token: expected_workspace_diagnostic_token.clone(),
value: lsp::ProgressParamsValue::WorkspaceDiagnostic(
lsp::WorkspaceDiagnosticReportResult::Report(lsp::WorkspaceDiagnosticReport {
@@ -3069,7 +3074,7 @@ async fn test_lsp_pull_diagnostics(
});
if should_stream_workspace_diagnostic {
fake_language_server.notify::<lsp::notification::Progress>(&lsp::ProgressParams {
fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
token: expected_workspace_diagnostic_token.clone(),
value: lsp::ProgressParamsValue::WorkspaceDiagnostic(
lsp::WorkspaceDiagnosticReportResult::Report(lsp::WorkspaceDiagnosticReport {

View File

@@ -84,7 +84,11 @@ async fn test_project_diff(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext)
diff.update(cx_b, |diff, cx| {
assert_eq!(
diff.excerpt_paths(cx),
vec!["changed.txt", "deleted.txt", "created.txt"]
vec![
rel_path("changed.txt").into_arc(),
rel_path("deleted.txt").into_arc(),
rel_path("created.txt").into_arc()
]
);
});
@@ -121,7 +125,11 @@ async fn test_project_diff(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext)
diff.update(cx_b, |diff, cx| {
assert_eq!(
diff.excerpt_paths(cx),
vec!["deleted.txt", "unchanged.txt", "created.txt"]
vec![
rel_path("deleted.txt").into_arc(),
rel_path("unchanged.txt").into_arc(),
rel_path("created.txt").into_arc()
]
);
});
}

View File

@@ -4077,7 +4077,7 @@ async fn test_collaborating_with_diagnostics(
.receive_notification::<lsp::notification::DidOpenTextDocument>()
.await;
fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
&lsp::PublishDiagnosticsParams {
lsp::PublishDiagnosticsParams {
uri: lsp::Uri::from_file_path(path!("/a/a.rs")).unwrap(),
version: None,
diagnostics: vec![lsp::Diagnostic {
@@ -4097,7 +4097,7 @@ async fn test_collaborating_with_diagnostics(
.await
.unwrap();
fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
&lsp::PublishDiagnosticsParams {
lsp::PublishDiagnosticsParams {
uri: lsp::Uri::from_file_path(path!("/a/a.rs")).unwrap(),
version: None,
diagnostics: vec![lsp::Diagnostic {
@@ -4171,7 +4171,7 @@ async fn test_collaborating_with_diagnostics(
// Simulate a language server reporting more errors for a file.
fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
&lsp::PublishDiagnosticsParams {
lsp::PublishDiagnosticsParams {
uri: lsp::Uri::from_file_path(path!("/a/a.rs")).unwrap(),
version: None,
diagnostics: vec![
@@ -4269,7 +4269,7 @@ async fn test_collaborating_with_diagnostics(
// Simulate a language server reporting no errors for a file.
fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
&lsp::PublishDiagnosticsParams {
lsp::PublishDiagnosticsParams {
uri: lsp::Uri::from_file_path(path!("/a/a.rs")).unwrap(),
version: None,
diagnostics: Vec::new(),
@@ -4365,7 +4365,7 @@ async fn test_collaborating_with_lsp_progress_updates_and_diagnostics_ordering(
.await
.into_response()
.unwrap();
fake_language_server.notify::<lsp::notification::Progress>(&lsp::ProgressParams {
fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Begin(
lsp::WorkDoneProgressBegin {
@@ -4376,7 +4376,7 @@ async fn test_collaborating_with_lsp_progress_updates_and_diagnostics_ordering(
});
for file_name in file_names {
fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
&lsp::PublishDiagnosticsParams {
lsp::PublishDiagnosticsParams {
uri: lsp::Uri::from_file_path(Path::new(path!("/test")).join(file_name)).unwrap(),
version: None,
diagnostics: vec![lsp::Diagnostic {
@@ -4389,7 +4389,7 @@ async fn test_collaborating_with_lsp_progress_updates_and_diagnostics_ordering(
},
);
}
fake_language_server.notify::<lsp::notification::Progress>(&lsp::ProgressParams {
fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::End(
lsp::WorkDoneProgressEnd { message: None },

View File

@@ -183,9 +183,10 @@ pub async fn run_randomized_test<T: RandomizedTest>(
for (client, cx) in clients {
cx.update(|cx| {
let store = cx.remove_global::<SettingsStore>();
let settings = cx.remove_global::<SettingsStore>();
cx.clear_globals();
cx.set_global(store);
cx.set_global(settings);
theme::init(theme::LoadThemes::JustBase, cx);
drop(client);
});
}

View File

@@ -172,6 +172,7 @@ impl TestServer {
}
let settings = SettingsStore::test(cx);
cx.set_global(settings);
theme::init(theme::LoadThemes::JustBase, cx);
release_channel::init(SemanticVersion::default(), cx);
client::init_settings(cx);
});

View File

@@ -248,7 +248,7 @@ impl ChannelView {
.editor
.update(cx, |editor, cx| editor.snapshot(window, cx));
if let Some(outline) = snapshot.buffer_snapshot.outline(None)
if let Some(outline) = snapshot.buffer_snapshot().outline(None)
&& let Some(item) = outline
.items
.iter()
@@ -305,7 +305,7 @@ impl ChannelView {
let mut closest_heading = None;
if let Some(outline) = snapshot.buffer_snapshot.outline(None) {
if let Some(outline) = snapshot.buffer_snapshot().outline(None) {
for item in outline.items {
if item.range.start.to_display_point(&snapshot) > position {
break;
@@ -508,10 +508,6 @@ impl Item for ChannelView {
}))
}
fn is_singleton(&self, _cx: &App) -> bool {
false
}
fn navigate(
&mut self,
data: Box<dyn Any>,

View File

@@ -18,7 +18,7 @@ pub struct NotificationPanelSettings {
}
impl Settings for CollaborationPanelSettings {
fn from_settings(content: &settings::SettingsContent, _cx: &mut ui::App) -> Self {
fn from_settings(content: &settings::SettingsContent) -> Self {
let panel = content.collaboration_panel.as_ref().unwrap();
Self {
@@ -30,7 +30,7 @@ impl Settings for CollaborationPanelSettings {
}
impl Settings for NotificationPanelSettings {
fn from_settings(content: &settings::SettingsContent, _cx: &mut ui::App) -> Self {
fn from_settings(content: &settings::SettingsContent) -> Self {
let panel = content.notification_panel.as_ref().unwrap();
return Self {
button: panel.button.unwrap(),

View File

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

View File

@@ -97,11 +97,10 @@ impl CommandPaletteFilter {
pub struct CommandInterceptResult {
/// The action produced as a result of the interception.
pub action: Box<dyn Action>,
// TODO: Document this field.
#[allow(missing_docs)]
/// The display string to show in the command palette for this result.
pub string: String,
// TODO: Document this field.
#[allow(missing_docs)]
/// The character positions in the string that match the query.
/// Used for highlighting matched characters in the command palette UI.
pub positions: Vec<usize>,
}

View File

@@ -41,12 +41,9 @@ impl StdioTransport {
command.current_dir(working_directory);
}
let mut server = command.spawn().with_context(|| {
format!(
"failed to spawn command. (path={:?}, args={:?})",
binary.executable, &binary.args
)
})?;
let mut server = command
.spawn()
.with_context(|| format!("failed to spawn command {command:?})",))?;
let stdin = server.stdin.take().unwrap();
let stdout = server.stdout.take().unwrap();

View File

@@ -270,7 +270,7 @@ impl RegisteredBuffer {
server
.lsp
.notify::<lsp::notification::DidChangeTextDocument>(
&lsp::DidChangeTextDocumentParams {
lsp::DidChangeTextDocumentParams {
text_document: lsp::VersionedTextDocumentIdentifier::new(
buffer.uri.clone(),
buffer.snapshot_version,
@@ -744,7 +744,7 @@ impl Copilot {
let snapshot = buffer.read(cx).snapshot();
server
.notify::<lsp::notification::DidOpenTextDocument>(
&lsp::DidOpenTextDocumentParams {
lsp::DidOpenTextDocumentParams {
text_document: lsp::TextDocumentItem {
uri: uri.clone(),
language_id: language_id.clone(),
@@ -792,13 +792,14 @@ impl Copilot {
server
.lsp
.notify::<lsp::notification::DidSaveTextDocument>(
&lsp::DidSaveTextDocumentParams {
lsp::DidSaveTextDocumentParams {
text_document: lsp::TextDocumentIdentifier::new(
registered_buffer.uri.clone(),
),
text: None,
},
)?;
)
.ok();
}
language::BufferEvent::FileHandleChanged
| language::BufferEvent::LanguageChanged => {
@@ -814,14 +815,15 @@ impl Copilot {
server
.lsp
.notify::<lsp::notification::DidCloseTextDocument>(
&lsp::DidCloseTextDocumentParams {
lsp::DidCloseTextDocumentParams {
text_document: lsp::TextDocumentIdentifier::new(old_uri),
},
)?;
)
.ok();
server
.lsp
.notify::<lsp::notification::DidOpenTextDocument>(
&lsp::DidOpenTextDocumentParams {
lsp::DidOpenTextDocumentParams {
text_document: lsp::TextDocumentItem::new(
registered_buffer.uri.clone(),
registered_buffer.language_id.clone(),
@@ -829,7 +831,8 @@ impl Copilot {
registered_buffer.snapshot.text(),
),
},
)?;
)
.ok();
}
}
_ => {}
@@ -846,7 +849,7 @@ impl Copilot {
server
.lsp
.notify::<lsp::notification::DidCloseTextDocument>(
&lsp::DidCloseTextDocumentParams {
lsp::DidCloseTextDocumentParams {
text_document: lsp::TextDocumentIdentifier::new(buffer.uri),
},
)
@@ -1151,9 +1154,12 @@ fn notify_did_change_config_to_server(
}
});
server.notify::<lsp::notification::DidChangeConfiguration>(&lsp::DidChangeConfigurationParams {
settings,
})
server
.notify::<lsp::notification::DidChangeConfiguration>(lsp::DidChangeConfigurationParams {
settings,
})
.ok();
Ok(())
}
async fn clear_copilot_dir() {

View File

@@ -46,6 +46,7 @@ pub trait DapDelegate: Send + Sync + 'static {
async fn which(&self, command: &OsStr) -> Option<PathBuf>;
async fn read_text_file(&self, path: &RelPath) -> Result<String>;
async fn shell_env(&self) -> collections::HashMap<String, String>;
fn is_headless(&self) -> bool;
}
#[derive(

View File

@@ -1,5 +1,4 @@
use dap_types::SteppingGranularity;
use gpui::App;
use settings::{Settings, SettingsContent};
pub struct DebuggerSettings {
@@ -34,7 +33,7 @@ pub struct DebuggerSettings {
}
impl Settings for DebuggerSettings {
fn from_settings(content: &SettingsContent, _cx: &mut App) -> Self {
fn from_settings(content: &SettingsContent) -> Self {
let content = content.debugger.clone().unwrap();
Self {
stepping_granularity: dap_granularity_from_settings(

View File

@@ -674,13 +674,7 @@ impl StdioTransport {
command.args(&binary.arguments);
command.envs(&binary.envs);
let mut process = Child::spawn(command, Stdio::piped()).with_context(|| {
format!(
"failed to spawn command `{} {}`.",
binary_command,
binary.arguments.join(" ")
)
})?;
let mut process = Child::spawn(command, Stdio::piped())?;
let err_task = process.stderr.take().map(|stderr| {
cx.background_spawn(TransportDelegate::handle_adapter_log(
@@ -1058,11 +1052,13 @@ impl Child {
#[cfg(not(windows))]
fn spawn(mut command: std::process::Command, stdin: Stdio) -> Result<Self> {
util::set_pre_exec_to_start_new_session(&mut command);
let process = smol::process::Command::from(command)
let mut command = smol::process::Command::from(command);
let process = command
.stdin(stdin)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
.spawn()
.with_context(|| format!("failed to spawn command `{command:?}`",))?;
Ok(Self { process })
}
@@ -1070,11 +1066,13 @@ impl Child {
fn spawn(command: std::process::Command, stdin: Stdio) -> Result<Self> {
// TODO(windows): create a job object and add the child process handle to it,
// see https://learn.microsoft.com/en-us/windows/win32/procthread/job-objects
let process = smol::process::Command::from(command)
let mut command = smol::process::Command::from(command);
let process = command
.stdin(stdin)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
.spawn()
.with_context(|| format!("failed to spawn command `{command:?}`",))?;
Ok(Self { process })
}

View File

@@ -120,6 +120,13 @@ impl JsDebugAdapter {
configuration
.entry("sourceMapRenames")
.or_insert(true.into());
// Set up remote browser debugging
if delegate.is_headless() {
configuration
.entry("browserLaunchLocation")
.or_insert("ui".into());
}
}
let adapter_path = if let Some(user_installed_path) = user_installed_path {

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

@@ -41,8 +41,8 @@ use serde_json::Value;
use settings::Settings;
use stack_frame_list::StackFrameList;
use task::{
BuildTaskDefinition, DebugScenario, ShellBuilder, SpawnInTerminal, TaskContext, ZedDebugConfig,
substitute_variables_in_str,
BuildTaskDefinition, DebugScenario, Shell, ShellBuilder, SpawnInTerminal, TaskContext,
ZedDebugConfig, substitute_variables_in_str,
};
use terminal_view::TerminalView;
use ui::{
@@ -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()
@@ -988,7 +989,7 @@ impl RunningState {
(task, None)
}
};
let Some(task) = task_template.resolve_task("debug-build-task", &task_context) else {
let Some(mut task) = task_template.resolve_task("debug-build-task", &task_context) else {
anyhow::bail!("Could not resolve task variables within a debug scenario");
};
@@ -1025,7 +1026,11 @@ impl RunningState {
None
};
let builder = ShellBuilder::new(remote_shell.as_deref(), &task.resolved.shell);
if let Some(remote_shell) = remote_shell && task.resolved.shell == Shell::System {
task.resolved.shell = Shell::Program(remote_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);
@@ -1228,7 +1233,6 @@ impl RunningState {
terminal.read_with(cx, |terminal, _| {
terminal
.pty_info
.pid()
.map(|pid| pid.as_u32())
.context("Terminal was spawned but PID was not available")

View File

@@ -59,7 +59,7 @@ impl StackTraceView {
editor
.snapshot(window, cx)
.buffer_snapshot
.buffer_snapshot()
.excerpt_containing(position..position)
.map(|excerpt| excerpt.id())
});
@@ -259,7 +259,7 @@ impl StackTraceView {
let mut is_first = true;
for (_, highlight) in self.highlights.iter().skip(active_idx) {
let position = highlight.to_point(&snapshot.buffer_snapshot);
let position = highlight.to_point(&snapshot.buffer_snapshot());
let color = if is_first {
is_first = false;
first_color
@@ -268,11 +268,11 @@ impl StackTraceView {
};
let start = snapshot
.buffer_snapshot
.buffer_snapshot()
.clip_point(Point::new(position.row, 0), Bias::Left);
let end = start + Point::new(1, 0);
let start = snapshot.buffer_snapshot.anchor_before(start);
let end = snapshot.buffer_snapshot.anchor_before(end);
let start = snapshot.buffer_snapshot().anchor_before(start);
let end = snapshot.buffer_snapshot().anchor_before(end);
editor.highlight_rows::<DebugStackFrameLine>(
start..end,
color,
@@ -354,10 +354,6 @@ impl Item for StackTraceView {
self.editor.for_each_project_item(cx, f)
}
fn is_singleton(&self, _: &App) -> bool {
false
}
fn set_nav_history(
&mut self,
nav_history: ItemNavHistory,

View File

@@ -1604,7 +1604,7 @@ async fn test_active_debug_line_setting(executor: BackgroundExecutor, cx: &mut T
let point = editor
.snapshot(window, cx)
.buffer_snapshot
.buffer_snapshot()
.summary_for_anchor::<language::Point>(&active_debug_lines.first().unwrap().0.start);
assert_eq!(point.row, 1);
@@ -1679,7 +1679,7 @@ async fn test_active_debug_line_setting(executor: BackgroundExecutor, cx: &mut T
let point = editor
.snapshot(window, cx)
.buffer_snapshot
.buffer_snapshot()
.summary_for_anchor::<language::Point>(&active_debug_lines.first().unwrap().0.start);
assert_eq!(point.row, 2);

View File

@@ -341,8 +341,8 @@ async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppC
editor
.highlighted_rows::<editor::ActiveDebugLine>()
.map(|(range, _)| {
let start = range.start.to_point(&snapshot.buffer_snapshot);
let end = range.end.to_point(&snapshot.buffer_snapshot);
let start = range.start.to_point(&snapshot.buffer_snapshot());
let end = range.end.to_point(&snapshot.buffer_snapshot());
start.row..end.row
})
.collect::<Vec<_>>()
@@ -404,8 +404,8 @@ async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppC
editor
.highlighted_rows::<editor::ActiveDebugLine>()
.map(|(range, _)| {
let start = range.start.to_point(&snapshot.buffer_snapshot);
let end = range.end.to_point(&snapshot.buffer_snapshot);
let start = range.start.to_point(&snapshot.buffer_snapshot());
let end = range.end.to_point(&snapshot.buffer_snapshot());
start.row..end.row
})
.collect::<Vec<_>>()

View File

@@ -730,10 +730,6 @@ impl Item for BufferDiagnosticsEditor {
self.multibuffer.read(cx).is_dirty(cx)
}
fn is_singleton(&self, _cx: &App) -> bool {
false
}
fn navigate(
&mut self,
data: Box<dyn Any>,

View File

@@ -138,7 +138,7 @@ impl editor::DiagnosticRenderer for DiagnosticRenderer {
BlockProperties {
placement: BlockPlacement::Near(
snapshot
.buffer_snapshot
.buffer_snapshot()
.anchor_after(block.initial_range.start),
),
height: Some(1),
@@ -278,7 +278,7 @@ impl DiagnosticBlock {
}
} else if let Some(diagnostic) = editor
.snapshot(window, cx)
.buffer_snapshot
.buffer_snapshot()
.diagnostic_group(buffer_id, group_id)
.nth(ix)
{

View File

@@ -716,10 +716,6 @@ impl Item for ProjectDiagnosticsEditor {
self.editor.for_each_project_item(cx, f)
}
fn is_singleton(&self, _: &App) -> bool {
false
}
fn set_nav_history(
&mut self,
nav_history: ItemNavHistory,
@@ -969,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.
@@ -984,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) {
@@ -1001,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

@@ -863,20 +863,20 @@ async fn test_random_diagnostics_with_inlays(cx: &mut TestAppContext, mut rng: S
21..=50 => mutated_diagnostics.update_in(cx, |diagnostics, window, cx| {
diagnostics.editor.update(cx, |editor, cx| {
let snapshot = editor.snapshot(window, cx);
if !snapshot.buffer_snapshot.is_empty() {
let position = rng.random_range(0..snapshot.buffer_snapshot.len());
let position = snapshot.buffer_snapshot.clip_offset(position, Bias::Left);
if !snapshot.buffer_snapshot().is_empty() {
let position = rng.random_range(0..snapshot.buffer_snapshot().len());
let position = snapshot.buffer_snapshot().clip_offset(position, Bias::Left);
log::info!(
"adding inlay at {position}/{}: {:?}",
snapshot.buffer_snapshot.len(),
snapshot.buffer_snapshot.text(),
snapshot.buffer_snapshot().len(),
snapshot.buffer_snapshot().text(),
);
editor.splice_inlays(
&[],
vec![Inlay::edit_prediction(
post_inc(&mut next_inlay_id),
snapshot.buffer_snapshot.anchor_before(position),
snapshot.buffer_snapshot().anchor_before(position),
Rope::from_iter(["Test inlay ", "next_inlay_id"]),
)],
cx,

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

@@ -2,7 +2,7 @@ use std::ops::Range;
use client::EditPredictionUsage;
use gpui::{App, Context, Entity, SharedString};
use language::Buffer;
use language::{Anchor, Buffer, BufferSnapshot, OffsetRangeExt};
// TODO: Find a better home for `Direction`.
//
@@ -242,3 +242,51 @@ where
self.update(cx, |this, cx| this.suggest(buffer, cursor_position, cx))
}
}
/// Returns edits updated based on user edits since the old snapshot. None is returned if any user
/// edit is not a prefix of a predicted insertion.
pub fn interpolate_edits(
old_snapshot: &BufferSnapshot,
new_snapshot: &BufferSnapshot,
current_edits: &[(Range<Anchor>, String)],
) -> Option<Vec<(Range<Anchor>, String)>> {
let mut edits = Vec::new();
let mut model_edits = current_edits.iter().peekable();
for user_edit in new_snapshot.edits_since::<usize>(&old_snapshot.version) {
while let Some((model_old_range, _)) = model_edits.peek() {
let model_old_range = model_old_range.to_offset(old_snapshot);
if model_old_range.end < user_edit.old.start {
let (model_old_range, model_new_text) = model_edits.next().unwrap();
edits.push((model_old_range.clone(), model_new_text.clone()));
} else {
break;
}
}
if let Some((model_old_range, model_new_text)) = model_edits.peek() {
let model_old_offset_range = model_old_range.to_offset(old_snapshot);
if user_edit.old == model_old_offset_range {
let user_new_text = new_snapshot
.text_for_range(user_edit.new.clone())
.collect::<String>();
if let Some(model_suffix) = model_new_text.strip_prefix(&user_new_text) {
if !model_suffix.is_empty() {
let anchor = old_snapshot.anchor_after(user_edit.old.end);
edits.push((anchor..anchor, model_suffix.to_string()));
}
model_edits.next();
continue;
}
}
}
return None;
}
edits.extend(model_edits.cloned());
if edits.is_empty() { None } else { Some(edits) }
}

View File

@@ -16,6 +16,7 @@ doctest = false
anyhow.workspace = true
client.workspace = true
cloud_llm_client.workspace = true
codestral.workspace = true
copilot.workspace = true
editor.workspace = true
feature_flags.workspace = true

View File

@@ -1,6 +1,7 @@
use anyhow::Result;
use client::{UserStore, zed_urls};
use cloud_llm_client::UsageLimit;
use codestral::CodestralCompletionProvider;
use copilot::{Copilot, Status};
use editor::{Editor, SelectionEffects, actions::ShowEditPrediction, scroll::Autoscroll};
use feature_flags::{FeatureFlagAppExt, PredictEditsRateCompletionsFeatureFlag};
@@ -234,6 +235,67 @@ impl Render for EditPredictionButton {
)
}
EditPredictionProvider::Codestral => {
let enabled = self.editor_enabled.unwrap_or(true);
let has_api_key = CodestralCompletionProvider::has_api_key(cx);
let fs = self.fs.clone();
let this = cx.entity();
div().child(
PopoverMenu::new("codestral")
.menu(move |window, cx| {
if has_api_key {
Some(this.update(cx, |this, cx| {
this.build_codestral_context_menu(window, cx)
}))
} else {
Some(ContextMenu::build(window, cx, |menu, _, _| {
let fs = fs.clone();
menu.entry("Use Zed AI instead", None, move |_, cx| {
set_completion_provider(
fs.clone(),
cx,
EditPredictionProvider::Zed,
)
})
.separator()
.entry(
"Configure Codestral API Key",
None,
move |window, cx| {
window.dispatch_action(
zed_actions::agent::OpenSettings.boxed_clone(),
cx,
);
},
)
}))
}
})
.anchor(Corner::BottomRight)
.trigger_with_tooltip(
IconButton::new("codestral-icon", IconName::AiMistral)
.shape(IconButtonShape::Square)
.when(!has_api_key, |this| {
this.indicator(Indicator::dot().color(Color::Error))
.indicator_border_color(Some(
cx.theme().colors().status_bar_background,
))
})
.when(has_api_key && !enabled, |this| {
this.indicator(Indicator::dot().color(Color::Ignored))
.indicator_border_color(Some(
cx.theme().colors().status_bar_background,
))
}),
move |window, cx| {
Tooltip::for_action("Codestral", &ToggleMenu, window, cx)
},
)
.with_handle(self.popover_menu_handle.clone()),
)
}
EditPredictionProvider::Zed => {
let enabled = self.editor_enabled.unwrap_or(true);
@@ -493,6 +555,7 @@ impl EditPredictionButton {
EditPredictionProvider::Zed
| EditPredictionProvider::Copilot
| EditPredictionProvider::Supermaven
| EditPredictionProvider::Codestral
) {
menu = menu
.separator()
@@ -719,6 +782,25 @@ impl EditPredictionButton {
})
}
fn build_codestral_context_menu(
&self,
window: &mut Window,
cx: &mut Context<Self>,
) -> Entity<ContextMenu> {
let fs = self.fs.clone();
ContextMenu::build(window, cx, |menu, window, cx| {
self.build_language_settings_menu(menu, window, cx)
.separator()
.entry("Use Zed AI instead", None, move |_, cx| {
set_completion_provider(fs.clone(), cx, EditPredictionProvider::Zed)
})
.separator()
.entry("Configure Codestral API Key", None, move |window, cx| {
window.dispatch_action(zed_actions::agent::OpenSettings.boxed_clone(), cx);
})
})
}
fn build_zeta_context_menu(
&self,
window: &mut Window,

View File

@@ -19,6 +19,7 @@ collections.workspace = true
futures.workspace = true
gpui.workspace = true
hashbrown.workspace = true
indoc.workspace = true
itertools.workspace = true
language.workspace = true
log.workspace = true
@@ -45,5 +46,8 @@ project = {workspace= true, features = ["test-support"]}
serde_json.workspace = true
settings = {workspace= true, features = ["test-support"]}
text = { workspace = true, features = ["test-support"] }
tree-sitter-c.workspace = true
tree-sitter-cpp.workspace = true
tree-sitter-go.workspace = true
util = { workspace = true, features = ["test-support"] }
zlog.workspace = true

View File

@@ -1,9 +1,11 @@
use language::LanguageId;
use language::{Language, LanguageId};
use project::ProjectEntryId;
use std::borrow::Cow;
use std::ops::Range;
use std::sync::Arc;
use std::{borrow::Cow, path::Path};
use text::{Bias, BufferId, Rope};
use util::paths::{path_ends_with, strip_path_suffix};
use util::rel_path::RelPath;
use crate::outline::OutlineDeclaration;
@@ -22,12 +24,14 @@ pub enum Declaration {
File {
project_entry_id: ProjectEntryId,
declaration: FileDeclaration,
cached_path: CachedDeclarationPath,
},
Buffer {
project_entry_id: ProjectEntryId,
buffer_id: BufferId,
rope: Rope,
declaration: BufferDeclaration,
cached_path: CachedDeclarationPath,
},
}
@@ -73,6 +77,13 @@ impl Declaration {
}
}
pub fn cached_path(&self) -> &CachedDeclarationPath {
match self {
Declaration::File { cached_path, .. } => cached_path,
Declaration::Buffer { cached_path, .. } => cached_path,
}
}
pub fn item_range(&self) -> Range<usize> {
match self {
Declaration::File { declaration, .. } => declaration.item_range.clone(),
@@ -235,3 +246,69 @@ impl BufferDeclaration {
}
}
}
#[derive(Debug, Clone)]
pub struct CachedDeclarationPath {
pub worktree_abs_path: Arc<Path>,
pub rel_path: Arc<RelPath>,
/// The relative path of the file, possibly stripped according to `import_path_strip_regex`.
pub rel_path_after_regex_stripping: Arc<RelPath>,
}
impl CachedDeclarationPath {
pub fn new(
worktree_abs_path: Arc<Path>,
path: &Arc<RelPath>,
language: Option<&Arc<Language>>,
) -> Self {
let rel_path = path.clone();
let rel_path_after_regex_stripping = if let Some(language) = language
&& let Some(strip_regex) = language.config().import_path_strip_regex.as_ref()
&& let Ok(stripped) = RelPath::unix(&Path::new(
strip_regex.replace_all(rel_path.as_unix_str(), "").as_ref(),
)) {
Arc::from(stripped)
} else {
rel_path.clone()
};
CachedDeclarationPath {
worktree_abs_path,
rel_path,
rel_path_after_regex_stripping,
}
}
#[cfg(test)]
pub fn new_for_test(worktree_abs_path: &str, rel_path: &str) -> Self {
let rel_path: Arc<RelPath> = util::rel_path::rel_path(rel_path).into();
CachedDeclarationPath {
worktree_abs_path: std::path::PathBuf::from(worktree_abs_path).into(),
rel_path_after_regex_stripping: rel_path.clone(),
rel_path,
}
}
pub fn ends_with_posix_path(&self, path: &Path) -> bool {
if path.as_os_str().len() <= self.rel_path_after_regex_stripping.as_unix_str().len() {
path_ends_with(self.rel_path_after_regex_stripping.as_std_path(), path)
} else {
if let Some(remaining) =
strip_path_suffix(path, self.rel_path_after_regex_stripping.as_std_path())
{
path_ends_with(&self.worktree_abs_path, remaining)
} else {
false
}
}
}
pub fn equals_absolute_path(&self, path: &Path) -> bool {
if let Some(remaining) =
strip_path_suffix(path, &self.rel_path_after_regex_stripping.as_std_path())
{
self.worktree_abs_path.as_ref() == remaining
} else {
false
}
}
}

View File

@@ -1,15 +1,17 @@
use cloud_llm_client::predict_edits_v3::DeclarationScoreComponents;
use collections::HashMap;
use itertools::Itertools as _;
use language::BufferSnapshot;
use ordered_float::OrderedFloat;
use project::ProjectEntryId;
use serde::Serialize;
use std::{cmp::Reverse, ops::Range};
use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc};
use strum::EnumIter;
use text::{Point, ToPoint};
use util::RangeExt as _;
use crate::{
Declaration, EditPredictionExcerpt, Identifier,
CachedDeclarationPath, Declaration, EditPredictionExcerpt, Identifier,
imports::{Import, Imports, Module},
reference::{Reference, ReferenceRegion},
syntax_index::SyntaxIndexState,
text_similarity::{Occurrences, jaccard_similarity, weighted_overlap_coefficient},
@@ -17,12 +19,17 @@ use crate::{
const MAX_IDENTIFIER_DECLARATION_COUNT: usize = 16;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EditPredictionScoreOptions {
pub omit_excerpt_overlaps: bool,
}
#[derive(Clone, Debug)]
pub struct ScoredDeclaration {
/// identifier used by the local reference
pub identifier: Identifier,
pub declaration: Declaration,
pub score_components: DeclarationScoreComponents,
pub scores: DeclarationScores,
pub components: DeclarationScoreComponents,
}
#[derive(EnumIter, Clone, Copy, PartialEq, Eq, Hash, Debug)]
@@ -31,15 +38,61 @@ pub enum DeclarationStyle {
Declaration,
}
#[derive(Clone, Debug, Serialize, Default)]
pub struct DeclarationScores {
pub signature: f32,
pub declaration: f32,
pub retrieval: f32,
}
impl ScoredDeclaration {
/// Returns the score for this declaration with the specified style.
pub fn score(&self, style: DeclarationStyle) -> f32 {
// TODO: handle truncation
// Score related to how likely this is the correct declaration, range 0 to 1
let retrieval = self.retrieval_score();
// Score related to the distance between the reference and cursor, range 0 to 1
let distance_score = if self.components.is_referenced_nearby {
1.0 / (1.0 + self.components.reference_line_distance as f32 / 10.0).powf(2.0)
} else {
// same score as ~14 lines away, rationale is to not overly penalize references from parent signatures
0.5
};
// For now instead of linear combination, the scores are just multiplied together.
let combined_score = 10.0 * retrieval * distance_score;
match style {
DeclarationStyle::Signature => self.scores.signature,
DeclarationStyle::Declaration => self.scores.declaration,
DeclarationStyle::Signature => {
combined_score * self.components.excerpt_vs_signature_weighted_overlap
}
DeclarationStyle::Declaration => {
2.0 * combined_score * self.components.excerpt_vs_item_weighted_overlap
}
}
}
pub fn retrieval_score(&self) -> f32 {
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
} else if self.components.wildcard_path_import_match_count > 0 {
1.0
} else if self.components.normalized_import_similarity > 0.0 {
self.components.normalized_import_similarity
} else if self.components.normalized_wildcard_import_similarity > 0.0 {
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 {
match &self.declaration {
Declaration::File { declaration, .. } => match style {
@@ -54,110 +107,259 @@ impl ScoredDeclaration {
}
pub fn score_density(&self, style: DeclarationStyle) -> f32 {
self.score(style) / (self.size(style)) as f32
self.score(style) / self.size(style) as f32
}
}
pub fn scored_declarations(
options: &EditPredictionScoreOptions,
index: &SyntaxIndexState,
excerpt: &EditPredictionExcerpt,
excerpt_occurrences: &Occurrences,
adjacent_occurrences: &Occurrences,
imports: &Imports,
identifier_to_references: HashMap<Identifier, Vec<Reference>>,
cursor_offset: usize,
current_buffer: &BufferSnapshot,
) -> Vec<ScoredDeclaration> {
let cursor_point = cursor_offset.to_point(&current_buffer);
let mut declarations = identifier_to_references
.into_iter()
.flat_map(|(identifier, references)| {
let declarations =
index.declarations_for_identifier::<MAX_IDENTIFIER_DECLARATION_COUNT>(&identifier);
let declaration_count = declarations.len();
let mut wildcard_import_occurrences = Vec::new();
let mut wildcard_import_paths = Vec::new();
for wildcard_import in imports.wildcard_modules.iter() {
match wildcard_import {
Module::Namespace(namespace) => {
wildcard_import_occurrences.push(namespace.occurrences())
}
Module::SourceExact(path) => wildcard_import_paths.push(path),
Module::SourceFuzzy(path) => {
wildcard_import_occurrences.push(Occurrences::from_path(&path))
}
}
}
declarations
.into_iter()
.filter_map(|(declaration_id, declaration)| match declaration {
Declaration::Buffer {
buffer_id,
declaration: buffer_declaration,
..
} => {
let is_same_file = buffer_id == &current_buffer.remote_id();
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 is_same_file {
let overlaps_excerpt =
range_intersection(&buffer_declaration.item_range, &excerpt.range)
.is_some();
if overlaps_excerpt
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))
}
}
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 { .. } => {}
}
}
}
}
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)
{
None
} else {
let declaration_line = buffer_declaration
.item_range
.start
.to_point(current_buffer)
.row;
Some((
true,
(cursor_point.row as i32 - declaration_line as i32)
.unsigned_abs(),
declaration,
))
}
} else {
Some((false, u32::MAX, declaration))
.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 { .. } => {
// We can assume that a file declaration is in a different file,
// because the current one must be open
Some((false, u32::MAX, declaration))
}
}
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)
})
.sorted_by_key(|&(_, distance, _)| distance)
.enumerate()
.map(
|(
declaration_line_distance_rank,
(is_same_file, declaration_line_distance, declaration),
)| {
let same_file_declaration_count = index.file_declaration_count(declaration);
.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,
});
}
score_declaration(
&identifier,
&references,
declaration.clone(),
is_same_file,
declaration_line_distance,
declaration_line_distance_rank,
same_file_declaration_count,
declaration_count,
&excerpt_occurrences,
&adjacent_occurrences,
cursor_point,
current_buffer,
)
},
)
.collect::<Vec<_>>()
})
.flatten()
.collect::<Vec<_>>();
let mut max_import_similarity = 0.0;
let mut max_wildcard_import_similarity = 0.0;
declarations.sort_unstable_by_key(|declaration| {
let score_density = declaration
.score_density(DeclarationStyle::Declaration)
.max(declaration.score_density(DeclarationStyle::Signature));
Reverse(OrderedFloat(score_density))
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;
}
if declaration.components.wildcard_import_similarity > max_wildcard_import_similarity {
max_wildcard_import_similarity = declaration.components.wildcard_import_similarity;
}
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> {
declaration: &'a Declaration,
same_file_line_distance: Option<u32>,
path_import_match_count: usize,
wildcard_path_import_match_count: usize,
}
fn declaration_path_matches_import(
declaration_path: &CachedDeclarationPath,
import_path: &Arc<Path>,
) -> bool {
if import_path.is_absolute() {
declaration_path.equals_absolute_path(import_path)
} else {
declaration_path.ends_with_posix_path(import_path)
}
}
fn range_intersection<T: Ord + Clone>(a: &Range<T>, b: &Range<T>) -> Option<Range<T>> {
@@ -173,17 +375,23 @@ fn range_intersection<T: Ord + Clone>(a: &Range<T>, b: &Range<T>) -> Option<Rang
fn score_declaration(
identifier: &Identifier,
references: &[Reference],
declaration: Declaration,
is_same_file: bool,
declaration_line_distance: u32,
declaration_line_distance_rank: usize,
checked_declaration: CheckedDeclaration,
same_file_declaration_count: usize,
declaration_count: usize,
excerpt_occurrences: &Occurrences,
adjacent_occurrences: &Occurrences,
import_occurrences: &[Occurrences],
wildcard_import_occurrences: &[Occurrences],
cursor: Point,
current_buffer: &BufferSnapshot,
) -> Option<ScoredDeclaration> {
) -> ScoredDeclaration {
let CheckedDeclaration {
declaration,
same_file_line_distance,
path_import_match_count,
wildcard_path_import_match_count,
} = checked_declaration;
let is_referenced_nearby = references
.iter()
.any(|r| r.region == ReferenceRegion::Nearby);
@@ -200,6 +408,9 @@ fn score_declaration(
.min()
.unwrap();
let is_same_file = same_file_line_distance.is_some();
let declaration_line_distance = same_file_line_distance.unwrap_or(u32::MAX);
let item_source_occurrences = Occurrences::within_string(&declaration.item_text().0);
let item_signature_occurrences = Occurrences::within_string(&declaration.signature_text().0);
let excerpt_vs_item_jaccard = jaccard_similarity(excerpt_occurrences, &item_source_occurrences);
@@ -219,6 +430,37 @@ fn score_declaration(
let adjacent_vs_signature_weighted_overlap =
weighted_overlap_coefficient(adjacent_occurrences, &item_signature_occurrences);
let mut import_similarity = 0f32;
let mut wildcard_import_similarity = 0f32;
if !import_occurrences.is_empty() || !wildcard_import_occurrences.is_empty() {
let cached_path = declaration.cached_path();
let path_occurrences = Occurrences::from_worktree_path(
cached_path
.worktree_abs_path
.file_name()
.map(|f| f.to_string_lossy()),
&cached_path.rel_path,
);
import_similarity = import_occurrences
.iter()
.map(|namespace_occurrences| {
OrderedFloat(jaccard_similarity(namespace_occurrences, &path_occurrences))
})
.max()
.map(|similarity| similarity.into_inner())
.unwrap_or_default();
// TODO: Consider something other than max
wildcard_import_similarity = wildcard_import_occurrences
.iter()
.map(|namespace_occurrences| {
OrderedFloat(jaccard_similarity(namespace_occurrences, &path_occurrences))
})
.max()
.map(|similarity| similarity.into_inner())
.unwrap_or_default();
}
// TODO: Consider adding declaration_file_count
let score_components = DeclarationScoreComponents {
is_same_file,
@@ -226,7 +468,6 @@ fn score_declaration(
is_referenced_in_breadcrumb,
reference_line_distance,
declaration_line_distance,
declaration_line_distance_rank,
reference_count,
same_file_declaration_count,
declaration_count,
@@ -238,52 +479,61 @@ fn score_declaration(
excerpt_vs_signature_weighted_overlap,
adjacent_vs_item_weighted_overlap,
adjacent_vs_signature_weighted_overlap,
path_import_match_count,
wildcard_path_import_match_count,
import_similarity,
max_import_similarity: 0.0,
normalized_import_similarity: 0.0,
wildcard_import_similarity,
normalized_wildcard_import_similarity: 0.0,
included_by_others: 0,
includes_others: 0,
};
Some(ScoredDeclaration {
ScoredDeclaration {
identifier: identifier.clone(),
declaration: declaration,
scores: DeclarationScores::score(&score_components),
score_components,
})
}
#[derive(Clone, Debug, Serialize)]
pub struct DeclarationScores {
pub signature: f32,
pub declaration: f32,
pub retrieval: f32,
}
impl DeclarationScores {
fn score(components: &DeclarationScoreComponents) -> DeclarationScores {
// TODO: handle truncation
// Score related to how likely this is the correct declaration, range 0 to 1
let retrieval = if components.is_same_file {
// TODO: use declaration_line_distance_rank
1.0 / components.same_file_declaration_count as f32
} else {
1.0 / components.declaration_count as f32
};
// Score related to the distance between the reference and cursor, range 0 to 1
let distance_score = if components.is_referenced_nearby {
1.0 / (1.0 + components.reference_line_distance as f32 / 10.0).powf(2.0)
} else {
// same score as ~14 lines away, rationale is to not overly penalize references from parent signatures
0.5
};
// For now instead of linear combination, the scores are just multiplied together.
let combined_score = 10.0 * retrieval * distance_score;
DeclarationScores {
signature: combined_score * components.excerpt_vs_signature_weighted_overlap,
// declaration score gets boosted both by being multiplied by 2 and by there being more
// weighted overlap.
declaration: 2.0 * combined_score * components.excerpt_vs_item_weighted_overlap,
retrieval,
}
declaration: declaration.clone(),
components: score_components,
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_declaration_path_matches() {
let declaration_path =
CachedDeclarationPath::new_for_test("/home/user/project", "src/maths.ts");
assert!(declaration_path_matches_import(
&declaration_path,
&Path::new("maths.ts").into()
));
assert!(declaration_path_matches_import(
&declaration_path,
&Path::new("project/src/maths.ts").into()
));
assert!(declaration_path_matches_import(
&declaration_path,
&Path::new("user/project/src/maths.ts").into()
));
assert!(declaration_path_matches_import(
&declaration_path,
&Path::new("/home/user/project/src/maths.ts").into()
));
assert!(!declaration_path_matches_import(
&declaration_path,
&Path::new("other.ts").into()
));
assert!(!declaration_path_matches_import(
&declaration_path,
&Path::new("/home/user/project/src/other.ts").into()
));
}
}

View File

@@ -1,12 +1,13 @@
mod declaration;
mod declaration_scoring;
mod excerpt;
mod imports;
mod outline;
mod reference;
mod syntax_index;
pub mod text_similarity;
use std::sync::Arc;
use std::{path::Path, sync::Arc};
use collections::HashMap;
use gpui::{App, AppContext as _, Entity, Task};
@@ -16,9 +17,17 @@ use text::{Point, ToOffset as _};
pub use declaration::*;
pub use declaration_scoring::*;
pub use excerpt::*;
pub use imports::*;
pub use reference::*;
pub use syntax_index::*;
#[derive(Clone, Debug, PartialEq)]
pub struct EditPredictionContextOptions {
pub use_imports: bool,
pub excerpt: EditPredictionExcerptOptions,
pub score: EditPredictionScoreOptions,
}
#[derive(Clone, Debug)]
pub struct EditPredictionContext {
pub excerpt: EditPredictionExcerpt,
@@ -31,21 +40,34 @@ impl EditPredictionContext {
pub fn gather_context_in_background(
cursor_point: Point,
buffer: BufferSnapshot,
excerpt_options: EditPredictionExcerptOptions,
options: EditPredictionContextOptions,
syntax_index: Option<Entity<SyntaxIndex>>,
cx: &mut App,
) -> Task<Option<Self>> {
let parent_abs_path = project::File::from_dyn(buffer.file()).and_then(|f| {
let mut path = f.worktree.read(cx).absolutize(&f.path);
if path.pop() { Some(path) } else { None }
});
if let Some(syntax_index) = syntax_index {
let index_state =
syntax_index.read_with(cx, |index, _cx| Arc::downgrade(index.state()));
cx.background_spawn(async move {
let parent_abs_path = parent_abs_path.as_deref();
let index_state = index_state.upgrade()?;
let index_state = index_state.lock().await;
Self::gather_context(cursor_point, &buffer, &excerpt_options, Some(&index_state))
Self::gather_context(
cursor_point,
&buffer,
parent_abs_path,
&options,
Some(&index_state),
)
})
} else {
cx.background_spawn(async move {
Self::gather_context(cursor_point, &buffer, &excerpt_options, None)
let parent_abs_path = parent_abs_path.as_deref();
Self::gather_context(cursor_point, &buffer, parent_abs_path, &options, None)
})
}
}
@@ -53,13 +75,20 @@ impl EditPredictionContext {
pub fn gather_context(
cursor_point: Point,
buffer: &BufferSnapshot,
excerpt_options: &EditPredictionExcerptOptions,
parent_abs_path: Option<&Path>,
options: &EditPredictionContextOptions,
index_state: Option<&SyntaxIndexState>,
) -> Option<Self> {
let imports = if options.use_imports {
Imports::gather(&buffer, parent_abs_path)
} else {
Imports::default()
};
Self::gather_context_with_references_fn(
cursor_point,
buffer,
excerpt_options,
&imports,
options,
index_state,
references_in_excerpt,
)
@@ -68,7 +97,8 @@ impl EditPredictionContext {
pub fn gather_context_with_references_fn(
cursor_point: Point,
buffer: &BufferSnapshot,
excerpt_options: &EditPredictionExcerptOptions,
imports: &Imports,
options: &EditPredictionContextOptions,
index_state: Option<&SyntaxIndexState>,
get_references: impl FnOnce(
&EditPredictionExcerpt,
@@ -79,7 +109,7 @@ impl EditPredictionContext {
let excerpt = EditPredictionExcerpt::select_from_buffer(
cursor_point,
buffer,
excerpt_options,
&options.excerpt,
index_state,
)?;
let excerpt_text = excerpt.text(buffer);
@@ -101,10 +131,12 @@ impl EditPredictionContext {
let references = get_references(&excerpt, &excerpt_text, buffer);
scored_declarations(
&options.score,
&index_state,
&excerpt,
&excerpt_occurrences,
&adjacent_occurrences,
&imports,
references,
cursor_offset_in_file,
buffer,
@@ -160,12 +192,18 @@ mod tests {
EditPredictionContext::gather_context_in_background(
cursor_point,
buffer_snapshot,
EditPredictionExcerptOptions {
max_bytes: 60,
min_bytes: 10,
target_before_cursor_over_total_bytes: 0.5,
EditPredictionContextOptions {
use_imports: true,
excerpt: EditPredictionExcerptOptions {
max_bytes: 60,
min_bytes: 10,
target_before_cursor_over_total_bytes: 0.5,
},
score: EditPredictionScoreOptions {
omit_excerpt_overlaps: true,
},
},
Some(index),
Some(index.clone()),
cx,
)
})

File diff suppressed because it is too large Load Diff

View File

@@ -5,6 +5,7 @@ use futures::lock::Mutex;
use futures::{FutureExt as _, StreamExt, future};
use gpui::{App, AppContext as _, AsyncApp, Context, Entity, Task, WeakEntity};
use itertools::Itertools;
use language::{Buffer, BufferEvent};
use postage::stream::Stream as _;
use project::buffer_store::{BufferStore, BufferStoreEvent};
@@ -17,6 +18,7 @@ use std::sync::Arc;
use text::BufferId;
use util::{RangeExt as _, debug_panic, some_or_debug_panic};
use crate::CachedDeclarationPath;
use crate::declaration::{
BufferDeclaration, Declaration, DeclarationId, FileDeclaration, Identifier,
};
@@ -28,6 +30,8 @@ use crate::outline::declarations_in_buffer;
// `buffer_declarations_containing_range` assumes that the index is always immediately up to date.
//
// * Add a per language configuration for skipping indexing.
//
// * Handle tsx / ts / js referencing each-other
// Potential future improvements:
//
@@ -61,6 +65,7 @@ pub struct SyntaxIndex {
state: Arc<Mutex<SyntaxIndexState>>,
project: WeakEntity<Project>,
initial_file_indexing_done_rx: postage::watch::Receiver<bool>,
_file_indexing_task: Option<Task<()>>,
}
pub struct SyntaxIndexState {
@@ -70,7 +75,6 @@ pub struct SyntaxIndexState {
buffers: HashMap<BufferId, BufferState>,
dirty_files: HashMap<ProjectEntryId, ProjectPath>,
dirty_files_tx: mpsc::Sender<()>,
_file_indexing_task: Option<Task<()>>,
}
#[derive(Debug, Default)]
@@ -102,12 +106,12 @@ impl SyntaxIndex {
buffers: HashMap::default(),
dirty_files: HashMap::default(),
dirty_files_tx,
_file_indexing_task: None,
};
let this = Self {
let mut this = Self {
project: project.downgrade(),
state: Arc::new(Mutex::new(initial_state)),
initial_file_indexing_done_rx,
_file_indexing_task: None,
};
let worktree_store = project.read(cx).worktree_store();
@@ -116,75 +120,77 @@ impl SyntaxIndex {
.worktrees()
.map(|w| w.read(cx).snapshot())
.collect::<Vec<_>>();
if !initial_worktree_snapshots.is_empty() {
this.state.try_lock().unwrap()._file_indexing_task =
Some(cx.spawn(async move |this, cx| {
let snapshots_file_count = initial_worktree_snapshots
.iter()
.map(|worktree| worktree.file_count())
.sum::<usize>();
let chunk_size = snapshots_file_count.div_ceil(file_indexing_parallelism);
let chunk_count = snapshots_file_count.div_ceil(chunk_size);
let file_chunks = initial_worktree_snapshots
.iter()
.flat_map(|worktree| {
let worktree_id = worktree.id();
worktree.files(false, 0).map(move |entry| {
(
entry.id,
ProjectPath {
worktree_id,
path: entry.path.clone(),
},
)
})
this._file_indexing_task = Some(cx.spawn(async move |this, cx| {
let snapshots_file_count = initial_worktree_snapshots
.iter()
.map(|worktree| worktree.file_count())
.sum::<usize>();
if snapshots_file_count > 0 {
let chunk_size = snapshots_file_count.div_ceil(file_indexing_parallelism);
let chunk_count = snapshots_file_count.div_ceil(chunk_size);
let file_chunks = initial_worktree_snapshots
.iter()
.flat_map(|worktree| {
let worktree_id = worktree.id();
worktree.files(false, 0).map(move |entry| {
(
entry.id,
ProjectPath {
worktree_id,
path: entry.path.clone(),
},
)
})
.chunks(chunk_size);
})
.chunks(chunk_size);
let mut tasks = Vec::with_capacity(chunk_count);
for chunk in file_chunks.into_iter() {
tasks.push(Self::update_dirty_files(
&this,
chunk.into_iter().collect(),
cx.clone(),
));
}
futures::future::join_all(tasks).await;
let mut tasks = Vec::with_capacity(chunk_count);
for chunk in file_chunks.into_iter() {
tasks.push(Self::update_dirty_files(
&this,
chunk.into_iter().collect(),
cx.clone(),
));
}
futures::future::join_all(tasks).await;
log::info!("Finished initial file indexing");
}
log::info!("Finished initial file indexing");
*initial_file_indexing_done_tx.borrow_mut() = true;
*initial_file_indexing_done_tx.borrow_mut() = true;
let Ok(state) = this.read_with(cx, |this, _cx| this.state.clone()) else {
return;
};
while dirty_files_rx.next().await.is_some() {
let mut state = state.lock().await;
let was_underused = state.dirty_files.capacity() > 255
&& state.dirty_files.len() * 8 < state.dirty_files.capacity();
let dirty_files = state.dirty_files.drain().collect::<Vec<_>>();
if was_underused {
state.dirty_files.shrink_to_fit();
}
drop(state);
if dirty_files.is_empty() {
continue;
}
let Ok(state) = this.read_with(cx, |this, _cx| Arc::downgrade(&this.state)) else {
return;
};
while dirty_files_rx.next().await.is_some() {
let Some(state) = state.upgrade() else {
return;
};
let mut state = state.lock().await;
let was_underused = state.dirty_files.capacity() > 255
&& state.dirty_files.len() * 8 < state.dirty_files.capacity();
let dirty_files = state.dirty_files.drain().collect::<Vec<_>>();
if was_underused {
state.dirty_files.shrink_to_fit();
}
drop(state);
if dirty_files.is_empty() {
continue;
}
let chunk_size = dirty_files.len().div_ceil(file_indexing_parallelism);
let chunk_count = dirty_files.len().div_ceil(chunk_size);
let mut tasks = Vec::with_capacity(chunk_count);
let chunks = dirty_files.into_iter().chunks(chunk_size);
for chunk in chunks.into_iter() {
tasks.push(Self::update_dirty_files(
&this,
chunk.into_iter().collect(),
cx.clone(),
));
}
futures::future::join_all(tasks).await;
}
}));
}
let chunk_size = dirty_files.len().div_ceil(file_indexing_parallelism);
let chunk_count = dirty_files.len().div_ceil(chunk_size);
let mut tasks = Vec::with_capacity(chunk_count);
let chunks = dirty_files.into_iter().chunks(chunk_size);
for chunk in chunks.into_iter() {
tasks.push(Self::update_dirty_files(
&this,
chunk.into_iter().collect(),
cx.clone(),
));
}
futures::future::join_all(tasks).await;
}
}));
cx.subscribe(&worktree_store, Self::handle_worktree_store_event)
.detach();
@@ -364,7 +370,9 @@ impl SyntaxIndex {
cx: &mut Context<Self>,
) {
match event {
BufferEvent::Edited => self.update_buffer(buffer, cx),
BufferEvent::Edited |
// paths are cached and so should be updated
BufferEvent::FileHandleChanged => self.update_buffer(buffer, cx),
_ => {}
}
}
@@ -375,8 +383,16 @@ impl SyntaxIndex {
return;
}
let Some(project_entry_id) =
project::File::from_dyn(buffer.file()).and_then(|f| f.project_entry_id(cx))
let Some((project_entry_id, cached_path)) = project::File::from_dyn(buffer.file())
.and_then(|f| {
let project_entry_id = f.project_entry_id()?;
let cached_path = CachedDeclarationPath::new(
f.worktree.read(cx).abs_path(),
&f.path,
buffer.language(),
);
Some((project_entry_id, cached_path))
})
else {
return;
};
@@ -440,6 +456,7 @@ impl SyntaxIndex {
buffer_id,
declaration,
project_entry_id,
cached_path: cached_path.clone(),
});
new_ids.push(declaration_id);
@@ -507,13 +524,14 @@ impl SyntaxIndex {
let snapshot_task = worktree.update(cx, |worktree, cx| {
let load_task = worktree.load_file(&project_path.path, cx);
let worktree_abs_path = worktree.abs_path();
cx.spawn(async move |_this, cx| {
let loaded_file = load_task.await?;
let language = language.await?;
let buffer = cx.new(|cx| {
let mut buffer = Buffer::local(loaded_file.text, cx);
buffer.set_language(Some(language), cx);
buffer.set_language(Some(language.clone()), cx);
buffer
})?;
@@ -522,14 +540,22 @@ impl SyntaxIndex {
parse_status.changed().await?;
}
buffer.read_with(cx, |buffer, _cx| buffer.snapshot())
let cached_path = CachedDeclarationPath::new(
worktree_abs_path,
&project_path.path,
Some(&language),
);
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
anyhow::Ok((snapshot, cached_path))
})
});
let state = Arc::downgrade(&self.state);
cx.background_spawn(async move {
// TODO: How to handle errors?
let Ok(snapshot) = snapshot_task.await else {
let Ok((snapshot, cached_path)) = snapshot_task.await else {
return;
};
let rope = snapshot.as_rope();
@@ -567,6 +593,7 @@ impl SyntaxIndex {
let declaration_id = state.declarations.insert(Declaration::File {
project_entry_id: entry_id,
declaration,
cached_path: cached_path.clone(),
});
new_ids.push(declaration_id);
@@ -921,6 +948,7 @@ mod tests {
if let Declaration::File {
declaration,
project_entry_id: file,
..
} = declaration
{
assert_eq!(

View File

@@ -1,9 +1,12 @@
use hashbrown::HashTable;
use regex::Regex;
use std::{
borrow::Cow,
hash::{Hash, Hasher as _},
path::Path,
sync::LazyLock,
};
use util::rel_path::RelPath;
use crate::reference::Reference;
@@ -45,19 +48,34 @@ impl Occurrences {
)
}
pub fn from_identifiers<'a>(identifiers: impl IntoIterator<Item = &'a str>) -> Self {
pub fn from_identifiers(identifiers: impl IntoIterator<Item = impl AsRef<str>>) -> Self {
let mut this = Self::default();
// TODO: Score matches that match case higher?
//
// TODO: Also include unsplit identifier?
for identifier in identifiers {
for identifier_part in split_identifier(identifier) {
for identifier_part in split_identifier(identifier.as_ref()) {
this.add_hash(fx_hash(&identifier_part.to_lowercase()));
}
}
this
}
pub fn from_worktree_path(worktree_name: Option<Cow<'_, str>>, rel_path: &RelPath) -> Self {
if let Some(worktree_name) = worktree_name {
Self::from_identifiers(
std::iter::once(worktree_name)
.chain(iter_path_without_extension(rel_path.as_std_path())),
)
} else {
Self::from_path(rel_path.as_std_path())
}
}
pub fn from_path(path: &Path) -> Self {
Self::from_identifiers(iter_path_without_extension(path))
}
fn add_hash(&mut self, hash: u64) {
self.table
.entry(
@@ -82,6 +100,15 @@ impl Occurrences {
}
}
fn iter_path_without_extension(path: &Path) -> impl Iterator<Item = Cow<'_, str>> {
let last_component: Option<Cow<'_, str>> = path.file_stem().map(|stem| stem.to_string_lossy());
let mut path_components = path.components();
path_components.next_back();
path_components
.map(|component| component.as_os_str().to_string_lossy())
.chain(last_component)
}
pub fn fx_hash<T: Hash + ?Sized>(data: &T) -> u64 {
let mut hasher = collections::FxHasher::default();
data.hash(&mut hasher);
@@ -269,4 +296,19 @@ mod test {
// the smaller set, 10.
assert_eq!(weighted_overlap_coefficient(&set_a, &set_b), 7.0 / 10.0);
}
#[test]
fn test_iter_path_without_extension() {
let mut iter = iter_path_without_extension(Path::new(""));
assert_eq!(iter.next(), None);
let iter = iter_path_without_extension(Path::new("foo"));
assert_eq!(iter.collect::<Vec<_>>(), ["foo"]);
let iter = iter_path_without_extension(Path::new("foo/bar.txt"));
assert_eq!(iter.collect::<Vec<_>>(), ["foo", "bar"]);
let iter = iter_path_without_extension(Path::new("foo/bar/baz.txt"));
assert_eq!(iter.collect::<Vec<_>>(), ["foo", "bar", "baz"]);
}
}

View File

@@ -456,6 +456,33 @@ actions!(
Fold,
/// Folds all foldable regions in the editor.
FoldAll,
/// Folds all code blocks at indentation level 1.
#[action(name = "FoldAtLevel_1")]
FoldAtLevel1,
/// Folds all code blocks at indentation level 2.
#[action(name = "FoldAtLevel_2")]
FoldAtLevel2,
/// Folds all code blocks at indentation level 3.
#[action(name = "FoldAtLevel_3")]
FoldAtLevel3,
/// Folds all code blocks at indentation level 4.
#[action(name = "FoldAtLevel_4")]
FoldAtLevel4,
/// Folds all code blocks at indentation level 5.
#[action(name = "FoldAtLevel_5")]
FoldAtLevel5,
/// Folds all code blocks at indentation level 6.
#[action(name = "FoldAtLevel_6")]
FoldAtLevel6,
/// Folds all code blocks at indentation level 7.
#[action(name = "FoldAtLevel_7")]
FoldAtLevel7,
/// Folds all code blocks at indentation level 8.
#[action(name = "FoldAtLevel_8")]
FoldAtLevel8,
/// Folds all code blocks at indentation level 9.
#[action(name = "FoldAtLevel_9")]
FoldAtLevel9,
/// Folds all function bodies in the editor.
FoldFunctionBodies,
/// Folds the current code block and all its children.

View File

@@ -1,9 +1,10 @@
use std::path::PathBuf;
use anyhow::Context as _;
use gpui::{App, Context, Entity, Window};
use language::Language;
use project::lsp_store::lsp_ext_command::SwitchSourceHeaderResult;
use rpc::proto;
use url::Url;
use util::paths::PathStyle;
use workspace::{OpenOptions, OpenVisible};
@@ -77,16 +78,17 @@ pub fn switch_source_header(
return Ok(());
}
let goto = Url::parse(&switch_source_header.0).with_context(|| {
format!(
"Parsing URL \"{}\" returned from switch source/header failed",
switch_source_header.0
)
})?;
let goto = switch_source_header
.0
.strip_prefix("file://")
.with_context(|| {
format!(
"Parsing file url \"{}\" returned from switch source/header failed",
switch_source_header.0
)
})?;
let path = goto
.to_file_path()
.map_err(|()| anyhow::anyhow!("URL conversion to file path failed for \"{goto}\""))?;
let path = PathBuf::from(goto);
workspace
.update_in(cx, |workspace, window, cx| {

View File

@@ -1518,6 +1518,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 +1529,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

@@ -170,20 +170,15 @@ impl DisplayMap {
let buffer_snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
let (inlay_snapshot, edits) = self.inlay_map.sync(buffer_snapshot, edits);
let (fold_snapshot, edits) = self.fold_map.read(inlay_snapshot.clone(), edits);
let (fold_snapshot, edits) = self.fold_map.read(inlay_snapshot, edits);
let tab_size = Self::tab_size(&self.buffer, cx);
let (tab_snapshot, edits) = self.tab_map.sync(fold_snapshot.clone(), edits, tab_size);
let (tab_snapshot, edits) = self.tab_map.sync(fold_snapshot, edits, tab_size);
let (wrap_snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(tab_snapshot.clone(), edits, cx));
let block_snapshot = self.block_map.read(wrap_snapshot.clone(), edits).snapshot;
.update(cx, |map, cx| map.sync(tab_snapshot, edits, cx));
let block_snapshot = self.block_map.read(wrap_snapshot, edits).snapshot;
DisplaySnapshot {
buffer_snapshot: self.buffer.read(cx).snapshot(cx),
fold_snapshot,
inlay_snapshot,
tab_snapshot,
wrap_snapshot,
block_snapshot,
diagnostics_max_severity: self.diagnostics_max_severity,
crease_snapshot: self.crease_map.snapshot(),
@@ -198,10 +193,10 @@ impl DisplayMap {
pub fn set_state(&mut self, other: &DisplaySnapshot, cx: &mut Context<Self>) {
self.fold(
other
.folds_in_range(0..other.buffer_snapshot.len())
.folds_in_range(0..other.buffer_snapshot().len())
.map(|fold| {
Crease::simple(
fold.range.to_offset(&other.buffer_snapshot),
fold.range.to_offset(other.buffer_snapshot()),
fold.placeholder.clone(),
)
})
@@ -762,12 +757,7 @@ impl<'a> HighlightedChunk<'a> {
#[derive(Clone)]
pub struct DisplaySnapshot {
pub buffer_snapshot: MultiBufferSnapshot,
pub fold_snapshot: FoldSnapshot,
pub crease_snapshot: CreaseSnapshot,
inlay_snapshot: InlaySnapshot,
tab_snapshot: TabSnapshot,
wrap_snapshot: WrapSnapshot,
block_snapshot: BlockSnapshot,
text_highlights: TextHighlights,
inlay_highlights: InlayHighlights,
@@ -776,15 +766,44 @@ pub struct DisplaySnapshot {
diagnostics_max_severity: DiagnosticSeverity,
pub(crate) fold_placeholder: FoldPlaceholder,
}
impl DisplaySnapshot {
pub fn wrap_snapshot(&self) -> &WrapSnapshot {
&self.block_snapshot.wrap_snapshot
}
pub fn tab_snapshot(&self) -> &TabSnapshot {
&self.block_snapshot.wrap_snapshot.tab_snapshot
}
pub fn fold_snapshot(&self) -> &FoldSnapshot {
&self.block_snapshot.wrap_snapshot.tab_snapshot.fold_snapshot
}
pub fn inlay_snapshot(&self) -> &InlaySnapshot {
&self
.block_snapshot
.wrap_snapshot
.tab_snapshot
.fold_snapshot
.inlay_snapshot
}
pub fn buffer_snapshot(&self) -> &MultiBufferSnapshot {
&self
.block_snapshot
.wrap_snapshot
.tab_snapshot
.fold_snapshot
.inlay_snapshot
.buffer
}
#[cfg(test)]
pub fn fold_count(&self) -> usize {
self.fold_snapshot.fold_count()
self.fold_snapshot().fold_count()
}
pub fn is_empty(&self) -> bool {
self.buffer_snapshot.len() == 0
self.buffer_snapshot().len() == 0
}
pub fn row_infos(&self, start_row: DisplayRow) -> impl Iterator<Item = RowInfo> + '_ {
@@ -792,16 +811,16 @@ impl DisplaySnapshot {
}
pub fn widest_line_number(&self) -> u32 {
self.buffer_snapshot.widest_line_number()
self.buffer_snapshot().widest_line_number()
}
pub fn prev_line_boundary(&self, mut point: MultiBufferPoint) -> (Point, DisplayPoint) {
loop {
let mut inlay_point = self.inlay_snapshot.to_inlay_point(point);
let mut fold_point = self.fold_snapshot.to_fold_point(inlay_point, Bias::Left);
let mut inlay_point = self.inlay_snapshot().to_inlay_point(point);
let mut fold_point = self.fold_snapshot().to_fold_point(inlay_point, Bias::Left);
fold_point.0.column = 0;
inlay_point = fold_point.to_inlay_point(&self.fold_snapshot);
point = self.inlay_snapshot.to_buffer_point(inlay_point);
inlay_point = fold_point.to_inlay_point(self.fold_snapshot());
point = self.inlay_snapshot().to_buffer_point(inlay_point);
let mut display_point = self.point_to_display_point(point, Bias::Left);
*display_point.column_mut() = 0;
@@ -819,11 +838,11 @@ impl DisplaySnapshot {
) -> (MultiBufferPoint, DisplayPoint) {
let original_point = point;
loop {
let mut inlay_point = self.inlay_snapshot.to_inlay_point(point);
let mut fold_point = self.fold_snapshot.to_fold_point(inlay_point, Bias::Right);
fold_point.0.column = self.fold_snapshot.line_len(fold_point.row());
inlay_point = fold_point.to_inlay_point(&self.fold_snapshot);
point = self.inlay_snapshot.to_buffer_point(inlay_point);
let mut inlay_point = self.inlay_snapshot().to_inlay_point(point);
let mut fold_point = self.fold_snapshot().to_fold_point(inlay_point, Bias::Right);
fold_point.0.column = self.fold_snapshot().line_len(fold_point.row());
inlay_point = fold_point.to_inlay_point(self.fold_snapshot());
point = self.inlay_snapshot().to_buffer_point(inlay_point);
let mut display_point = self.point_to_display_point(point, Bias::Right);
*display_point.column_mut() = self.line_len(display_point.row());
@@ -841,7 +860,8 @@ impl DisplaySnapshot {
let new_end = if range.end.column > 0 {
MultiBufferPoint::new(
range.end.row,
self.buffer_snapshot.line_len(MultiBufferRow(range.end.row)),
self.buffer_snapshot()
.line_len(MultiBufferRow(range.end.row)),
)
} else {
range.end
@@ -851,52 +871,52 @@ impl DisplaySnapshot {
}
pub fn point_to_display_point(&self, point: MultiBufferPoint, bias: Bias) -> DisplayPoint {
let inlay_point = self.inlay_snapshot.to_inlay_point(point);
let fold_point = self.fold_snapshot.to_fold_point(inlay_point, bias);
let tab_point = self.tab_snapshot.to_tab_point(fold_point);
let wrap_point = self.wrap_snapshot.tab_point_to_wrap_point(tab_point);
let inlay_point = self.inlay_snapshot().to_inlay_point(point);
let fold_point = self.fold_snapshot().to_fold_point(inlay_point, bias);
let tab_point = self.tab_snapshot().to_tab_point(fold_point);
let wrap_point = self.wrap_snapshot().tab_point_to_wrap_point(tab_point);
let block_point = self.block_snapshot.to_block_point(wrap_point);
DisplayPoint(block_point)
}
pub fn display_point_to_point(&self, point: DisplayPoint, bias: Bias) -> Point {
self.inlay_snapshot
self.inlay_snapshot()
.to_buffer_point(self.display_point_to_inlay_point(point, bias))
}
pub fn display_point_to_inlay_offset(&self, point: DisplayPoint, bias: Bias) -> InlayOffset {
self.inlay_snapshot
self.inlay_snapshot()
.to_offset(self.display_point_to_inlay_point(point, bias))
}
pub fn anchor_to_inlay_offset(&self, anchor: Anchor) -> InlayOffset {
self.inlay_snapshot
.to_inlay_offset(anchor.to_offset(&self.buffer_snapshot))
self.inlay_snapshot()
.to_inlay_offset(anchor.to_offset(self.buffer_snapshot()))
}
pub fn display_point_to_anchor(&self, point: DisplayPoint, bias: Bias) -> Anchor {
self.buffer_snapshot
self.buffer_snapshot()
.anchor_at(point.to_offset(self, bias), bias)
}
fn display_point_to_inlay_point(&self, point: DisplayPoint, bias: Bias) -> InlayPoint {
let block_point = point.0;
let wrap_point = self.block_snapshot.to_wrap_point(block_point, bias);
let tab_point = self.wrap_snapshot.to_tab_point(wrap_point);
let fold_point = self.tab_snapshot.to_fold_point(tab_point, bias).0;
fold_point.to_inlay_point(&self.fold_snapshot)
let tab_point = self.wrap_snapshot().to_tab_point(wrap_point);
let fold_point = self.tab_snapshot().to_fold_point(tab_point, bias).0;
fold_point.to_inlay_point(self.fold_snapshot())
}
pub fn display_point_to_fold_point(&self, point: DisplayPoint, bias: Bias) -> FoldPoint {
let block_point = point.0;
let wrap_point = self.block_snapshot.to_wrap_point(block_point, bias);
let tab_point = self.wrap_snapshot.to_tab_point(wrap_point);
self.tab_snapshot.to_fold_point(tab_point, bias).0
let tab_point = self.wrap_snapshot().to_tab_point(wrap_point);
self.tab_snapshot().to_fold_point(tab_point, bias).0
}
pub fn fold_point_to_display_point(&self, fold_point: FoldPoint) -> DisplayPoint {
let tab_point = self.tab_snapshot.to_tab_point(fold_point);
let wrap_point = self.wrap_snapshot.tab_point_to_wrap_point(tab_point);
let tab_point = self.tab_snapshot().to_tab_point(fold_point);
let wrap_point = self.wrap_snapshot().tab_point_to_wrap_point(tab_point);
let block_point = self.block_snapshot.to_block_point(wrap_point);
DisplayPoint(block_point)
}
@@ -1118,7 +1138,7 @@ impl DisplaySnapshot {
}
pub fn buffer_chars_at(&self, mut offset: usize) -> impl Iterator<Item = (char, usize)> + '_ {
self.buffer_snapshot.chars_at(offset).map(move |ch| {
self.buffer_snapshot().chars_at(offset).map(move |ch| {
let ret = (ch, offset);
offset += ch.len_utf8();
ret
@@ -1129,7 +1149,7 @@ impl DisplaySnapshot {
&self,
mut offset: usize,
) -> impl Iterator<Item = (char, usize)> + '_ {
self.buffer_snapshot
self.buffer_snapshot()
.reversed_chars_at(offset)
.map(move |ch| {
offset -= ch.len_utf8();
@@ -1152,11 +1172,11 @@ impl DisplaySnapshot {
pub fn clip_at_line_end(&self, display_point: DisplayPoint) -> DisplayPoint {
let mut point = self.display_point_to_point(display_point, Bias::Left);
if point.column != self.buffer_snapshot.line_len(MultiBufferRow(point.row)) {
if point.column != self.buffer_snapshot().line_len(MultiBufferRow(point.row)) {
return display_point;
}
point.column = point.column.saturating_sub(1);
point = self.buffer_snapshot.clip_point(point, Bias::Left);
point = self.buffer_snapshot().clip_point(point, Bias::Left);
self.point_to_display_point(point, Bias::Left)
}
@@ -1164,7 +1184,7 @@ impl DisplaySnapshot {
where
T: ToOffset,
{
self.fold_snapshot.folds_in_range(range)
self.fold_snapshot().folds_in_range(range)
}
pub fn blocks_in_range(
@@ -1185,12 +1205,12 @@ impl DisplaySnapshot {
}
pub fn intersects_fold<T: ToOffset>(&self, offset: T) -> bool {
self.fold_snapshot.intersects_fold(offset)
self.fold_snapshot().intersects_fold(offset)
}
pub fn is_line_folded(&self, buffer_row: MultiBufferRow) -> bool {
self.block_snapshot.is_line_replaced(buffer_row)
|| self.fold_snapshot.is_line_folded(buffer_row)
|| self.fold_snapshot().is_line_folded(buffer_row)
}
pub fn is_block_line(&self, display_row: DisplayRow) -> bool {
@@ -1207,7 +1227,7 @@ impl DisplaySnapshot {
.block_snapshot
.to_wrap_point(BlockPoint::new(display_row.0, 0), Bias::Left)
.row();
self.wrap_snapshot.soft_wrap_indent(wrap_row)
self.wrap_snapshot().soft_wrap_indent(wrap_row)
}
pub fn text(&self) -> String {
@@ -1228,7 +1248,7 @@ impl DisplaySnapshot {
}
pub fn line_indent_for_buffer_row(&self, buffer_row: MultiBufferRow) -> LineIndent {
self.buffer_snapshot.line_indent_for_row(buffer_row)
self.buffer_snapshot().line_indent_for_row(buffer_row)
}
pub fn line_len(&self, row: DisplayRow) -> u32 {
@@ -1246,7 +1266,7 @@ impl DisplaySnapshot {
}
pub fn starts_indent(&self, buffer_row: MultiBufferRow) -> bool {
let max_row = self.buffer_snapshot.max_row();
let max_row = self.buffer_snapshot().max_row();
if buffer_row >= max_row {
return false;
}
@@ -1271,10 +1291,11 @@ impl DisplaySnapshot {
}
pub fn crease_for_buffer_row(&self, buffer_row: MultiBufferRow) -> Option<Crease<Point>> {
let start = MultiBufferPoint::new(buffer_row.0, self.buffer_snapshot.line_len(buffer_row));
let start =
MultiBufferPoint::new(buffer_row.0, self.buffer_snapshot().line_len(buffer_row));
if let Some(crease) = self
.crease_snapshot
.query_row(buffer_row, &self.buffer_snapshot)
.query_row(buffer_row, self.buffer_snapshot())
{
match crease {
Crease::Inline {
@@ -1284,7 +1305,7 @@ impl DisplaySnapshot {
render_trailer,
metadata,
} => Some(Crease::Inline {
range: range.to_point(&self.buffer_snapshot),
range: range.to_point(self.buffer_snapshot()),
placeholder: placeholder.clone(),
render_toggle: render_toggle.clone(),
render_trailer: render_trailer.clone(),
@@ -1298,7 +1319,7 @@ impl DisplaySnapshot {
block_priority,
render_toggle,
} => Some(Crease::Block {
range: range.to_point(&self.buffer_snapshot),
range: range.to_point(self.buffer_snapshot()),
block_height: *block_height,
block_style: *block_style,
render_block: render_block.clone(),
@@ -1310,7 +1331,7 @@ impl DisplaySnapshot {
&& !self.is_line_folded(MultiBufferRow(start.row))
{
let start_line_indent = self.line_indent_for_buffer_row(buffer_row);
let max_point = self.buffer_snapshot.max_point();
let max_point = self.buffer_snapshot().max_point();
let mut end = None;
for row in (buffer_row.0 + 1)..=max_point.row {
@@ -1321,7 +1342,7 @@ impl DisplaySnapshot {
let prev_row = row - 1;
end = Some(Point::new(
prev_row,
self.buffer_snapshot.line_len(MultiBufferRow(prev_row)),
self.buffer_snapshot().line_len(MultiBufferRow(prev_row)),
));
break;
}
@@ -1330,7 +1351,7 @@ impl DisplaySnapshot {
let mut row_before_line_breaks = end.unwrap_or(max_point);
while row_before_line_breaks.row > start.row
&& self
.buffer_snapshot
.buffer_snapshot()
.is_line_blank(MultiBufferRow(row_before_line_breaks.row))
{
row_before_line_breaks.row -= 1;
@@ -1338,7 +1359,7 @@ impl DisplaySnapshot {
row_before_line_breaks = Point::new(
row_before_line_breaks.row,
self.buffer_snapshot
self.buffer_snapshot()
.line_len(MultiBufferRow(row_before_line_breaks.row)),
);
@@ -1482,23 +1503,23 @@ impl DisplayPoint {
pub fn to_offset(self, map: &DisplaySnapshot, bias: Bias) -> usize {
let wrap_point = map.block_snapshot.to_wrap_point(self.0, bias);
let tab_point = map.wrap_snapshot.to_tab_point(wrap_point);
let fold_point = map.tab_snapshot.to_fold_point(tab_point, bias).0;
let inlay_point = fold_point.to_inlay_point(&map.fold_snapshot);
map.inlay_snapshot
.to_buffer_offset(map.inlay_snapshot.to_offset(inlay_point))
let tab_point = map.wrap_snapshot().to_tab_point(wrap_point);
let fold_point = map.tab_snapshot().to_fold_point(tab_point, bias).0;
let inlay_point = fold_point.to_inlay_point(map.fold_snapshot());
map.inlay_snapshot()
.to_buffer_offset(map.inlay_snapshot().to_offset(inlay_point))
}
}
impl ToDisplayPoint for usize {
fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint {
map.point_to_display_point(self.to_point(&map.buffer_snapshot), Bias::Left)
map.point_to_display_point(self.to_point(map.buffer_snapshot()), Bias::Left)
}
}
impl ToDisplayPoint for OffsetUtf16 {
fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint {
self.to_offset(&map.buffer_snapshot).to_display_point(map)
self.to_offset(map.buffer_snapshot()).to_display_point(map)
}
}
@@ -1510,7 +1531,7 @@ impl ToDisplayPoint for Point {
impl ToDisplayPoint for Anchor {
fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint {
self.to_point(&map.buffer_snapshot).to_display_point(map)
self.to_point(map.buffer_snapshot()).to_display_point(map)
}
}
@@ -1599,10 +1620,10 @@ pub mod tests {
let mut blocks = Vec::new();
let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
log::info!("buffer text: {:?}", snapshot.buffer_snapshot.text());
log::info!("fold text: {:?}", snapshot.fold_snapshot.text());
log::info!("tab text: {:?}", snapshot.tab_snapshot.text());
log::info!("wrap text: {:?}", snapshot.wrap_snapshot.text());
log::info!("buffer text: {:?}", snapshot.buffer_snapshot().text());
log::info!("fold text: {:?}", snapshot.fold_snapshot().text());
log::info!("tab text: {:?}", snapshot.tab_snapshot().text());
log::info!("wrap text: {:?}", snapshot.wrap_snapshot().text());
log::info!("block text: {:?}", snapshot.block_snapshot.text());
log::info!("display text: {:?}", snapshot.text());
@@ -1634,7 +1655,8 @@ pub mod tests {
30..=44 => {
map.update(cx, |map, cx| {
if rng.random() || blocks.is_empty() {
let buffer = map.snapshot(cx).buffer_snapshot;
let snapshot = map.snapshot(cx);
let buffer = snapshot.buffer_snapshot();
let block_properties = (0..rng.random_range(1..=1))
.map(|_| {
let position = buffer.anchor_after(buffer.clip_offset(
@@ -1715,15 +1737,15 @@ pub mod tests {
let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
fold_count = snapshot.fold_count();
log::info!("buffer text: {:?}", snapshot.buffer_snapshot.text());
log::info!("fold text: {:?}", snapshot.fold_snapshot.text());
log::info!("tab text: {:?}", snapshot.tab_snapshot.text());
log::info!("wrap text: {:?}", snapshot.wrap_snapshot.text());
log::info!("buffer text: {:?}", snapshot.buffer_snapshot().text());
log::info!("fold text: {:?}", snapshot.fold_snapshot().text());
log::info!("tab text: {:?}", snapshot.tab_snapshot().text());
log::info!("wrap text: {:?}", snapshot.wrap_snapshot().text());
log::info!("block text: {:?}", snapshot.block_snapshot.text());
log::info!("display text: {:?}", snapshot.text());
// Line boundaries
let buffer = &snapshot.buffer_snapshot;
let buffer = snapshot.buffer_snapshot();
for _ in 0..5 {
let row = rng.random_range(0..=buffer.max_point().row);
let column = rng.random_range(0..=buffer.line_len(MultiBufferRow(row)));
@@ -1907,7 +1929,7 @@ pub mod tests {
)
);
let ix = snapshot.buffer_snapshot.text().find("seven").unwrap();
let ix = snapshot.buffer_snapshot().text().find("seven").unwrap();
buffer.update(cx, |buffer, cx| {
buffer.edit([(ix..ix, "and ")], None, cx);
});

View File

@@ -33,8 +33,8 @@ const BULLETS: &[u8; u128::BITS as usize] = &[b'*'; _];
///
/// See the [`display_map` module documentation](crate::display_map) for more information.
pub struct BlockMap {
pub(super) wrap_snapshot: RefCell<WrapSnapshot>,
next_block_id: AtomicUsize,
wrap_snapshot: RefCell<WrapSnapshot>,
custom_blocks: Vec<Arc<CustomBlock>>,
custom_blocks_by_id: TreeMap<CustomBlockId, Arc<CustomBlock>>,
transforms: RefCell<SumTree<Transform>>,
@@ -53,7 +53,7 @@ pub struct BlockMapWriter<'a>(&'a mut BlockMap);
#[derive(Clone)]
pub struct BlockSnapshot {
wrap_snapshot: WrapSnapshot,
pub(super) wrap_snapshot: WrapSnapshot,
transforms: SumTree<Transform>,
custom_blocks_by_id: TreeMap<CustomBlockId, Arc<CustomBlock>>,
pub(super) buffer_header_height: u32,
@@ -689,6 +689,7 @@ impl BlockMap {
// For each of these blocks, insert a new isomorphic transform preceding the block,
// and then insert the block itself.
let mut just_processed_folded_buffer = false;
for (block_placement, block) in blocks_in_edit.drain(..) {
let mut summary = TransformSummary {
input_rows: 0,
@@ -701,8 +702,12 @@ impl BlockMap {
match block_placement {
BlockPlacement::Above(position) => {
rows_before_block = position.0 - new_transforms.summary().input_rows;
just_processed_folded_buffer = false;
}
BlockPlacement::Near(position) | BlockPlacement::Below(position) => {
if just_processed_folded_buffer {
continue;
}
if position.0 + 1 < new_transforms.summary().input_rows {
continue;
}
@@ -711,6 +716,7 @@ impl BlockMap {
BlockPlacement::Replace(range) => {
rows_before_block = range.start().0 - new_transforms.summary().input_rows;
summary.input_rows = range.end().0 - range.start().0 + 1;
just_processed_folded_buffer = matches!(block, Block::FoldedBuffer { .. });
}
}
@@ -3566,6 +3572,96 @@ mod tests {
assert_eq!(blocks_snapshot.text(), "abc\n\ndef\nghi\njkl\nmno");
}
#[gpui::test]
fn test_folded_buffer_with_near_blocks(cx: &mut gpui::TestAppContext) {
cx.update(init_test);
let text = "line 1\nline 2\nline 3";
let buffer = cx.update(|cx| {
MultiBuffer::build_multi([(text, vec![Point::new(0, 0)..Point::new(2, 6)])], cx)
});
let buffer_snapshot = cx.update(|cx| buffer.read(cx).snapshot(cx));
let buffer_ids = buffer_snapshot
.excerpts()
.map(|(_, buffer_snapshot, _)| buffer_snapshot.remote_id())
.dedup()
.collect::<Vec<_>>();
assert_eq!(buffer_ids.len(), 1);
let buffer_id = buffer_ids[0];
let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
let (_, wrap_snapshot) =
cx.update(|cx| WrapMap::new(tab_snapshot, font("Helvetica"), px(14.0), None, cx));
let mut block_map = BlockMap::new(wrap_snapshot.clone(), 1, 1);
let mut writer = block_map.write(wrap_snapshot.clone(), Patch::default());
writer.insert(vec![BlockProperties {
style: BlockStyle::Fixed,
placement: BlockPlacement::Near(buffer_snapshot.anchor_after(Point::new(0, 0))),
height: Some(1),
render: Arc::new(|_| div().into_any()),
priority: 0,
}]);
let blocks_snapshot = block_map.read(wrap_snapshot.clone(), Patch::default());
assert_eq!(blocks_snapshot.text(), "\nline 1\n\nline 2\nline 3");
let mut writer = block_map.write(wrap_snapshot.clone(), Patch::default());
buffer.read_with(cx, |buffer, cx| {
writer.fold_buffers([buffer_id], buffer, cx);
});
let blocks_snapshot = block_map.read(wrap_snapshot, Patch::default());
assert_eq!(blocks_snapshot.text(), "");
}
#[gpui::test]
fn test_folded_buffer_with_near_blocks_on_last_line(cx: &mut gpui::TestAppContext) {
cx.update(init_test);
let text = "line 1\nline 2\nline 3\nline 4";
let buffer = cx.update(|cx| {
MultiBuffer::build_multi([(text, vec![Point::new(0, 0)..Point::new(3, 6)])], cx)
});
let buffer_snapshot = cx.update(|cx| buffer.read(cx).snapshot(cx));
let buffer_ids = buffer_snapshot
.excerpts()
.map(|(_, buffer_snapshot, _)| buffer_snapshot.remote_id())
.dedup()
.collect::<Vec<_>>();
assert_eq!(buffer_ids.len(), 1);
let buffer_id = buffer_ids[0];
let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
let (_, wrap_snapshot) =
cx.update(|cx| WrapMap::new(tab_snapshot, font("Helvetica"), px(14.0), None, cx));
let mut block_map = BlockMap::new(wrap_snapshot.clone(), 1, 1);
let mut writer = block_map.write(wrap_snapshot.clone(), Patch::default());
writer.insert(vec![BlockProperties {
style: BlockStyle::Fixed,
placement: BlockPlacement::Near(buffer_snapshot.anchor_after(Point::new(3, 6))),
height: Some(1),
render: Arc::new(|_| div().into_any()),
priority: 0,
}]);
let blocks_snapshot = block_map.read(wrap_snapshot.clone(), Patch::default());
assert_eq!(blocks_snapshot.text(), "\nline 1\nline 2\nline 3\nline 4\n");
let mut writer = block_map.write(wrap_snapshot.clone(), Patch::default());
buffer.read_with(cx, |buffer, cx| {
writer.fold_buffers([buffer_id], buffer, cx);
});
let blocks_snapshot = block_map.read(wrap_snapshot, Patch::default());
assert_eq!(blocks_snapshot.text(), "");
}
fn init_test(cx: &mut gpui::App) {
let settings = SettingsStore::test(cx);
cx.set_global(settings);

View File

@@ -624,10 +624,10 @@ impl FoldMap {
#[derive(Clone)]
pub struct FoldSnapshot {
pub inlay_snapshot: InlaySnapshot,
transforms: SumTree<Transform>,
folds: SumTree<Fold>,
fold_metadata_by_id: TreeMap<FoldId, FoldMetadata>,
pub inlay_snapshot: InlaySnapshot,
pub version: usize,
}

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