Compare commits

...

142 Commits

Author SHA1 Message Date
Nia Espera
bbd9b9f2e9 add insane test 2025-09-30 17:53:38 +02:00
George Waters
6e80fca0d5 Order venvs by distance to worktree root (#39067)
This is a follow up to #37510 and is also related to #38910.

Release Notes:

- Improved ordering of virtual environments, sort by distance to
worktree root.
2025-09-29 16:25:41 +00:00
George Waters
778ca84f85 Fix selecting and deleting user toolchains (#39068)
I was trying to use the new user toolchains but every time I clicked on
one I had added, it would delete it from the picker. Ironically, it
wouldn't delete it permanently when I tried to by clicking on the trash
can icon. Every time I reopened the workspace all user toolchains were
there.

Release Notes:

- Fixed selecting and deleting user toolchains.
2025-09-29 18:08:47 +02:00
Lukas Wirth
ebdc0572c6 zed: Add binary type to sentry crash tags (#39107)
This allows to filter by main zed binary or remote server crashes, as
well as easily tell whether a crash happened in a remote-server binary
or not.

Release Notes:

- N/A
2025-09-29 09:03:00 -07:00
Bennet Bo Fenner
cda48a3a1c zeta2: Allow provider to suggest edits in different files (#39110)
Release Notes:

- N/A

---------

Co-authored-by: Agus Zubiaga <agus@zed.dev>
2025-09-29 15:48:58 +00:00
Bennet Bo Fenner
b7f9fd7d74 zeta2: Do not include empty edit events (#39116)
Release Notes:

- N/A
2025-09-29 15:45:23 +00:00
Lukas Wirth
98ab118526 git: Work around windows command length limit message fetching (#39115)
Release Notes:

- Fix git blame failing on windows for files with lots of blame entries
2025-09-29 15:29:42 +00:00
tsjason
1e70a1a4ce Improve recent projects search result ordering (#38795)
Previously, search results were sorted solely by candidate_id
(preserving original order from the database), which could result in
less relevant matches appearing before better ones.

This change sorts results primarily by fuzzy match score (descending),
with candidate_id as a tiebreaker for equal scores. This ensures that
better matches appear first while preserving recency order among items
with identical scores.

Example improvement:
- Searching for 'pica' will now rank 'picabo' higher than scattered
matches like 'project-api, project-chat'
- Consecutive character matches are prioritized over scattered matches
across multiple path segments

Release Notes:
- Improved project search relevance by ranking results using match score
instead of insertion order.
2025-09-29 17:15:47 +02:00
AidanV
163219af35 editor: Make kill ring cut at EOF a no-op (#39069)
Release Notes:

- Emacs's kill ring cut at the end of the last line of the file will now
no-op instead of cutting the entire line
2025-09-29 08:55:38 -06:00
Miao
f96fd928d7 git: Fix git modal and panel amend tooltip (#39008)
Closes #38783

Release Notes:

- Fixed the amend button tooltip shortcut in Git panel and modal.
2025-09-29 19:58:14 +05:30
Ben Kunkle
9aa5817b85 Fix panic due to ThemeRegistry::global call in remote server (#39111)
Fixes ZED-1PV

Note: Nightly only panic

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-09-29 14:24:17 +00:00
David Kleingeld
28cc39ad56 Replace linear resampler with fft based one (#39098)
Replaces the use of Rodio's basic linear resampler with an fft based
resampler from the rubato crate. As we are down-sampling to the minimal
(transparent) sample rate for human speech (16kHz) any down-sampling
artifact will be noticeable.

This also refactors the rodio_ext module into sub-models as it was
getting quite long.

Release Notes:

- N/A
2025-09-29 16:22:45 +02:00
warrenjokinen
0da3f9ffda docs_preprocessor: Update deprecated actions message (#39062)
Minor correction to label (string) used when generating big table of
actions.

Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-09-29 10:11:48 -04:00
Lukas Wirth
f2efe78feb editor: Shrink size of Inlay slightly (#39089)
And some other smaller cleanup things I noticed while reading through
some stuff

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-09-29 15:33:21 +02:00
Danilo Leal
ed7217ff46 ui prompt: Adjust UI and focus visibility (#39106)
Closes https://github.com/zed-industries/zed/issues/38643

This PR adds some UI improvements to the Zed replacement of the system
dialog/prompt, including better visibility of which button is currently
focused.

One little design note, though: because of a current (and somewhat
annoying) constraint of button component, where we're only drawing a
border when its style is outlined, if I kept them horizontally stacked,
there'd be a little layout shift now that I'm toggling styles for better
focus visibility. So, for this reason, I changed them to be vertically
stacked, which matches the macOS design and avoids this problem. Maybe
in the future, we'll revert it back to being `flex_row` because that
ultimately consumes less space.


https://github.com/user-attachments/assets/500c840b-6b56-4c0c-b56a-535939398a7b

Release Notes:

- Improve focus visibility of the actions within Zed's UI system prompt.
2025-09-29 10:09:31 -03:00
Lukas Wirth
f9fb389f86 themes: Set font_weight to null for syntax.hint (#39105)
Since https://github.com/zed-industries/zed/pull/36219 we now render
inlay hints as bold due to this.

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-09-29 12:54:12 +00:00
Bartosz Kaszubowski
632e569c5f markdown_preview: Improve table elements appearance (#39101)
# How

Eliminate double borders between Markdown rows and cells, restyle
headers relying on background color alteration instead of thicker pixel
border.

Release Notes:

- Improved table elements appearance in Markdown Preview

# Preview

### Before

<img width="1206" height="594" alt="Screenshot 2025-09-29 at 13 28 23"
src="https://github.com/user-attachments/assets/9fe2b8a8-13e1-4052-9e97-34559b44f2d0"
/>

### After

<img width="1206" height="578" alt="Screenshot 2025-09-29 at 13 28 40"
src="https://github.com/user-attachments/assets/0b627ada-f287-436b-9448-92900d4bff59"
/>
2025-09-29 09:41:41 -03:00
Smit Barmase
0c71aa9f01 Bump tree-sitter-python to 0.25.0 (#39103)
- The fork with the patch is now included in 0.25.0
(7ff26dacd7).
- We no longer need `except*` as a keyword, which was added in
https://github.com/zed-industries/zed/pull/21389. It now highlights
correctly without explicitly mentioning it after
1b1ca93298.

Release Notes:

- N/A
2025-09-29 17:57:11 +05:30
Jowell Young
92a09ecf25 x_ai: Add support for tools and images with custom models (#38792)
After the change, we can add "supports_images", "supports_tools" and
"parallel_tool_calls" properties to set up new models. Our
`settings.json` will be as follows:
```json
  "language_models": {
     "x_ai": {
       "api_url": "https://api.x.ai/v1",
       "available_models": [
         {
           "name": "grok-4-fast-reasoning",
           "display_name": "Grok 4 Fast Reasoning",
           "max_tokens": 2000000,
           "max_output_tokens": 64000,
           "supports_tools": true,
           "parallel_tool_calls": true,
         },
         {
           "name": "grok-4-fast-non-reasoning",
           "display_name": "Grok 4 Fast Non-Reasoning",
           "max_tokens": 2000000,
           "max_output_tokens": 64000,
           "supports_images": true,
         }
       ]
     }
   }

```

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

Release Notes:

- xAI: Added support for for configuring tool and image support for
custom model configurations
2025-09-29 11:38:55 +00:00
Ben Brandt
bad96776cd acp: Add NO_PROXY if not set otherwise to not proxy localhost urls (#39100)
Since we might run MCP servers locally for an agent, we don't want to
use the proxy for those.
We set this if the user has set a proxy, but not a custom NO_PROXY env
var.

Closes #38839

Release Notes:

- acp: Don't run local mcp servers through proxy, if set
2025-09-29 11:34:52 +00:00
Kirill Bulatov
aa14980523 Mention pure style changes in the contributing docs (#39096)
Release Notes:

- N/A
2025-09-29 11:02:47 +00:00
warrenjokinen
12aba6193e docs: Fix minor typos in configuring-zed.md (#39048)
Fixed numbering under heading  Bottom Dock Layout

Closes #ISSUE

Release Notes:

- N/A
2025-09-29 13:12:07 +03:00
Bartosz Kaszubowski
720971e47b git_ui: Fix last commit UI glitching on panel resize (#39059)
# Why

Spotted that on Git Panel resize last commit UI part could glitch due to
commit message being wrapped into second line in certain situations.

# How

Force only one line for the last commit message in Git Panel via
`line_clamp`.

I have also remove manual `max-width` setting since it is controlled by
flex layout and gap setting no matter if there is an additional element
on the right or not.

Release Notes:

- Fixed last commit UI glitching on panel resize

# Preview

### Before


https://github.com/user-attachments/assets/9ce74f6f-d33c-4787-b7e4-010de8f0ffff

<img width="852" height="502" alt="Screenshot 2025-09-28 at 18 16 35"
src="https://github.com/user-attachments/assets/1131c73f-fe06-4d8e-adbb-5ce84ecf31e0"
/>

### After


https://github.com/user-attachments/assets/279b8c37-7ec9-4038-8761-197cba26aa83
2025-09-29 13:06:15 +03:00
Lukas Wirth
0a10e3e264 acp_thread: Fix terminal tool incorrectly redirecting stdin to /dev/null (#39092)
Closes https://github.com/zed-industries/zed/issues/38462

Release Notes:

- Fixed AI terminal tool incorrectly redirecting stdin to `/dev/null`
2025-09-29 09:43:50 +00:00
Xiaobo Liu
77854f4627 windows: Refactor shell environment capture to use new_smol_command (#39055)
Using `crate::command::new_smol_command` on the Windows platform will
not display the PowerShell window.

Closes #39052

Release Notes:

- N/A

Signed-off-by: Xiaobo Liu <cppcoffee@gmail.com>
2025-09-28 18:54:26 +02:00
Lukas Wirth
5ce7eda8d2 ui: Fix panic in highlight_ranges when given an oob index (#39051)
Fixes ZED-1QW

Release Notes:

- Fixed a panic when highlighting labels
2025-09-28 11:54:18 +00:00
Lukas Wirth
6d7a4c441b search: Fix panic in project search due to workspace double lease (#39049)
Fixes ZED-1K1

Release Notes:

- Fixed panic when spawning a new project search with include file only
filtering
2025-09-28 11:35:36 +00:00
Lukas Wirth
cc85a48de5 editor: Fix panic when syncing empty selections (#39047)
Fixes ZED-1KF

Release Notes:

- Fixed commit modal panicking in specific scenario
2025-09-28 11:01:16 +00:00
warrenjokinen
4cd839e352 Fix typo in search.rs (#39045)
Fixed confusing word

Release Notes:

- Fixed a typo in the tooltip for search case sensitivity.
2025-09-28 11:17:08 +02:00
Yang Gang
78098f6809 windows: Update Windows keymap (#38767)
Pickup the changes from #36550

Release Notes:

- N/A

---------

Signed-off-by: Yang Gang <yanggang.uefi@gmail.com>
Co-authored-by: 张小白 <364772080@qq.com>
2025-09-28 02:09:44 +08:00
warrenjokinen
4d2ff6c899 markup: Update yara.md (#39027)
Minor fixes / clarifications for two links in one markdown file

Release Notes:

- N/A
2025-09-27 19:43:19 +02:00
Lukas Wirth
6f5d1522cb git_ui: Allow splitting commit_view pane (#39025)
Release Notes:

- Allow splitting git commit view pane
2025-09-27 15:28:37 +00:00
Xiaobo Liu
682cf023ca windows: Implement shell environment loading for git operations (#39019)
Fixes the "failed to get working directory environment for repository"
error on Windows by implementing proper shell environment variable
capture.

Release Notes:

- Fixed failed to get working directory environment for repository

---------

Signed-off-by: Xiaobo Liu <cppcoffee@gmail.com>
2025-09-27 17:10:06 +02:00
Lukas Wirth
72948e14ee Use into_owned over to_string for Cow<str> (#39024)
This removes unnecessary allocations when the `Cow` is already owned


Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-09-27 14:50:10 +00:00
Cole Miller
a063a70cfb call: Play a different sound when a guest joins (#38987)
Release Notes:

- collab: A distinct sound effect is now used for when a guest joins a
call.
- collab: Fixed the "joined" sound being excessively loud when joining a
call that already has many participants.

---------

Co-authored-by: David Kleingeld <davidsk@zed.dev>
2025-09-27 09:20:55 -04:00
Cole Miller
687e22b4c3 extension_host: Use the more permissive RelPath constructor for paths from extensions (#38965)
Closes #38922 

Release Notes:

- N/A

---------

Co-authored-by: Jakub Konka <kubkon@jakubkonka.com>
2025-09-27 09:20:42 -04:00
loczek
e13b88e4bd snippets: Fix configure snippets not opening on remote workspaces (#38790)
Release Notes:

- Fixed `snippets: configure snippets` action not working on remote
workspaces
2025-09-27 11:01:04 +02:00
Bedis Nbiba
e1e9f78dc3 docs: Document config completion for Deno (#38993)
Closes #ISSUE

Release Notes:

- doc: document config completion for deno
2025-09-26 22:17:46 -04:00
Max Brunsfeld
0fe696bc7c Bump html extension version to 0.2.3 (#38997)
Release Notes:

- N/A
2025-09-26 23:23:00 +00:00
Lukas Wirth
ead38fd1be fsevent: Check CFURLCreateFromFileSystemRepresentation return value (#38996)
Fixes ZED-1T

Release Notes:

- Fixed a segmentation fault on macOS fervent stream creation
2025-09-26 22:28:52 +00:00
Lukas Wirth
fbdf5d4df4 editor: Do not panic on tab_size > 16, cap it at 128 (#38994)
Fixes ZED-1PT
Fixes ZED-1PW
Fixes ZED-1G2

Release Notes:

- Fixed Zed panicking when the `tab_size` is set higher than 16
2025-09-27 00:13:16 +02:00
Max Brunsfeld
837f282f1e html: Remove Windows workaround (#38069)
⚠️ Don't merge until Zed 0.205.x is on stable ⚠️ 

See https://github.com/zed-industries/zed/pull/37811

This PR updates the HTML extension, bumping the zed extension API to the
latest version, which removes the need to work around a bug where
`current_dir()` returned an invalid path on windows.

Release Notes:

- N/A
2025-09-26 12:14:54 -07:00
Xiaobo Liu
bd3cccea15 edit_prediction_button: Fix Copilot menu not updating after sign out (#38854)
The edit prediction button menu was displaying stale authentication
status due to capturing the Copilot status in a closure. After signing
out, the menu would still show "Sign Out" instead of "Sign In to
Copilot".

This change fixes the issue by reading the current Copilot status each
time the menu is displayed, ensuring the menu options are always
accurate.

Release Notes:

- Fixed Copilot AI menu not updating after sign out

Signed-off-by: Xiaobo Liu <cppcoffee@gmail.com>
2025-09-26 12:42:06 -06:00
justin talbott
d437bbaa0a Don't let ctrl-g clobber git panel keybindings in Emacs keymap (#37732)
i'm testing out zed, coming from emacs, and so i'm trying out the base
keymap for it. i noticed though that zed's default git keybindings don't
work when the gitpanel is open though, because of the top-level binding
of `ctrl-g` to cancel. my expectation is that the emacs-like keybindings
would work insofar as they don't clobber zed's defaults (which would
take precedence), but obviously i'll defer to others on this!

another option could be to use the `C-x v` keymap prefix that the emacs
built-in `vc` package uses, but it doesn't contain the same set of
bindings for git commands that zed has.
2025-09-26 12:13:35 -06:00
Conrad Irwin
114791e1a8 Revert "Fix arrow function detection in TypeScript/JavaScript outline (#38411)" (#38982)
This reverts commit 1bbf98aea6.

We found that #38411 caused problems where anonymous functions are
included too many times in the outline. We'd like to figure out a better
fix before shipping this to stable.

Fixes #38956

Release Notes:

- (preview only) revert changes to outline view
2025-09-26 13:54:52 -04:00
Martin Pool
d6fcd404af Show config messages from install-wild, install-mold (#38979)
Follows on from
https://github.com/zed-industries/zed/pull/37717#discussion_r2376739687

@dvdsk suggested this but I didn't get to it in the previous PR.

# Tested

```
; sudo rm /usr/local/bin/wild
; ./script/install-wild
Downloading from https://github.com/davidlattimore/wild/releases/download/0.6.0/wild-linker-0.6.0-x86_64-unknown-linux-gnu.tar.gz
Wild is installed to /usr/local/bin/wild

To make it your default, add or merge these lines into your ~/.cargo/config.toml:

[target.x86_64-unknown-linux-gnu]
linker = "clang"
rustflags = ["-C", "link-arg=--ld-path=wild"]

[target.aarch64-unknown-linux-gnu]
linker = "clang"
rustflags = ["-C", "link-arg=--ld-path=wild"]

```

```
; sudo rm /usr/local/bin/mold
; ./script/install-mold 2.34.0
Downloading from https://github.com/rui314/mold/releases/download/v2.34.0/mold-2.34.0-x86_64-linux.tar.gz
Mold is installed to /usr/local/bin/mold

To make it your default, add or merge these lines into your ~/.cargo/config.toml:

[target.'cfg(target_os = "linux")']
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
```

Release Notes:

- N/A
2025-09-26 16:47:38 +00:00
Bartosz Kaszubowski
7ad9ca9bcc editor: Replace hardcoded keystroke in Excerpt Fold Toggle tooltip (#38978)
# Why

I have recently corrected this tooltip content for macOS, but recently
have learnt that keystroke to text helpers already exist in the
codebase.

# How

Replace hardcoded keystroke for Excerpt Fold Toggle in Uncommitted
Changes tab.

> [!important]
> Should be merged after #38969 and #38971, otherwise it would be a
regression on macOS.

Release Notes:

- N/A

# Preview (stacked on mentioned above PRs)

<img width="618" height="248" alt="Screenshot 2025-09-26 at 17 43 53"
src="https://github.com/user-attachments/assets/cdc7fb74-e1d8-4a59-b847-8a8d2edd4641"
/>
2025-09-26 10:44:46 -06:00
Bartosz Kaszubowski
a55dff7834 ui: Fix Vim mode detection in keybinding to text helpers (#38971)
# Why

Refs:
* #38969

When working on the PR above I have spotted that keybinding to text
helpers incorrectly detects if Vim mode is enabled.

# How

Replace inline check with an existing `KeyBinding::is_vim_mode` method
in keybinding text helpers.

Release Notes:

- Fixed incorrect Vim mode detection in UI keybinding to text helpers.

# Test plan

Made sure that when Vim mode is not specified in settings file it
resolves to `false`, and correct keybindings are displayed, than I have
added the `"vim_mode": true,` line to my settings file and made sure
that keybindings text have changed accordingly.

### Before

<img width="712" height="264" alt="Screenshot 2025-09-26 at 16 57 08"
src="https://github.com/user-attachments/assets/62bc24bd-c335-420f-9c2e-3690031518c1"
/>

### After

<img width="712" height="264" alt="Screenshot 2025-09-26 at 17 13 50"
src="https://github.com/user-attachments/assets/e0088897-eb6b-4d7b-855a-931adcc15fe8"
/>
2025-09-26 10:18:49 -06:00
Bartosz Kaszubowski
6db621a1ed ui: Display option in lowercase in Vim mode keybindings (#38969)
# Why

Spotted that some tooltips include `alt` keystroke combination on macOS.

# How

Add missing `vim_mode` version definition of `Option` key to the
`keystroke_text` helper.

Release Notes:

- Fixed keystroke to text helper output for macOS `Option` key in Vim
mode

# Preview

### Before

<img width="712" height="264" alt="Screenshot 2025-09-26 at 16 57 08"
src="https://github.com/user-attachments/assets/d5daa37f-0da7-4430-91ea-4a750c025472"
/>

### After

<img width="712" height="264" alt="Screenshot 2025-09-26 at 16 56 21"
src="https://github.com/user-attachments/assets/5804ed39-9b1b-4028-a9c9-32c066042f4a"
/>
2025-09-26 10:18:31 -06:00
Kirill Bulatov
948b4379df Stop using linear color space on Linux Blade renderer (#38967)
Part of https://github.com/zed-industries/zed/issues/7992
Closes https://github.com/zed-industries/zed/issues/22711

Left is main, right is patched.

* default font

<img width="3862" height="2152" alt="image"
src="https://github.com/user-attachments/assets/c4e3d18a-a0dd-48b8-a1f0-182407655efb"
/>
<img width="3862" height="2152" alt="image"
src="https://github.com/user-attachments/assets/6eea07e7-1676-422c-961f-05bc72677fad"
/>


<img width="3862" height="2152" alt="image"
src="https://github.com/user-attachments/assets/4d9e30dc-6905-48ad-849d-48eac6ebed03"
/>
<img width="3862" height="2152" alt="image"
src="https://github.com/user-attachments/assets/ef20986e-c29c-4fe0-9f20-56da4fb0ac29"
/>


* font size 7

<img width="3862" height="2152" alt="image"
src="https://github.com/user-attachments/assets/8b277e92-9ae4-4415-8903-68566b580f5a"
/>
<img width="3862" height="2152" alt="image"
src="https://github.com/user-attachments/assets/b9140e73-81af-430b-b07f-af118c7e3dae"
/>

<img width="3862" height="2152" alt="image"
src="https://github.com/user-attachments/assets/185f526a-241e-4573-af1d-f27aedeac48e"
/>
<img width="3862" height="2152" alt="image"
src="https://github.com/user-attachments/assets/7a239121-ae13-4db9-99d9-785ec26cd98e"
/>


Release Notes:

- Improved color rendering on Linux

Co-authored-by: Kate <kate@zed.dev>
Co-authored-by: John <john-tur@outlook.com>
Co-authored-by: apricotbucket28 <71973804+apricotbucket28@users.noreply.github.com>
2025-09-26 16:09:30 +00:00
Danilo Leal
8db24dd8ad docs: Update wording around configuring MCP servers (#38973)
Felt like this could be clarified a bit.

Release Notes:

- N/A
2025-09-26 12:47:26 -03:00
Ben Kunkle
4aac5642c1 JSON Schema URIs (#38916)
Closes #ISSUE

Improves the efficiency of our interactions with the Zed language
server. Previously, on startup and after every workspace configuration
changed notification, we would send >1MB of JSON Schemas to the JSON
LSP. The only reason this had to happen was due to the case where an
extension was installed that would result in a change to the JSON schema
for settings (i.e. added language, theme, etc).

This PR changes the behavior to use the URI LSP extensions of
`vscode-json-language-server` in order to send the server URI's that it
can then use to fetch the schemas as needed (i.e. the settings schema is
only generated and sent when `settings.json` is opened. This brings the
JSON we send to on startup and after every workspace configuration
changed notification down to a couple of KB.

Additionally, using another LSP extension request we can notify the
server when a schema has changed using the URI as a key, so we no longer
have to send a workspace configuration changed notification, and the
schema contents will only be re-requested and regenerated if the schema
is in use.

Release Notes:

- Improved the efficiency of communication with the builtin JSON LSP.
JSON Schemas are no longer sent to the JSON language server in their
full form. If you wish to view a builtin JSON schema in the language
server info tab of the language server logs (`dev: open language server
logs`), you must now use the `editor: open url` action with your cursor
over the URL that is sent to the server.
- Made it so that Zed urls (`zed://...`) are resolved locally when
opened within the editor instead of being resolved through the OS. Users
who could not previously open `zed://*` URLs in the editor can now do so
by pasting the link into a buffer and using the `editor: open url`
action (please open an issue if this is the case for you!).

---------

Co-authored-by: Michael <michael@zed.dev>
2025-09-26 11:41:26 -04:00
Nia
30b49cfbf5 perf: Fixup ordering, fix pathing, docs (#38970)
Release Notes:

- N/A
2025-09-26 15:28:48 +00:00
Lukas Wirth
c69912c76a Forbid std::process::Command spawning, replace with smol where appropriate (#38894)
std commands can block for an arbitrary duration and so runs risk of
blocking tasks for too long. This replaces all such uses where sensible
with async processes.

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-09-26 15:17:36 +00:00
Smit Barmase
7f14ab26dd copilot: Ensure minimum Node version (#38945)
Closes #38918

Release Notes:

- N/A
2025-09-26 20:08:21 +05:30
Marshall Bowers
5ee73d3e3c Move settings_macros to Cargo workspace (#38962)
Release Notes:

- N/A
2025-09-26 14:20:36 +00:00
Martin Pool
d5aa81a5b2 Fix up Wild package name and decompression (#38961)
Wild changed in 0.6.0 to using gzip rather than xz, and changed the
format of the package name.

Follows on from and fixes
https://github.com/zed-industries/zed/pull/37717

cc @dvdsk @mati865 

Release Notes:

- N/A
2025-09-26 16:20:01 +02:00
Kirill Bulatov
21855c15e4 Disable subpixel shifting for y axis on Linux (#38959)
Part of https://github.com/zed-industries/zed/issues/7992
Port of #38440

<img width="3836" height="2142" alt="zed_nightly_vs_zed_dev_2"
src="https://github.com/user-attachments/assets/66bcbb9a-2159-4790-8a9a-d4814058d966"
/>

Does not change the rendering on Linux, but prepares us for the times
without cosmic-text where this will be needed.

Release Notes:

- N/A

Co-authored-by: Kate <kate@zed.dev>
Co-authored-by: John <john@zed.dev>
2025-09-26 13:31:59 +00:00
Kirill Bulatov
1f9279a56f linux: Add missing linear to sRGB transform in mono sprite rendering (#38944)
Part of https://github.com/zed-industries/zed/issues/7992
Takes
https://github.com/zed-industries/zed/issues/7992#issuecomment-3083871615
and applies its adjusted version on the current state of things

Screenshots (left is main, right is the patch): 

* default font size

<img width="3840" height="2160" alt="image"
src="https://github.com/user-attachments/assets/26fdc42c-12e6-447f-ad3d-74808e4b2562"
/>

<img width="3840" height="2160" alt="image"
src="https://github.com/user-attachments/assets/29829c61-c998-4e77-97c3-0e66e14b236d"
/>


* buffer and ui font size 7 

<img width="3840" height="2160" alt="image"
src="https://github.com/user-attachments/assets/5d0f1d94-b7ed-488d-ab22-c25eb01e6b4a"
/>

<img width="3840" height="2160" alt="image"
src="https://github.com/user-attachments/assets/7020d62e-de65-4b86-a64b-d3eea798c217"
/>


Release Notes:

- Added missing linear to sRGB transform in mono sprite rendering on
Linux

Co-authored-by: Thomas Dagenais <exrok@i64.dev>
Co-authored-by: Kate <work@localcc.cc>
2025-09-26 09:54:46 +00:00
Michael Sloan
da71465437 edit_prediction_context: Minor optimization of text similarity + some renames (#38941)
Release Notes:

- N/A
2025-09-26 07:57:28 +00:00
Nia
bcc8149263 perf: Fixes (#38935)
Release Notes:

- N/A
2025-09-26 05:41:06 +00:00
Danilo Leal
b1528601cc settings ui: Add some light design tweaks (#38934)
Release Notes:

- N/A
2025-09-26 05:22:57 +00:00
Marshall Bowers
ee357e8987 language_models: Send a header indicating that the client supports xAI models (#38931)
This PR adds an `x-zed-client-supports-x-ai` header to the `GET /models`
request sent to Cloud to indicate that the client supports xAI models.

Release Notes:

- N/A
2025-09-26 04:11:48 +00:00
Floyd Wang
0891a7142d gpui: Fix incorrect colors comment (#38929)
| Before | After |
| - | - |
| <img width="466" height="207" alt="SCR-20250926-khst"
src="https://github.com/user-attachments/assets/c28a9ea8-3d22-458c-a683-b2fabe275a04"
/> | <img width="480" height="215" alt="SCR-20250926-kgru"
src="https://github.com/user-attachments/assets/cfee6392-804c-46e2-a55a-f72071264d10"
/> |

Release Notes:

- N/A
2025-09-25 21:53:04 -06:00
Marshall Bowers
94fe862fb6 x_ai: Fix Model::from_id for Grok 4 (#38930)
This PR fixes `x_ai::Model::from_id`, which was not properly handling
`grok-4`.

Release Notes:

- N/A
2025-09-26 03:49:14 +00:00
Marshall Bowers
4f91fab190 language_models: Add xAI support to Zed Cloud provider (#38928)
This PR adds xAI support to the Zed Cloud provider.

Release Notes:

- N/A
2025-09-26 03:19:12 +00:00
Mikayla Maki
0e0f48d8e1 Introduce SettingsField type to the settings UI (#38921)
Release Notes:

- N/A

---------

Co-authored-by: Nathan Sobo <nathan@zed.dev>
Co-authored-by: Anthony Eid <hello@anthonyeid.me>
Co-authored-by: Ben Kunkle <ben@zed.dev>
2025-09-26 01:08:55 +00:00
Max Brunsfeld
7980dbdaea Add API docs for RelPath (#38923)
Also reduce the use of `unsafe` in that module.

Release Notes:

- N/A
2025-09-26 00:49:10 +00:00
Michael Sloan
a5683f3541 zeta_cli: Add --output-format both and --prompt-format only-snippets (#38920)
These are options are probably temporary, added for use in some
experimental code

Release Notes:

- N/A

Co-authored-by: Oleksiy <oleksiy@zed.dev>
2025-09-25 22:49:36 +00:00
Michael Sloan
67984d5e49 provider configuration: Use SingleLineInput instead of Editor (#38814)
Release Notes:

- N/A
2025-09-25 22:38:27 +00:00
Cole Miller
d83d7d35cb windows: Fix inconsistent separators in buffer headers and breadcrumbs (#38898)
Make `resolve_full_path` use the appropriate separators, and return a
`String`.

As part of fixing the fallout from that type change, this also fixes a
bunch of places in the agent code that were using `std::path::Path`
operations on paths that could be non-local, by changing them to operate
instead on strings and use the project's `PathStyle`.

This clears the way a bit for making `full_path` also return a string
instead of a `PathBuf`, but I've left that for a follow-up.

Release Notes:

- N/A
2025-09-25 22:24:32 +00:00
Derek Nguyen
6470443271 python: Fix ty archive extraction on Linux (#38917)
Closes #38553 
Release Notes:

- Fixed wrong AssetKind specified on linux for ty 


As discussed in the linked issue. All of the non windows assets for ty
are `tar.gz` files. This change applies that fix.
2025-09-25 22:17:49 +00:00
Jakub Konka
5b72dfff87 helix: Streamline mode naming in the UI and in settings (#38870)
Release Notes:

- When `helix_mode = true`, modes are called without the `HELIX_` prefix
in the UI:
  `HELIX_NORMAL` becomes `NORMAL`
  `HELIX_SELECT` becomes `SELECT`
- (breaking change) Helix users should remove `"default_mode":
"helix_normal"` from their settings. This is now the default when
`"helix_mode": true`.
2025-09-25 23:57:01 +02:00
Max Brunsfeld
495a7b0a84 Clean up RelPath API (#38912)
Consolidate constructors and accessors.

Release Notes:

- N/A

---------

Co-authored-by: Cole Miller <cole@zed.dev>
2025-09-25 14:42:32 -07:00
Lauren Hinchcliffe
301e976465 Fix inlay hints using status theming instead of syntax theming (#36219)
Release Notes:

- Fixed editor inlay hints incorrectly using status theming when syntax
theming is available

Previously, a theme's `style.syntax.hint` object is completely ignored,
and `style.hint` `style.hint.background` are used instead. However,
these seem to be related to status hints, such as the inline git blame
integration.

For syntax hints (as given by an LSP), the reasonable assumption would
be that the `style.syntax.hint` object is used instead, but it isn't.
This means that defining other style characteristics (`font_style`, for
example) does nothing.

I've fixed the issue in a backward-compatible way, by using the theme
`syntax` `HighlightStyle` as the base for inlay hint styling, and
falling back to the original `status` colors should the syntax object
not contain the color definitions.

 With the following theme settings:
```jsonc
{
  "hint": "#ff00ff",                    // Status hints (git blame, etc.)
  "hint.background": "#ff00ff10",
  "syntax": {
    "hint": {
      "color": "#ffffff",               // LSP inlay hints
      "background_color": "#ffffff10",
      "font_style": "italic",           // Now properly applied
      "font_weight": 700
    }
  }
}
```


Current behavior:
<img width="896" height="201" alt="image"
src="https://github.com/user-attachments/assets/e89d212f-ed7e-4d27-94e4-96d716e229d2"
/>

Italics and font weight are ignored. Uses status colors instead.

Fixed behavior:
<img width="896" height="202" alt="image"
src="https://github.com/user-attachments/assets/f14ed2c3-bb60-4b74-886d-6b409d338714"
/>

Italics and font weight are used properly. Status color is preserved for
the git blame status, but correct syntax colors are used for the inlay
hints.
2025-09-25 16:39:12 -05:00
Anthony Eid
daebc4052d settings ui: Implement dynamic navbar based on pages section headers (#38915)
Release Notes:

- N/A

---------

Co-authored-by: Ben Kunkle <ben@zed.dev>
2025-09-25 21:19:18 +00:00
Cole Miller
ecc35fcd9a acp: Fix @mentions when remoting from Windows to Linux (#38882)
Closes #38620

`Url::from_file_path` and `Url::from_directory_path` assume the path
style of the target they were compiled for, so we can't use them in
general. So, switch from `file://` to encoding the absolute path (for
mentions that have one) as a query parameter, which works no matter the
platforms. We'll still parse the old `file://` mention URIs for
compatibility with thread history.

Release Notes:

- windows: Fixed a crash when using `@mentions` in agent threads when
remoting from Windows to Linux or WSL.
2025-09-25 16:23:45 -04:00
Ben Kunkle
236006b6b3 settings_ui: Small UI improvements (#38911)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-09-25 20:23:02 +00:00
Jakub Konka
39c4480841 terminal: Trace terminal events (#38896)
Tracing terminal events can now be enabled using typical `RUST_LOG`
invocation:

```
RUST_LOG=info,terminal=trace,alacritty_terminal=trace cargo run
```

Release Notes:

- N/A
2025-09-25 22:21:33 +02:00
Ben Kunkle
48aac2a746 settings_ui: Add dropdown component + other fixes (#38909)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-09-25 15:59:34 -04:00
Nathan Sobo
ae036f8ead Read env vars in TestScheduler::many (#38897)
This allows ITERATIONS and SEED environment variables to override the
hard coded values during testing.

cc @ConradIrwin @as-cii 

Release Notes:

- N/A

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-09-25 13:03:56 -06:00
Bartosz Kaszubowski
de1de25712 keymap_editor: Fix filter input element alignment (#38895)
# Why

I have spotted that Keymap Editor filter input (editor) is misaligned
vertically.

# How

Switch the input wrapper to flex layout, use `items_center` to align
editor vertically in center of the wrapper.

Release Notes:

- Fixed Keymap Editor filter input alignment

# Test plan

I have tested the change locally and compared the UI before and after,
to make sure that change does not affect the size of the wrapper
element.

### Before

<img width="1622" height="428" alt="Screenshot 2025-09-25 at 18 18 59"
src="https://github.com/user-attachments/assets/7d09be5c-6caf-4873-8ecf-2542851cb40a"
/>

### After

<img width="1622" height="428" alt="Screenshot 2025-09-25 at 18 07 18"
src="https://github.com/user-attachments/assets/540fcb3e-691d-4fb7-8130-2ed45ddc0adc"
/>
2025-09-25 15:29:02 -03:00
Cole Miller
18fc951135 Fix flaky test_remote_resolve_path_in_buffer test (#38903)
Release Notes:

- N/A
2025-09-25 18:11:45 +00:00
Cole Miller
40138e12a4 windows: Make ctrl-n open a new terminal when in a terminal (#38900)
This is how `ctrl-n` works on macOS. Right now `ctrl-n` on Windows with
the default keymap usually causes a new buffer to open, which is
inconvenient.

Release Notes:

- N/A
2025-09-25 17:51:19 +00:00
Joseph T. Lyons
e7a5c81b07 Improve media-creation flow in release process (#38902)
Release Notes:

- N/A
2025-09-25 17:49:12 +00:00
Joseph T. Lyons
d98175c0a6 Update release process docs to reflect new process (#38892)
Release Notes:

- N/A
2025-09-25 11:57:00 -04:00
313838373473747564656e74766775
bc7d804a42 remote: Don’t pass --method=GET to wget (#38771)
BusyBox's off brand `wget` does not have support for the `--method`
argument, which makes `zed` incapable of downloading the remote server
unless the _☙authentic❧_ one is installed. Removing this should fix the
issue. Couldn't find much about guidelines on how the code is supposed
to be formatted, so I opted for commenting the line out with an
explanation.

Closes #38712

Release Notes:

- Fixed remote development on BusyBox
2025-09-25 14:59:04 +00:00
Ben Kunkle
50bb8a4ae6 gpui: Add tab group (#38531)
Closes #ISSUE

Co-Authored-By: Mikayla <mikayla@zed.dev>
Co-Authored-By: Anthony <anthony@zed.dev>
Co-Authored-By: Kate <kate@zed.dev>

Release Notes:

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

---------

Co-authored-by: Kate <kate@zed.dev>
Co-authored-by: Mikayla <mikayla@zed.dev>
Co-authored-by: Anthony <anthony@zed.dev>
Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
2025-09-25 14:41:29 +00:00
Agus Zubiaga
b2b90b003d zeta2: Add prompt format option to inspector (#38884)
Adds the new prompt format option to the inspector view


Release Notes:

- N/A

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
2025-09-25 14:23:58 +00:00
Agus Zubiaga
c0f56f500e zeta2: Test prediction request (#38794)
Release Notes:

- N/A

---------

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
2025-09-25 13:49:56 +00:00
Lukas Wirth
a9fe18f4cb Revert "gpui: Flash menu in menubar on macOS when action is triggered (#38588)" (#38880)
This reverts commit ed7bd5a8ed.

We noticed this PR causes the editor to hang if you hold down any of the
menu item actions like ctrl+z, ctrl+x, etc


Release Notes:

- Fixed macOS menu item actions hanging the editor when their key
combination is held down
2025-09-25 13:36:19 +00:00
David Kleingeld
3c5e683fbe Fix experimental audio, add denoise, auto volume.Prep migration (#38874)
Uses the previously merged denoising crate (and fixes a bug in it that
snug in during refactoring) to add denoising to the microphone input. 

Adds automatic volume control for microphone and output.

Prepares for migrating to 16kHz SR mono:
The experimental audio path now picks the samplerate and channel count depending on a setting. It can handle incoming streams with both the current (future legacy) and new samplerate & channel count. These are url-encoded into the livekit track name

Release Notes:

- N/A
2025-09-25 15:11:12 +02:00
Cole Miller
783ba389f7 Fix script/zed-local on Windows (#38832)
There's a mismatch between the URL used here and the one that's referred
to in `build_zed_cloud_url`, which prevents using the script on Windows.

A previous PR changed the script to use `127.0.0.1` instead of
`localhost` because of supposed URL parsing issues, but we were unable
to reproduce those.

Release Notes:

- N/A
2025-09-25 09:03:27 -04:00
Kirill Bulatov
e72021a26b Implement perceptual gamma / contrast correction for Linux font rendering (#38862)
Part of https://github.com/zed-industries/zed/issues/7992
Port of https://github.com/zed-industries/zed/pull/37167 to Linux

When using Blade rendering (Linux platforms and self-compiled builds
with the Blade renderer enabled), Zed reads `ZED_FONTS_GAMMA` and
`ZED_FONTS_GRAYSCALE_ENHANCED_CONTRAST` environment variables for the
values to use for font rendering.

`ZED_FONTS_GAMMA` corresponds to
[getgamma](https://learn.microsoft.com/en-us/windows/win32/api/dwrite/nf-dwrite-idwriterenderingparams-getgamma)
values.
Allowed range [1.0, 2.2], other values are clipped.
Default: 1.8

`ZED_FONTS_GRAYSCALE_ENHANCED_CONTRAST` corresponds to
[getgrayscaleenhancedcontrast](https://learn.microsoft.com/en-us/windows/win32/api/dwrite_1/nf-dwrite_1-idwriterenderingparams1-getgrayscaleenhancedcontrast)
values.
Allowed range: [0.0, ..), other values are clipped.
Default: 1.0

Screenshots (left is Nightly, right is the new code):

* Non-lodpi display

With the defaults:

<img width="2560" height="1600" alt="image"
src="https://github.com/user-attachments/assets/987168b4-3f5f-45a0-a740-9c0e49efbb9c"
/>


With `env ZED_FONTS_GRAYSCALE_ENHANCED_CONTRAST=7777`: 

<img width="2560" height="1600" alt="image"
src="https://github.com/user-attachments/assets/893bc2c7-9db4-4874-8ef6-3425d079db63"
/>


Lodpi, default settings:
<img width="3830" height="2160" alt="image"
src="https://github.com/user-attachments/assets/ec009e00-69b3-4c01-a18c-8286e2015e74"
/>

Lodpi, font size 7:
<img width="3830" height="2160" alt="image"
src="https://github.com/user-attachments/assets/f33e3df6-971b-4e18-b425-53d3404b19be"
/>


Release Notes:

- Implement perceptual gamma / contrast correction for Linux font
rendering

---------

Co-authored-by: localcc <work@localcc.cc>
2025-09-25 16:02:27 +03:00
Agus Zubiaga
f25ace6be0 zeta2 cli: Output raw request (#38876)
Release Notes:

- N/A

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
Co-authored-by: Oleksiy Syvokon <oleksiy.syvokon@gmail.com>
2025-09-25 12:59:17 +00:00
Umesh Yadav
c627543b46 assistant_context: Fix thread_summary_model not getting used in Text Threads (#38859)
Closes https://github.com/zed-industries/zed/issues/37472

Release Notes:

- Fixed an issue in Text Threads where it was using `default_model` even
in case `thread_summary_model` was set.

---------

Signed-off-by: Umesh Yadav <git@umesh.dev>
2025-09-25 14:41:49 +02:00
Ben Brandt
f303a461c4 acp: Use ACP error types in read_text_file (#38863)
- Map path lookup and internal failures to acp::Error 
- Return INVALID_PARAMS for reads beyond EOF

Release Notes:

- acp: Return more informative error types from `read_text_file` to
agents
2025-09-25 11:53:36 +00:00
localcc
a9def8128f Adjust keymap to not conflict with the french keyboard layout (#38868)
Closes #38382 

Release Notes:

- N/A
2025-09-25 11:25:22 +00:00
Lukas Wirth
6580eac077 auto_update: Unmount update disk image in the background (#38867)
Release Notes:

- Fixed potentially temporarily hanging on macOS when updating the app
2025-09-25 11:13:40 +00:00
localcc
5c3c79d667 Fix file association icons on Windows (#38713)
This now uses the default zed icon for file associations as our own icon
svgs are black/white shapes which are not suitable to set as an icon in
a file explorer.

Closes #36286

Release Notes:

- N/A
2025-09-25 12:58:58 +02:00
Lukas Wirth
16fccb5c76 editor: Assert ordering in selections of resolve_selections (#38861)
Inspired by the recent anchor assertions, this asserts that the produced
selections are always ordered at various resolutions stages, this is an
invariant within `SelectionsCollection` but something breaks it
somewhere causing us to seek cursors backwards which panics.

Related to ZED-13X

Release Notes:

- N/A
2025-09-25 10:06:47 +00:00
Driftcell
a25504edaf file_finder: Leverage or-patterns and bindings to deduplicate prefix handling (#38860)
Just small code changes, to deduplicate prefix handling.

Release Notes:

- N/A
2025-09-25 10:01:28 +00:00
Ben Brandt
bc11844b2e acp: Fix read_text_file erroring on empty files (#38856)
The previous validation was too strict and didn't permit reading empty
files.

Addresses: https://github.com/google-gemini/gemini-cli/issues/9280

Release Notes:

- acp: Fix `read_text_file` returning errors for empty files
2025-09-25 09:15:50 +00:00
Martin Pool
10b99c6f55 RFC: Recommend and enable using Wild rather than Mold on Linux for local builds (#37717)
# Summary 

Today, Zed uses Mold on Linux, but Wild can be significantly faster. 

On my machine, Wild is 14% faster at a whole-tree clean build, 20%
faster on an incremental build with a minimal change, and makes no
measurable effect on runtime performance of tests.

However, Wild's page says it's not yet ready for production, so it seems
to early to switch for production and CI builds.

This PR keeps using Mold in CI and lets developers choose in their own
config what linker to use. (The downside of this is that after landing
this change, developers will have to do some local config or it will
fall back to the default linker which may be slower.)

[Wild 0.6 is out, and their announcement has some
benchmarks](https://davidlattimore.github.io/posts/2025/09/23/wild-update-0.6.0.html).

cc @davidlattimore from Wild, just fyi

# Tasks

- [x] Measure Wild build, incremental build, and runtime performance in
different scenarios
- [x] Remove the Linux linker config from `.cargo/config.toml` in the
tree
- [x] Test rope benchmarks etc
- [x] Set the linker to Mold in CI 
- [x] Add instructions to use Wild or Mold into `linux.md`
- [x] Add a script to download Wild
- [x] Measure binary size
- [x] Recommend Wild from `scripts/linux`

# Benchmarks 

| | wild 0.6 (rust 1.89) | mold 2.37.1 (1.89) | lld (rust 1.90) | wild
advantage |
| -- | -- | -- | -- | -- |
| clean workspace build | 176s | 184s | 182s | 5% faster than mold |
| nextest run workspace after build | 137s | 142s | 137s | in the noise?
|
| incremental rebuild | 3.9s | 5.0s | 6.6s | 22% faster than mold | 

I didn't observe any apparent significant change in runtime performance
or binary size, or in the in-tree microbenchmarks.

Release Notes:

- N/A

---------

Co-authored-by: Mateusz Mikuła <oss@mateuszmikula.dev>
2025-09-25 10:35:13 +02:00
Kirill Bulatov
17dea24533 Disable terminal breadcrumbs by default (#38806)
<img width="1211" height="238" alt="image"
src="https://github.com/user-attachments/assets/d847fabe-0e00-474c-ad79-cb4da221b319"
/>

At least on Windows, "git terminal" and PowerShell set the header, which
is not very useful but occupies space and sometimes confuses users:


![telegram-cloud-photo-size-2-5377720447174575846-x](https://github.com/user-attachments/assets/a889fa44-e879-4b3d-956b-0af959113e1e)

Release Notes:

- Disable terminal breadcrumbs by default. Set
`terminal.toolbar.breadcrumbs` to `true` to re-enable.

Co-authored-by: Finn Evers <finn@zed.dev>
2025-09-25 10:25:37 +02:00
Marshall Bowers
17e55daf6f Remove billing-v2 feature flag (#38843)
This PR removes the `billing-v2` feature flag, now that the new pricing
is launched.

Release Notes:

- N/A
2025-09-25 02:11:48 +00:00
Remy Suen
6b968e0118 Remove the duplicated Global LSP Settings section (#38811)
This section [shows up
twice](https://zed.dev/docs/configuring-zed#global-lsp-settings) in the
documentation.

<img width="701" height="1269" alt="image"
src="https://github.com/user-attachments/assets/4d930676-5cae-43c8-83d4-6406c27d149c"
/>

Release Notes:

- N/A

Signed-off-by: Remy Suen <remy.suen@docker.com>
2025-09-24 18:41:58 -06:00
Bartosz Kaszubowski
0f66310192 git_ui: Tweak appearance of repo and branch separator (#38447)
# Why

In Git Panel, it felt to me that repo and branch separator can be
slightly demphasized (since it is not-interactable) and separated a bit
more from the repo and branch popover triggers.

# How

Use `icon_muted` color for the separator (happy to know if this is an
abuse of the UI styleguide 😄), add one pixel horizontal spacing around
the `/` character.

Release Notes:

- Improved appearance of repo and branch separator in Git Commit Panel

# Test plan

I have tested the change locally and compared the UI before and after to
make sure it feels right.

### Before

<img width="466" height="196" alt="Screenshot 2025-09-18 at 20 25 46"
src="https://github.com/user-attachments/assets/7bfcd1a4-8d16-4e75-8660-9cbfa3952848"
/>

### After

<img width="466" height="196" alt="Screenshot 2025-09-18 at 20 25 12"
src="https://github.com/user-attachments/assets/100d3599-ecc6-473f-b270-a71005b41494"
/>

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-09-25 00:39:29 +00:00
warrenjokinen
26adc70ae6 docs: Update glossary (#38820)
Added blank line in front of 2 image tags so markdown renders correctly
in zed. (Previously, images were skipped. They are also skipped in zed
if there are leading spaces in front of img tag.)

Updated text in 3 alt tags.

Fixed 1 typo.

Release Notes:

- N/A
2025-09-24 20:27:17 -04:00
Danilo Leal
a5fb290252 docs: Add stray design tweaks (#38835)
Tiny little improvements opportunities I noticed today while browsing
the docs.

Release Notes:

- N/A
2025-09-25 00:10:42 +00:00
Michael Sloan
8fc7bd9ae8 zeta2: Add labeled sections prompt format (#38828)
Release Notes:

- N/A

Co-authored-by: Agus <agus@zed.dev>
2025-09-25 00:07:43 +00:00
Smit Barmase
7167be5889 editor: Fix predict edit at cursor action when show_edit_predictions is false (#38821)
Closes #37601 

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

Edit: Original issue https://github.com/zed-industries/zed/issues/25744
is fixed for Zeta in this PR. For Copilot, it will be covered in a
follow-up. In the case of Copilot, even after discarding, we still get a
prediction on suggest, which is a bug.

Release Notes:

- Fixed issue where predict edit at cursor didn't work when
`show_edit_predictions` is `false`.
2025-09-25 05:28:32 +05:30
Cole Miller
d321cf93ba Fix semantic merge conflict from RelPath refactor (#38829)
Release Notes:

- N/A

Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
2025-09-24 23:27:11 +00:00
Conrad Irwin
ce7b02e3a1 Whitespace map more (#38827)
Release Notes:

- N/A
2025-09-24 23:11:40 +00:00
Max Brunsfeld
03f9cf4414 Represent relative paths using a dedicated, separator-agnostic type (#38744)
Closes https://github.com/zed-industries/zed/issues/38690
Closes #37353

### Background

On Windows, paths are normally separated by `\`, unlike mac and linux
where they are separated by `/`. When editing code in a project that
uses a different path style than your local system (e.g. remoting from
Windows to Linux, using WSL, and collaboration between windows and unix
users), the correct separator for a path may differ from the "native"
separator.

Previously, to work around this, Zed converted paths' separators in
numerous places. This was applied to both absolute and relative paths,
leading to incorrect conversions in some cases.

### Solution

Many code paths in Zed use paths that are *relative* to either a
worktree root or a git repository. This PR introduces a dedicated type
for these paths called `RelPath`, which stores the path in the same way
regardless of host platform, and offers `Path`-like manipulation APIs.
RelPath supports *displaying* the path using either separator, so that
we can display paths in a style that is determined at runtime based on
the current project.

The representation of absolute paths is left untouched, for now.
Absolute paths are different from relative paths because (except in
contexts where we know that the path refers to the local filesystem)
they should generally be treated as opaque strings. Currently we use a
mix of types for these paths (std::path::Path, String, SanitizedPath).

Release Notes:

- N/A

---------

Co-authored-by: Cole Miller <cole@zed.dev>
Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
Co-authored-by: Peter Tripp <petertripp@gmail.com>
Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
Co-authored-by: Lukas Wirth <me@lukaswirth.dev>
2025-09-24 18:57:33 -04:00
Conrad Irwin
3c626f3758 Only allow single chars for whitespace map (#38825)
Release Notes:

- Only allow single characters in the whitespace map
2025-09-24 16:18:00 -06:00
Joseph T. Lyons
4a1bab52f3 Update release process docs to include storing feature media (#38824)
Release Notes:

- N/A
2025-09-24 21:52:02 +00:00
Conrad Irwin
91b0f42382 Fix panic when hovering string ending with unicode (#38818)
Release Notes:

- Fixed a panic when hovering a string literal ending with an emoji
2025-09-24 15:33:31 -06:00
Ben Kunkle
523c042930 settings_ui: Collect all settings files (#38816)
Closes #ISSUE

Updates the settings editor to collect all known settings files from the
settings store, in order to show them in the UI. Additionally adds a
fake worktree instantiation in the settings UI example binary in order
to have more than one file available when testing.

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-09-24 21:16:06 +00:00
Victor Tran
ed7bd5a8ed gpui: Flash menu in menubar on macOS when action is triggered (#38588)
On macOS, traditionally when a keyboard shortcut is activated, the menu
in the menu bar flashes to indicate that the action was recognised.

<img width="289" height="172" alt="image"
src="https://github.com/user-attachments/assets/a03ecd2f-f159-4f82-b4fd-227f34393703"
/>

This PR adds this functionality to GPUI, where when a keybind is pressed
that triggers an action in the menu, the menu flashes.

Release Notes:

- N/A
2025-09-24 12:09:03 -07:00
Lukas Wirth
8ebe4fa149 gpui_macros: Hide inner test function from project symbols (#38809)
This makes rust-analyzer not consider the function for project symbols,
meaning searching for tests wont show two entries.

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-09-24 18:07:34 +00:00
Bennet Bo Fenner
6b646e3a14 zeta2: Support edit prediction: clear history (#38808)
Release Notes:

- N/A

Co-authored-by: Agus Zubiaga <agus@zed.dev>
2025-09-24 17:44:03 +00:00
Ben Kunkle
e653cc90c5 Clean up last remnants of Settings UI v1 (#38803)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-09-24 17:02:32 +00:00
Danilo Leal
0794de71e3 docs: Update note about agent message editor setting (#38805)
As of stable 206.0, the `agent.message_editor_min_lines` setting is
fully available, so removing the docs note that said it was only for
Preview.

Release Notes:

- N/A
2025-09-24 13:57:30 -03:00
Anthony Eid
2b283e7c53 Revert "Fix UTF-8 character boundary panic in DirectWrite text ... (#37767)" (#38800)
This reverts commit 9e7302520e.

I run into an infinite hang in Zed nightly and used instruments and
activity monitor to sample what was going on. The root cause seemed to
be the unwrap_unchecked introduced in reverted PR.

Release Notes:

- N/A
2025-09-24 16:44:39 +00:00
Joseph T. Lyons
45a4277026 Add community champion auto labeler (#38802)
Release Notes:

- N/A
2025-09-24 16:42:01 +00:00
Kirill Bulatov
fa76b6ce06 Switch to "standard" as a default line height in the terminal (#38798)
Closes https://github.com/zed-industries/zed/issues/38686

Release Notes:

- Switched to "standard" as a default line height in the terminal
2025-09-24 16:36:35 +00:00
morgankrey
a13e3a8af3 Docs updates September (#38796)
Closes #ISSUE

Release Notes:

- N/A

---------

Co-authored-by: Katie Geer <katie@zed.dev>
Co-authored-by: Marshall Bowers <git@maxdeviant.com>
Co-authored-by: David Kleingeld <davidsk@zed.dev>
2025-09-24 11:10:58 -05:00
Nia
39370bceb2 perf: Bugfixes (#38725)
Release Notes:

- N/A
2025-09-24 16:03:08 +00:00
Mikayla Maki
53885c00d3 Start up settings UI 2 (#38673)
Release Notes:

- N/A

---------

Co-authored-by: Anthony <hello@anthonyeid.me>
Co-authored-by: Ben Kunkle <ben@zed.dev>
Co-authored-by: Anthony <anthony@zed.dev>
Co-authored-by: Ben Kunkle <ben.kunkle@gmail.com>
2025-09-24 15:45:14 +00:00
Danilo Leal
6f3e66d027 Adjust stash picker design (#38789)
Just making it more consistent with other pickers—button actions
justified to the right and timestamp directly in the list item to avoid
as much as possible relevant information tucked away in a tooltip where
using the keyboard will mostly be the main mean of interaction.

<img width="500" height="310" alt="Screenshot 2025-09-24 at 10  41@2x"
src="https://github.com/user-attachments/assets/0bd478da-d1a6-48fe-ade7-a4759d175c60"
/>


Release Notes:

- N/A
2025-09-24 13:59:42 +00:00
Agus Zubiaga
b3f9be6e9c zeta2: Split up crate into modules (#38788)
Split up provider, prediction, and global into modules.

Release Notes:

- N/A
2025-09-24 13:40:29 +00:00
Agus Zubiaga
4353b61155 zeta2: Compute smaller edits (#38786)
The new cloud endpoint returns structured edits, but they may include
more of the input excerpt than what we want to display in the preview,
so we compute a smaller diff on the client side against the snapshot.

Release Notes:

- N/A
2025-09-24 13:10:52 +00:00
Lukas Wirth
e1b57f00a0 sum_tree: Reduce Cursor size for contextless summary types (#38776)
This reduces the size of cursor by a usize when the summary does not
require a context making Cursor usages and constructions slightly more
efficient.

This change is a bit annoying though, as Rust has no means of
specializing, so this uses a `ContextlessSummary` trait with a blanket
impl while turning the `Context` into a GAT `Context<'a>`. This means
`Summary` implies are a bit more verbose now while contextless ones are
slimmer. It does come with the downside that the lifetime in the GAT is
always considered invariant, so some lifetime splitting occurred due to
that.


 ```
push/4096               time:   [352.65 µs 360.87 µs 367.80 µs]
                        thrpt:  [10.621 MiB/s 10.825 MiB/s 11.077 MiB/s]
                 change:
time: [-2.6633% -1.3640% -0.0561%] (p = 0.05 < 0.05)
                        thrpt:  [+0.0561% +1.3828% +2.7361%]
                        Change within noise threshold.
Found 16 outliers among 100 measurements (16.00%)
  7 (7.00%) low severe
  3 (3.00%) low mild
  2 (2.00%) high mild
  4 (4.00%) high severe
push/65536              time:   [1.2917 ms 1.2949 ms 1.2979 ms]
                        thrpt:  [48.156 MiB/s 48.267 MiB/s 48.387 MiB/s]
                 change:
time: [+1.4428% +1.9844% +2.5299%] (p = 0.00 < 0.05)
                        thrpt:  [-2.4675% -1.9458% -1.4223%]
                        Performance has regressed.
Found 3 outliers among 100 measurements (3.00%)
  1 (1.00%) low severe
  1 (1.00%) low mild
  1 (1.00%) high severe

append/4096             time:   [677.87 ns 678.87 ns 679.83 ns]
                        thrpt:  [5.6112 GiB/s 5.6192 GiB/s 5.6274 GiB/s]
                 change:
time: [-0.8924% -0.5017% -0.1705%] (p = 0.00 < 0.05)
                        thrpt:  [+0.1708% +0.5043% +0.9004%]
                        Change within noise threshold.
Found 2 outliers among 100 measurements (2.00%)
  1 (1.00%) low mild
  1 (1.00%) high mild
append/65536            time:   [9.3275 µs 9.3406 µs 9.3536 µs]
                        thrpt:  [6.5253 GiB/s 6.5344 GiB/s 6.5435 GiB/s]
                 change:
time: [+0.5409% +0.7215% +0.9054%] (p = 0.00 < 0.05)
                        thrpt:  [-0.8973% -0.7163% -0.5380%]
                        Change within noise threshold.

slice/4096              time:   [27.673 µs 27.791 µs 27.907 µs]
                        thrpt:  [139.97 MiB/s 140.56 MiB/s 141.16 MiB/s]
                 change:
time: [-1.1065% -0.6725% -0.2429%] (p = 0.00 < 0.05)
                        thrpt:  [+0.2435% +0.6770% +1.1189%]
                        Change within noise threshold.
Found 5 outliers among 100 measurements (5.00%)
  4 (4.00%) low mild
  1 (1.00%) high mild
slice/65536             time:   [507.55 µs 517.40 µs 535.60 µs]
                        thrpt:  [116.69 MiB/s 120.80 MiB/s 123.14 MiB/s]
                 change:
time: [-1.3489% +0.0599% +2.2591%] (p = 0.96 > 0.05)
                        thrpt:  [-2.2092% -0.0598% +1.3674%]
                        No change in performance detected.
Found 8 outliers among 100 measurements (8.00%)
  5 (5.00%) low mild
  2 (2.00%) high mild
  1 (1.00%) high severe

bytes_in_range/4096     time:   [3.3917 µs 3.4108 µs 3.4313 µs]
                        thrpt:  [1.1117 GiB/s 1.1184 GiB/s 1.1247 GiB/s]
                 change:
time: [-5.3466% -4.7193% -4.1262%] (p = 0.00 < 0.05)
                        thrpt:  [+4.3038% +4.9531% +5.6487%]
                        Performance has improved.
Found 6 outliers among 100 measurements (6.00%)
  1 (1.00%) low mild
  5 (5.00%) high mild
bytes_in_range/65536    time:   [88.175 µs 88.613 µs 89.111 µs]
                        thrpt:  [701.37 MiB/s 705.31 MiB/s 708.82 MiB/s]
                 change:
time: [-0.6935% +0.3769% +1.4655%] (p = 0.50 > 0.05)
                        thrpt:  [-1.4443% -0.3755% +0.6984%]
                        No change in performance detected.
Found 2 outliers among 100 measurements (2.00%)
  2 (2.00%) high mild

chars/4096              time:   [678.70 ns 680.38 ns 682.08 ns]
                        thrpt:  [5.5927 GiB/s 5.6067 GiB/s 5.6206 GiB/s]
                 change:
time: [-0.6969% -0.2755% +0.1485%] (p = 0.20 > 0.05)
                        thrpt:  [-0.1483% +0.2763% +0.7018%]
                        No change in performance detected.
Found 9 outliers among 100 measurements (9.00%)
  5 (5.00%) low mild
  4 (4.00%) high mild
chars/65536             time:   [12.720 µs 12.775 µs 12.830 µs]
                        thrpt:  [4.7573 GiB/s 4.7778 GiB/s 4.7983 GiB/s]
                 change:
time: [-0.6172% -0.1110% +0.4179%] (p = 0.68 > 0.05)
                        thrpt:  [-0.4162% +0.1112% +0.6211%]
                        No change in performance detected.
Found 2 outliers among 100 measurements (2.00%)
  1 (1.00%) low mild
  1 (1.00%) high mild

clip_point/4096         time:   [33.240 µs 33.310 µs 33.394 µs]
                        thrpt:  [116.98 MiB/s 117.27 MiB/s 117.52 MiB/s]
                 change:
time: [-2.8892% -2.6305% -2.3438%] (p = 0.00 < 0.05)
                        thrpt:  [+2.4000% +2.7015% +2.9751%]
                        Performance has improved.
Found 12 outliers among 100 measurements (12.00%)
  1 (1.00%) low mild
  4 (4.00%) high mild
  7 (7.00%) high severe
clip_point/65536        time:   [1.6531 ms 1.6586 ms 1.6640 ms]
                        thrpt:  [37.560 MiB/s 37.683 MiB/s 37.808 MiB/s]
                 change:
time: [-6.6381% -5.9395% -5.2680%] (p = 0.00 < 0.05)
                        thrpt:  [+5.5610% +6.3146% +7.1100%]
                        Performance has improved.
Found 7 outliers among 100 measurements (7.00%)
  1 (1.00%) low mild
  2 (2.00%) high mild
  4 (4.00%) high severe

point_to_offset/4096    time:   [11.586 µs 11.603 µs 11.621 µs]
                        thrpt:  [336.15 MiB/s 336.67 MiB/s 337.16 MiB/s]
                 change:
time: [-14.289% -14.111% -13.939%] (p = 0.00 < 0.05)
                        thrpt:  [+16.197% +16.429% +16.672%]
                        Performance has improved.
Found 12 outliers among 100 measurements (12.00%)
  3 (3.00%) low severe
  5 (5.00%) low mild
  4 (4.00%) high mild
point_to_offset/65536   time:   [527.74 µs 532.08 µs 536.51 µs]
                        thrpt:  [116.49 MiB/s 117.46 MiB/s 118.43 MiB/s]
                 change:
time: [-6.7825% -4.6235% -2.3533%] (p = 0.00 < 0.05)
                        thrpt:  [+2.4100% +4.8477% +7.2760%]
                        Performance has improved.
Found 8 outliers among 100 measurements (8.00%)
  4 (4.00%) high mild
  4 (4.00%) high severe

cursor/4096             time:   [16.154 µs 16.192 µs 16.232 µs]
                        thrpt:  [240.66 MiB/s 241.24 MiB/s 241.81 MiB/s]
                 change:
time: [-3.2536% -2.9145% -2.5526%] (p = 0.00 < 0.05)
                        thrpt:  [+2.6194% +3.0019% +3.3630%]
                        Performance has improved.
Found 5 outliers among 100 measurements (5.00%)
  1 (1.00%) low mild
  2 (2.00%) high mild
  2 (2.00%) high severe
cursor/65536            time:   [509.60 µs 511.24 µs 512.93 µs]
                        thrpt:  [121.85 MiB/s 122.25 MiB/s 122.65 MiB/s]
                 change:
time: [-7.3677% -6.6017% -5.7840%] (p = 0.00 < 0.05)
                        thrpt:  [+6.1391% +7.0683% +7.9537%]
                        Performance has improved.
Found 6 outliers among 100 measurements (6.00%)
  3 (3.00%) high mild
  3 (3.00%) high severe
```
Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-09-24 14:35:38 +02:00
Oleksiy Syvokon
c5219e8fd2 agent: Clean up git exclusions after emergency (#38775)
In some rare cases, the auto-generated block gets stuck in
`.git/info/exclude`. We now auto-clean it.

Closes #38374

Release Notes:

- Remove auto-generated block from git excludes if it gets stuck there.
2025-09-24 10:58:39 +00:00
Piotr Osiewicz
5612a961b0 windows: Do not attempt to encrypt empty encrypted strings (#38774)
Related to #38427

Release Notes:

* N/A
2025-09-24 10:27:45 +00:00
Lukas Wirth
c53e5ba397 editor: Fix invalid anchors in hover_links::surrounding_filename (#38766)
Fixes ZED-1K3

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-09-24 08:30:11 +00:00
tidely
d5a99d079e ollama: Remove dead code (#38550)
The `Duration` argument in `get_models` has been unused for over a year.

The `complete` function is also unused and it has fallen behind in new
feature additions such as Authorization support. This used to exist
because ollama didn't support tools in streaming mode, `with_tools` also
existed because of that. Now however there is no reason to keep this
around.

`ChatResponseDelta ` had unnecessary `#[allow(unused)]` macros since the
fields are marked `pub`. Using `#[expect(unused)]` would've caught this.

Release Notes:

- N/A
2025-09-24 02:19:52 -06:00
Lukas Wirth
9418a2f4bc editor: Prevent panics in BlockChunks if the block spans more than 128 lines (#38763)
Not an ideal fix, but a proper one will require restructuring the
iterator state (which would be easier if Rust had first class
generators)
Fixes ZED-1MB

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-09-24 08:10:56 +00:00
Santiago Bernhardt
880fff471c ollama: Add support for qwen3-coder (#38608)
Release Notes:

- N/A
2025-09-24 02:09:40 -06:00
Michael Sloan
5f6ae2361f Delete unused types for Mistral non-streaming requests (#38758)
Confusing to have these interspersed with the streaming request types

Release Notes:

- N/A
2025-09-24 04:31:06 +00:00
Conrad Irwin
5d89b2ea26 Revert "Add setting to show/hide title bar (#37428)" (#38756)
Closes https://github.com/zed-industries/zed/issues/38547

Release Notes:

- Reverted the ability to show/hide the titlebar. This caused rendering
bugs on
macOS, and we're preparing for the redesign which requires the toolbar
being present.

---------

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2025-09-24 07:15:30 +03:00
492 changed files with 15701 additions and 11031 deletions

View File

@@ -10,3 +10,15 @@
# Here, we opted to use `[target.'cfg(all())']` instead of `[build]` because `[target.'**']` is guaranteed to be cumulative.
[target.'cfg(all())']
rustflags = ["-D", "warnings"]
# Use Mold on Linux, because it's faster than GNU ld and LLD.
#
# We no longer set this in the default `config.toml` so that developers can opt in to Wild, which
# is faster than Mold, in their own ~/.cargo/config.toml.
[target.x86_64-unknown-linux-gnu]
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
[target.aarch64-unknown-linux-gnu]
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=mold"]

View File

@@ -4,16 +4,9 @@ rustflags = ["-C", "symbol-mangling-version=v0", "--cfg", "tokio_unstable"]
[alias]
xtask = "run --package xtask --"
perf-test = ["test", "--profile", "release-fast", "--lib", "--bins", "--tests", "--config", "target.'cfg(true)'.runner='cargo run -p perf --release'", "--config", "target.'cfg(true)'.rustflags=[\"--cfg\", \"perf_enabled\"]"]
perf-compare = ["run", "--release", "-p", "perf", "--", "compare"]
[target.x86_64-unknown-linux-gnu]
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
[target.aarch64-unknown-linux-gnu]
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
perf-test = ["test", "--profile", "release-fast", "--lib", "--bins", "--tests", "--all-features", "--config", "target.'cfg(true)'.runner='cargo run -p perf --release'", "--config", "target.'cfg(true)'.rustflags=[\"--cfg\", \"perf_enabled\"]"]
# Keep similar flags here to share some ccache
perf-compare = ["run", "--profile", "release-fast", "-p", "perf", "--config", "target.'cfg(true)'.rustflags=[\"--cfg\", \"perf_enabled\"]", "--", "compare"]
[target.'cfg(target_os = "windows")']
rustflags = [

View File

@@ -26,7 +26,7 @@ third-party = [
# build of remote_server should not include scap / its x11 dependency
{ name = "scap", git = "https://github.com/zed-industries/scap", rev = "808aa5c45b41e8f44729d02e38fd00a2fe2722e7" },
# build of remote_server should not need to include on libalsa through rodio
{ name = "rodio", git = "https://github.com/RustAudio/rodio", branch = "better_wav_output"},
{ name = "rodio", git = "https://github.com/RustAudio/rodio" },
]
[final-excludes]

View File

@@ -0,0 +1,48 @@
name: Community Champion Auto Labeler
on:
issues:
types: [opened]
pull_request_target:
types: [opened]
jobs:
label_community_champion:
if: github.repository_owner == 'zed-industries'
runs-on: ubuntu-latest
steps:
- name: Check if author is a community champion and apply label
uses: actions/github-script@v7
with:
script: |
const communityChampionBody = `${{ secrets.COMMUNITY_CHAMPIONS }}`;
const communityChampions = communityChampionBody
.split('\n')
.map(handle => handle.trim().toLowerCase());
let author;
if (context.eventName === 'issues') {
author = context.payload.issue.user.login;
} else if (context.eventName === 'pull_request_target') {
author = context.payload.pull_request.user.login;
}
if (!author || !communityChampions.includes(author.toLowerCase())) {
return;
}
const issueNumber = context.payload.issue?.number || context.payload.pull_request?.number;
try {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
labels: ['community champion']
});
console.log(`Applied 'community champion' label to #${issueNumber} by ${author}`);
} catch (error) {
console.error(`Failed to apply label: ${error.message}`);
}

View File

@@ -63,6 +63,7 @@ Although there are few hard and fast rules, typically we don't merge:
- New file icons. Zed's default icon theme consists of icons that are hand-designed to fit together in a cohesive manner, please don't submit PRs with off-the-shelf SVGs.
- Giant refactorings.
- Non-trivial changes with no tests.
- Stylistic code changes that do not alter any app logic. Reducing allocations, removing `.unwrap()`s, fixing typos is great; making code "more readable" — maybe not so much.
- Features where (in our subjective opinion) the extra complexity isn't worth it for the number of people who will benefit.
- Anything that seems completely AI generated.

126
Cargo.lock generated
View File

@@ -195,9 +195,9 @@ dependencies = [
[[package]]
name = "agent-client-protocol"
version = "0.4.2"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00e33b9f4bd34d342b6f80b7156d3a37a04aeec16313f264001e52d6a9118600"
checksum = "3aaa2bd05a2401887945f8bfd70026e90bc3cf96c62ab9eba2779835bf21dc60"
dependencies = [
"anyhow",
"async-broadcast",
@@ -486,7 +486,6 @@ dependencies = [
"client",
"cloud_llm_client",
"component",
"feature_flags",
"gpui",
"language_model",
"serde",
@@ -892,6 +891,7 @@ dependencies = [
"serde",
"serde_json",
"ui",
"util",
"workspace",
"workspace-hack",
]
@@ -1405,11 +1405,13 @@ dependencies = [
"async-tar",
"collections",
"crossbeam",
"denoise",
"gpui",
"libwebrtc",
"log",
"parking_lot",
"rodio",
"rubato",
"serde",
"settings",
"smol",
@@ -3695,6 +3697,7 @@ dependencies = [
"paths",
"project",
"rpc",
"semver",
"serde",
"serde_json",
"settings",
@@ -5123,7 +5126,6 @@ dependencies = [
"client",
"gpui",
"language",
"project",
"workspace-hack",
]
@@ -5170,6 +5172,7 @@ dependencies = [
"collections",
"futures 0.3.31",
"gpui",
"hashbrown 0.15.3",
"indoc",
"itertools 0.14.0",
"language",
@@ -6267,6 +6270,7 @@ dependencies = [
"bitflags 2.9.0",
"core-foundation 0.10.0",
"fsevent-sys 3.1.0",
"log",
"parking_lot",
"tempfile",
"workspace-hack",
@@ -7086,6 +7090,7 @@ dependencies = [
"parking_lot",
"pathfinder_geometry",
"postage",
"pretty_assertions",
"profiling",
"rand 0.9.1",
"raw-window-handle",
@@ -8386,6 +8391,28 @@ dependencies = [
"thiserror 1.0.69",
]
[[package]]
name = "json_schema_store"
version = "0.1.0"
dependencies = [
"anyhow",
"dap",
"extension",
"gpui",
"language",
"paths",
"project",
"schemars 1.0.1",
"serde",
"serde_json",
"settings",
"snippet_provider",
"task",
"theme",
"util",
"workspace-hack",
]
[[package]]
name = "jsonschema"
version = "0.30.0"
@@ -8473,6 +8500,7 @@ dependencies = [
"fuzzy",
"gpui",
"itertools 0.14.0",
"json_schema_store",
"language",
"log",
"menu",
@@ -8680,7 +8708,6 @@ dependencies = [
"credentials_provider",
"deepseek",
"editor",
"feature_flags",
"fs",
"futures 0.3.31",
"google_ai",
@@ -8705,7 +8732,6 @@ dependencies = [
"settings",
"smol",
"strum 0.27.1",
"theme",
"thiserror 2.0.12",
"tiktoken-rs",
"tokio",
@@ -8792,17 +8818,16 @@ dependencies = [
"async-trait",
"chrono",
"collections",
"dap",
"futures 0.3.31",
"gpui",
"http_client",
"itertools 0.14.0",
"json_schema_store",
"language",
"log",
"lsp",
"node_runtime",
"parking_lot",
"paths",
"pet",
"pet-conda",
"pet-core",
@@ -8815,7 +8840,6 @@ dependencies = [
"regex",
"rope",
"rust-embed",
"schemars 1.0.1",
"serde",
"serde_json",
"serde_json_lenient",
@@ -8823,7 +8847,6 @@ dependencies = [
"sha2",
"shlex",
"smol",
"snippet_provider",
"task",
"tempfile",
"text",
@@ -12094,7 +12117,6 @@ dependencies = [
"markdown",
"node_runtime",
"parking_lot",
"pathdiff",
"paths",
"postage",
"prettier",
@@ -12147,7 +12169,6 @@ dependencies = [
"git",
"git_ui",
"gpui",
"indexmap 2.9.0",
"language",
"menu",
"pretty_assertions",
@@ -13043,6 +13064,7 @@ dependencies = [
"gpui",
"gpui_tokio",
"http_client",
"json_schema_store",
"language",
"language_extension",
"language_model",
@@ -13401,7 +13423,7 @@ dependencies = [
[[package]]
name = "rodio"
version = "0.21.1"
source = "git+https://github.com/RustAudio/rodio?branch=better_wav_output#82514bd1f2c6cfd9a1a885019b26a8ffea75bc5c"
source = "git+https://github.com/RustAudio/rodio#e2074c6c2acf07b57cf717e076bdda7a9ac6e70b"
dependencies = [
"cpal",
"dasp_sample",
@@ -13489,6 +13511,18 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad8388ea1a9e0ea807e442e8263a699e7edcb320ecbcd21b4fa8ff859acce3ba"
[[package]]
name = "rubato"
version = "0.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5258099699851cfd0082aeb645feb9c084d9a5e1f1b8d5372086b989fc5e56a1"
dependencies = [
"num-complex",
"num-integer",
"num-traits",
"realfft",
]
[[package]]
name = "rules_library"
version = "0.1.0"
@@ -14463,6 +14497,7 @@ dependencies = [
"serde_with",
"settings_macros",
"smallvec",
"strum 0.27.1",
"tree-sitter",
"tree-sitter-json",
"unindent",
@@ -14502,6 +14537,36 @@ dependencies = [
"zed_actions",
]
[[package]]
name = "settings_ui"
version = "0.1.0"
dependencies = [
"anyhow",
"assets",
"client",
"command_palette_hooks",
"editor",
"feature_flags",
"fs",
"futures 0.3.31",
"gpui",
"language",
"menu",
"node_runtime",
"paths",
"project",
"serde",
"session",
"settings",
"strum 0.27.1",
"theme",
"ui",
"util",
"workspace",
"workspace-hack",
"zlog",
]
[[package]]
name = "sha1"
version = "0.10.6"
@@ -14779,6 +14844,7 @@ dependencies = [
"paths",
"schemars 1.0.1",
"serde",
"serde_json",
"serde_json_lenient",
"snippet",
"util",
@@ -16040,6 +16106,7 @@ dependencies = [
"gpui",
"itertools 0.14.0",
"libc",
"log",
"rand 0.9.1",
"regex",
"release_channel",
@@ -17023,8 +17090,9 @@ dependencies = [
[[package]]
name = "tree-sitter-python"
version = "0.23.6"
source = "git+https://github.com/zed-industries/tree-sitter-python?rev=218fcbf3fda3d029225f3dec005cb497d111b35e#218fcbf3fda3d029225f3dec005cb497d111b35e"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6bf85fd39652e740bf60f46f4cda9492c3a9ad75880575bf14960f775cb74a1c"
dependencies = [
"cc",
"tree-sitter-language",
@@ -17497,6 +17565,7 @@ dependencies = [
"libc",
"log",
"nix 0.29.0",
"pretty_assertions",
"rand 0.9.1",
"regex",
"rust-embed",
@@ -17654,6 +17723,7 @@ dependencies = [
"multi_buffer",
"nvim-rs",
"parking_lot",
"perf",
"picker",
"project",
"project_panel",
@@ -19691,7 +19761,6 @@ dependencies = [
"nix 0.29.0",
"nix 0.30.1",
"nom 7.1.3",
"num",
"num-bigint",
"num-bigint-dig",
"num-complex",
@@ -20207,6 +20276,7 @@ dependencies = [
"install_cli",
"itertools 0.14.0",
"journal",
"json_schema_store",
"keymap_editor",
"language",
"language_extension",
@@ -20253,6 +20323,7 @@ dependencies = [
"session",
"settings",
"settings_profile_selector",
"settings_ui",
"shellexpand 2.1.2",
"smol",
"snippet_provider",
@@ -20281,6 +20352,7 @@ dependencies = [
"url",
"urlencoding",
"util",
"util_macros",
"uuid",
"vim",
"vim_mode_setting",
@@ -20339,6 +20411,17 @@ dependencies = [
"wit-bindgen 0.41.0",
]
[[package]]
name = "zed_extension_api"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0729d50b4ca0a7e28e590bbe32e3ca0194d97ef654961451a424c661a366fca0"
dependencies = [
"serde",
"serde_json",
"wit-bindgen 0.41.0",
]
[[package]]
name = "zed_glsl"
version = "0.1.0"
@@ -20348,9 +20431,9 @@ dependencies = [
[[package]]
name = "zed_html"
version = "0.2.2"
version = "0.2.3"
dependencies = [
"zed_extension_api 0.1.0",
"zed_extension_api 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@@ -20581,18 +20664,23 @@ dependencies = [
"arrayvec",
"chrono",
"client",
"clock",
"cloud_llm_client",
"cloud_zeta2_prompt",
"edit_prediction",
"edit_prediction_context",
"futures 0.3.31",
"gpui",
"indoc",
"language",
"language_model",
"log",
"lsp",
"pretty_assertions",
"project",
"release_channel",
"serde_json",
"settings",
"thiserror 2.0.12",
"util",
"uuid",
@@ -20608,6 +20696,7 @@ dependencies = [
"chrono",
"clap",
"client",
"cloud_llm_client",
"collections",
"edit_prediction_context",
"editor",
@@ -20638,6 +20727,7 @@ dependencies = [
"anyhow",
"clap",
"client",
"cloud_llm_client",
"cloud_zeta2_prompt",
"debug_adapter_extension",
"edit_prediction_context",

View File

@@ -91,6 +91,7 @@ members = [
"crates/inspector_ui",
"crates/install_cli",
"crates/journal",
"crates/json_schema_store",
"crates/keymap_editor",
"crates/language",
"crates/language_extension",
@@ -151,6 +152,7 @@ members = [
"crates/settings",
"crates/settings_macros",
"crates/settings_profile_selector",
"crates/settings_ui",
"crates/snippet",
"crates/snippet_provider",
"crates/snippets_ui",
@@ -321,6 +323,7 @@ zeta2_tools = { path = "crates/zeta2_tools" }
inspector_ui = { path = "crates/inspector_ui" }
install_cli = { path = "crates/install_cli" }
journal = { path = "crates/journal" }
json_schema_store = { path = "crates/json_schema_store" }
keymap_editor = { path = "crates/keymap_editor" }
language = { path = "crates/language" }
language_extension = { path = "crates/language_extension" }
@@ -375,7 +378,7 @@ remote_server = { path = "crates/remote_server" }
repl = { path = "crates/repl" }
reqwest_client = { path = "crates/reqwest_client" }
rich_text = { path = "crates/rich_text" }
rodio = { git = "https://github.com/RustAudio/rodio", branch = "better_wav_output"}
rodio = { git = "https://github.com/RustAudio/rodio" }
rope = { path = "crates/rope" }
rpc = { path = "crates/rpc" }
rules_library = { path = "crates/rules_library" }
@@ -383,6 +386,7 @@ search = { path = "crates/search" }
semantic_version = { path = "crates/semantic_version" }
session = { path = "crates/session" }
settings = { path = "crates/settings" }
settings_macros = { path = "crates/settings_macros" }
settings_ui = { path = "crates/settings_ui" }
snippet = { path = "crates/snippet" }
snippet_provider = { path = "crates/snippet_provider" }
@@ -439,7 +443,7 @@ zlog_settings = { path = "crates/zlog_settings" }
# External crates
#
agent-client-protocol = { version = "0.4.2", features = ["unstable"] }
agent-client-protocol = { version = "0.4.3", features = ["unstable"] }
aho-corasick = "1.1"
alacritty_terminal = "0.25.1-rc1"
any_vec = "0.14"
@@ -510,6 +514,7 @@ futures-lite = "1.13"
git2 = { version = "0.20.1", default-features = false }
globset = "0.4"
handlebars = "4.3"
hashbrown = "0.15.3"
heck = "0.5"
heed = { version = "0.21.0", features = ["read-txn-no-tls"] }
hex = "0.4.3"
@@ -681,7 +686,7 @@ tree-sitter-html = "0.23"
tree-sitter-jsdoc = "0.23"
tree-sitter-json = "0.24"
tree-sitter-md = { git = "https://github.com/tree-sitter-grammars/tree-sitter-markdown", rev = "9a23c1a96c0513d8fc6520972beedd419a973539" }
tree-sitter-python = { git = "https://github.com/zed-industries/tree-sitter-python", rev = "218fcbf3fda3d029225f3dec005cb497d111b35e" }
tree-sitter-python = "0.25"
tree-sitter-regex = "0.24"
tree-sitter-ruby = "0.23"
tree-sitter-rust = "0.24"
@@ -808,6 +813,7 @@ image_viewer = { codegen-units = 1 }
edit_prediction_button = { codegen-units = 1 }
install_cli = { codegen-units = 1 }
journal = { codegen-units = 1 }
json_schema_store = { codegen-units = 1 }
lmstudio = { codegen-units = 1 }
menu = { codegen-units = 1 }
notifications = { codegen-units = 1 }
@@ -859,6 +865,7 @@ todo = "deny"
declare_interior_mutable_const = "deny"
redundant_clone = "deny"
disallowed_methods = "deny"
# We currently do not restrict any style rules
# as it slows down shipping code to Zed.

View File

@@ -345,7 +345,7 @@
}
},
{
"context": "AcpThread > Editor",
"context": "AcpThread > Editor && !use_modifier_to_send",
"use_key_equivalents": true,
"bindings": {
"enter": "agent::Chat",
@@ -355,6 +355,17 @@
"shift-tab": "agent::CycleModeSelector"
}
},
{
"context": "AcpThread > Editor && use_modifier_to_send",
"use_key_equivalents": true,
"bindings": {
"ctrl-enter": "agent::Chat",
"ctrl-shift-r": "agent::OpenAgentDiff",
"ctrl-shift-y": "agent::KeepAll",
"ctrl-shift-n": "agent::RejectAll",
"shift-tab": "agent::CycleModeSelector"
}
},
{
"context": "ThreadHistory",
"use_key_equivalents": true,
@@ -607,8 +618,6 @@
"shift-alt--": ["workspace::DecreaseActiveDockSize", { "px": 0 }],
"shift-alt-=": ["workspace::IncreaseActiveDockSize", { "px": 0 }],
"shift-alt-0": "workspace::ResetOpenDocksSize",
"ctrl-shift-alt--": ["workspace::DecreaseOpenDocksSize", { "px": 0 }],
"ctrl-shift-alt-=": ["workspace::IncreaseOpenDocksSize", { "px": 0 }],
"ctrl-shift-f": "pane::DeploySearch",
"ctrl-shift-h": ["pane::DeploySearch", { "replace_enabled": true }],
"ctrl-shift-t": "pane::ReopenClosedItem",
@@ -1112,6 +1121,7 @@
"alt-f": ["terminal::SendText", "\u001bf"],
"alt-.": ["terminal::SendText", "\u001b."],
"ctrl-delete": ["terminal::SendText", "\u001bd"],
"ctrl-n": "workspace::NewTerminal",
// Overrides for conflicting keybindings
"ctrl-b": ["terminal::SendKeystroke", "ctrl-b"],
"ctrl-c": ["terminal::SendKeystroke", "ctrl-c"],

View File

@@ -4,6 +4,7 @@
// from the command palette.
[
{
"context": "!GitPanel",
"bindings": {
"ctrl-g": "menu::Cancel"
}

View File

@@ -115,6 +115,7 @@
// Whether to enable vim modes and key bindings.
"vim_mode": false,
// Whether to enable helix mode and key bindings.
// Enabling this mode will automatically enable vim mode.
"helix_mode": false,
// Whether to show the informational hover box when moving the mouse
// over symbols in the editor.
@@ -391,8 +392,6 @@
"use_system_window_tabs": false,
// Titlebar related settings
"title_bar": {
// When to show the title bar: "always" | "never" | "hide_in_full_screen".
"show": "always",
// Whether to show the branch icon beside branch switcher in the titlebar.
"show_branch_icon": false,
// Whether to show the branch name button in the titlebar.
@@ -413,15 +412,33 @@
"experimental.rodio_audio": false,
// Requires 'rodio_audio: true'
//
// Use the new audio systems automatic gain control for your microphone.
// This affects how loud you sound to others.
"experimental.control_input_volume": false,
// Automatically increase or decrease you microphone's volume. This affects how
// loud you sound to others.
//
// Recommended: off (default)
// Microphones are too quite in zed, until everyone is on experimental
// audio and has auto speaker volume on this will make you very loud
// compared to other speakers.
"experimental.auto_microphone_volume": false,
// Requires 'rodio_audio: true'
//
// Use the new audio systems automatic gain control on everyone in the
// call. This makes call members who are too quite louder and those who are
// too loud quieter. This only affects how things sound for you.
"experimental.control_output_volume": false
// Automatically increate or decrease the volume of other call members.
// This only affects how things sound for you.
"experimental.auto_speaker_volume": true,
// Requires 'rodio_audio: true'
//
// Remove background noises. Works great for typing, cars, dogs, AC. Does
// not work well on music.
"experimental.denoise": true,
// Requires 'rodio_audio: true'
//
// Use audio parameters compatible with the previous versions of
// experimental audio and non-experimental audio. When this is false you
// will sound strange to anyone not on the latest experimental audio. In
// the future we will migrate by setting this to false
//
// You need to rejoin a call for this setting to apply
"experimental.legacy_audio_compatible": true
},
// Scrollbar related settings
"scrollbar": {
@@ -1414,7 +1431,7 @@
// "line_height": {
// "custom": 2
// },
"line_height": "comfortable",
"line_height": "standard",
// Activate the python virtual environment, if one is found, in the
// terminal's working directory (as resolved by the working_directory
// setting). Set this to "off" to disable this behavior.
@@ -1434,7 +1451,7 @@
//
// The shell running in the terminal needs to be configured to emit the title.
// Example: `echo -e "\e]2;New Title\007";`
"breadcrumbs": true
"breadcrumbs": false
},
// Scrollbar-related settings
"scrollbar": {

Binary file not shown.

View File

@@ -239,7 +239,7 @@
"hint": {
"color": "#628b80ff",
"font_style": null,
"font_weight": 700
"font_weight": null
},
"keyword": {
"color": "#ff8f3fff",

View File

@@ -248,7 +248,7 @@
"hint": {
"color": "#8c957dff",
"font_style": null,
"font_weight": 700
"font_weight": null
},
"keyword": {
"color": "#fb4833ff",

View File

@@ -244,7 +244,7 @@
"hint": {
"color": "#788ca6ff",
"font_style": null,
"font_weight": 700
"font_weight": null
},
"keyword": {
"color": "#b477cfff",

View File

@@ -5,3 +5,14 @@ ignore-interior-mutability = [
# and Hash impls do not use fields with interior mutability.
"agent::context::AgentContextKey"
]
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" },
]
disallowed-types = [
# { path = "std::collections::HashMap", replacement = "collections::HashMap" },
# { path = "std::collections::HashSet", replacement = "collections::HashSet" },
# { path = "indexmap::IndexSet", replacement = "collections::IndexSet" },
# { path = "indexmap::IndexMap", replacement = "collections::IndexMap" },
]

View File

@@ -573,7 +573,7 @@ impl ToolCallContent {
))),
acp::ToolCallContent::Diff { diff } => Ok(Self::Diff(cx.new(|cx| {
Diff::finalized(
diff.path,
diff.path.to_string_lossy().into_owned(),
diff.old_text,
diff.new_text,
language_registry,
@@ -1780,20 +1780,26 @@ impl AcpThread {
limit: Option<u32>,
reuse_shared_snapshot: bool,
cx: &mut Context<Self>,
) -> Task<Result<String>> {
) -> Task<Result<String, acp::Error>> {
// Args are 1-based, move to 0-based
let line = line.unwrap_or_default().saturating_sub(1);
let limit = limit.unwrap_or(u32::MAX);
let project = self.project.clone();
let action_log = self.action_log.clone();
cx.spawn(async move |this, cx| {
let load = project.update(cx, |project, cx| {
let path = project
.project_path_for_absolute_path(&path, cx)
.context("invalid path")?;
anyhow::Ok(project.open_buffer(path, cx))
});
let buffer = load??.await?;
let load = project
.update(cx, |project, cx| {
let path = project
.project_path_for_absolute_path(&path, cx)
.ok_or_else(|| {
acp::Error::resource_not_found(Some(path.display().to_string()))
})?;
Ok(project.open_buffer(path, cx))
})
.map_err(|e| acp::Error::internal_error().with_data(e.to_string()))
.flatten()?;
let buffer = load.await?;
let snapshot = if reuse_shared_snapshot {
this.read_with(cx, |this, _| {
@@ -1820,15 +1826,17 @@ impl AcpThread {
};
let max_point = snapshot.max_point();
if line >= max_point.row {
anyhow::bail!(
let start_position = Point::new(line, 0);
if start_position > max_point {
return Err(acp::Error::invalid_params().with_data(format!(
"Attempting to read beyond the end of the file, line {}:{}",
max_point.row + 1,
max_point.column
);
)));
}
let start = snapshot.anchor_before(Point::new(line, 0));
let start = snapshot.anchor_before(start_position);
let end = snapshot.anchor_before(Point::new(line.saturating_add(limit), 0));
project.update(cx, |project, cx| {
@@ -1977,7 +1985,7 @@ impl AcpThread {
let terminal_id = terminal_id.clone();
async move |_this, cx| {
let env = env.await;
let (command, args) = ShellBuilder::new(
let (task_command, task_args) = ShellBuilder::new(
project
.update(cx, |project, cx| {
project
@@ -1988,13 +1996,13 @@ impl AcpThread {
&Shell::Program(get_default_system_shell()),
)
.redirect_stdin_to_dev_null()
.build(Some(command), &args);
.build(Some(command.clone()), &args);
let terminal = project
.update(cx, |project, cx| {
project.create_terminal_task(
task::SpawnInTerminal {
command: Some(command.clone()),
args: args.clone(),
command: Some(task_command),
args: task_args,
cwd: cwd.clone(),
env,
..Default::default()
@@ -2449,6 +2457,81 @@ mod tests {
assert_eq!(content, "two\nthree\n");
// Invalid
let err = thread
.update(cx, |thread, cx| {
thread.read_text_file(path!("/tmp/foo").into(), Some(6), Some(2), false, cx)
})
.await
.unwrap_err();
assert_eq!(
err.to_string(),
"Invalid params: \"Attempting to read beyond the end of the file, line 5:0\""
);
}
#[gpui::test]
async fn test_reading_empty_file(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(path!("/tmp"), json!({"foo": ""})).await;
let project = Project::test(fs.clone(), [], cx).await;
project
.update(cx, |project, cx| {
project.find_or_create_worktree(path!("/tmp/foo"), true, cx)
})
.await
.unwrap();
let connection = Rc::new(FakeAgentConnection::new());
let thread = cx
.update(|cx| connection.new_thread(project, Path::new(path!("/tmp")), cx))
.await
.unwrap();
// Whole file
let content = thread
.update(cx, |thread, cx| {
thread.read_text_file(path!("/tmp/foo").into(), None, None, false, cx)
})
.await
.unwrap();
assert_eq!(content, "");
// Only start line
let content = thread
.update(cx, |thread, cx| {
thread.read_text_file(path!("/tmp/foo").into(), Some(1), None, false, cx)
})
.await
.unwrap();
assert_eq!(content, "");
// Only limit
let content = thread
.update(cx, |thread, cx| {
thread.read_text_file(path!("/tmp/foo").into(), None, Some(2), false, cx)
})
.await
.unwrap();
assert_eq!(content, "");
// Range
let content = thread
.update(cx, |thread, cx| {
thread.read_text_file(path!("/tmp/foo").into(), Some(1), Some(1), false, cx)
})
.await
.unwrap();
assert_eq!(content, "");
// Invalid
let err = thread
.update(cx, |thread, cx| {
@@ -2459,9 +2542,40 @@ mod tests {
assert_eq!(
err.to_string(),
"Attempting to read beyond the end of the file, line 5:0"
"Invalid params: \"Attempting to read beyond the end of the file, line 1:0\""
);
}
#[gpui::test]
async fn test_reading_non_existing_file(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(path!("/tmp"), json!({})).await;
let project = Project::test(fs.clone(), [], cx).await;
project
.update(cx, |project, cx| {
project.find_or_create_worktree(path!("/tmp"), true, cx)
})
.await
.unwrap();
let connection = Rc::new(FakeAgentConnection::new());
let thread = cx
.update(|cx| connection.new_thread(project, Path::new(path!("/tmp")), cx))
.await
.unwrap();
// Out of project file
let err = thread
.update(cx, |thread, cx| {
thread.read_text_file(path!("/foo").into(), None, None, false, cx)
})
.await
.unwrap_err();
assert_eq!(err.code, acp::ErrorCode::RESOURCE_NOT_FOUND.code);
}
#[gpui::test]
async fn test_succeeding_canceled_toolcall(cx: &mut TestAppContext) {

View File

@@ -6,12 +6,7 @@ use itertools::Itertools;
use language::{
Anchor, Buffer, Capability, LanguageRegistry, OffsetRangeExt as _, Point, Rope, TextBuffer,
};
use std::{
cmp::Reverse,
ops::Range,
path::{Path, PathBuf},
sync::Arc,
};
use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc};
use util::ResultExt;
pub enum Diff {
@@ -21,7 +16,7 @@ pub enum Diff {
impl Diff {
pub fn finalized(
path: PathBuf,
path: String,
old_text: Option<String>,
new_text: String,
language_registry: Arc<LanguageRegistry>,
@@ -36,7 +31,7 @@ impl Diff {
let buffer = new_buffer.clone();
async move |_, cx| {
let language = language_registry
.language_for_file_path(&path)
.language_for_file_path(Path::new(&path))
.await
.log_err();
@@ -152,12 +147,15 @@ impl Diff {
let path = match self {
Diff::Pending(PendingDiff {
new_buffer: buffer, ..
}) => buffer.read(cx).file().map(|file| file.path().as_ref()),
Diff::Finalized(FinalizedDiff { path, .. }) => Some(path.as_path()),
}) => buffer
.read(cx)
.file()
.map(|file| file.path().display(file.path_style(cx))),
Diff::Finalized(FinalizedDiff { path, .. }) => Some(path.as_str().into()),
};
format!(
"Diff: {}\n```\n{}\n```\n",
path.unwrap_or(Path::new("untitled")).display(),
path.unwrap_or("untitled".into()),
buffer_text
)
}
@@ -244,8 +242,8 @@ impl PendingDiff {
.new_buffer
.read(cx)
.file()
.map(|file| file.path().as_ref())
.unwrap_or(Path::new("untitled"))
.map(|file| file.path().display(file.path_style(cx)))
.unwrap_or("untitled".into())
.into();
// Replace the buffer in the multibuffer with the snapshot
@@ -348,7 +346,7 @@ impl PendingDiff {
}
pub struct FinalizedDiff {
path: PathBuf,
path: String,
base_text: Arc<String>,
new_buffer: Entity<Buffer>,
multibuffer: Entity<MultiBuffer>,

View File

@@ -126,6 +126,39 @@ impl MentionUri {
abs_path: None,
line_range,
})
} else if let Some(name) = path.strip_prefix("/agent/symbol/") {
let fragment = url
.fragment()
.context("Missing fragment for untitled buffer selection")?;
let line_range = parse_line_range(fragment)?;
let path =
single_query_param(&url, "path")?.context("Missing path for symbol")?;
Ok(Self::Symbol {
name: name.to_string(),
abs_path: path.into(),
line_range,
})
} else if path.starts_with("/agent/file") {
let path =
single_query_param(&url, "path")?.context("Missing path for file")?;
Ok(Self::File {
abs_path: path.into(),
})
} else if path.starts_with("/agent/directory") {
let path =
single_query_param(&url, "path")?.context("Missing path for directory")?;
Ok(Self::Directory {
abs_path: path.into(),
})
} else if path.starts_with("/agent/selection") {
let fragment = url.fragment().context("Missing fragment for selection")?;
let line_range = parse_line_range(fragment)?;
let path =
single_query_param(&url, "path")?.context("Missing path for selection")?;
Ok(Self::Selection {
abs_path: Some(path.into()),
line_range,
})
} else {
bail!("invalid zed url: {:?}", input);
}
@@ -180,20 +213,29 @@ impl MentionUri {
pub fn to_uri(&self) -> Url {
match self {
MentionUri::File { abs_path } => {
Url::from_file_path(abs_path).expect("mention path should be absolute")
let mut url = Url::parse("zed:///").unwrap();
url.set_path("/agent/file");
url.query_pairs_mut()
.append_pair("path", &abs_path.to_string_lossy());
url
}
MentionUri::PastedImage => Url::parse("zed:///agent/pasted-image").unwrap(),
MentionUri::Directory { abs_path } => {
Url::from_directory_path(abs_path).expect("mention path should be absolute")
let mut url = Url::parse("zed:///").unwrap();
url.set_path("/agent/directory");
url.query_pairs_mut()
.append_pair("path", &abs_path.to_string_lossy());
url
}
MentionUri::Symbol {
abs_path,
name,
line_range,
} => {
let mut url =
Url::from_file_path(abs_path).expect("mention path should be absolute");
url.query_pairs_mut().append_pair("symbol", name);
let mut url = Url::parse("zed:///").unwrap();
url.set_path(&format!("/agent/symbol/{name}"));
url.query_pairs_mut()
.append_pair("path", &abs_path.to_string_lossy());
url.set_fragment(Some(&format!(
"L{}:{}",
line_range.start() + 1,
@@ -202,15 +244,16 @@ impl MentionUri {
url
}
MentionUri::Selection {
abs_path: path,
abs_path,
line_range,
} => {
let mut url = if let Some(path) = path {
Url::from_file_path(path).expect("mention path should be absolute")
let mut url = Url::parse("zed:///").unwrap();
if let Some(abs_path) = abs_path {
url.set_path("/agent/selection");
url.query_pairs_mut()
.append_pair("path", &abs_path.to_string_lossy());
} else {
let mut url = Url::parse("zed:///").unwrap();
url.set_path("/agent/untitled-buffer");
url
};
url.set_fragment(Some(&format!(
"L{}:{}",
@@ -295,37 +338,32 @@ mod tests {
#[test]
fn test_parse_file_uri() {
let file_uri = uri!("file:///path/to/file.rs");
let parsed = MentionUri::parse(file_uri).unwrap();
let old_uri = uri!("file:///path/to/file.rs");
let parsed = MentionUri::parse(old_uri).unwrap();
match &parsed {
MentionUri::File { abs_path } => {
assert_eq!(abs_path.to_str().unwrap(), path!("/path/to/file.rs"));
}
_ => panic!("Expected File variant"),
}
assert_eq!(parsed.to_uri().to_string(), file_uri);
let new_uri = parsed.to_uri().to_string();
assert!(new_uri.starts_with("zed:///agent/file"));
assert_eq!(MentionUri::parse(&new_uri).unwrap(), parsed);
}
#[test]
fn test_parse_directory_uri() {
let file_uri = uri!("file:///path/to/dir/");
let parsed = MentionUri::parse(file_uri).unwrap();
let old_uri = uri!("file:///path/to/dir/");
let parsed = MentionUri::parse(old_uri).unwrap();
match &parsed {
MentionUri::Directory { abs_path } => {
assert_eq!(abs_path.to_str().unwrap(), path!("/path/to/dir/"));
}
_ => panic!("Expected Directory variant"),
}
assert_eq!(parsed.to_uri().to_string(), file_uri);
}
#[test]
fn test_to_directory_uri_with_slash() {
let uri = MentionUri::Directory {
abs_path: PathBuf::from(path!("/path/to/dir/")),
};
let expected = uri!("file:///path/to/dir/");
assert_eq!(uri.to_uri().to_string(), expected);
let new_uri = parsed.to_uri().to_string();
assert!(new_uri.starts_with("zed:///agent/directory"));
assert_eq!(MentionUri::parse(&new_uri).unwrap(), parsed);
}
#[test]
@@ -333,14 +371,15 @@ mod tests {
let uri = MentionUri::Directory {
abs_path: PathBuf::from(path!("/path/to/dir")),
};
let expected = uri!("file:///path/to/dir/");
assert_eq!(uri.to_uri().to_string(), expected);
let uri_string = uri.to_uri().to_string();
assert!(uri_string.starts_with("zed:///agent/directory"));
assert_eq!(MentionUri::parse(&uri_string).unwrap(), uri);
}
#[test]
fn test_parse_symbol_uri() {
let symbol_uri = uri!("file:///path/to/file.rs?symbol=MySymbol#L10:20");
let parsed = MentionUri::parse(symbol_uri).unwrap();
let old_uri = uri!("file:///path/to/file.rs?symbol=MySymbol#L10:20");
let parsed = MentionUri::parse(old_uri).unwrap();
match &parsed {
MentionUri::Symbol {
abs_path: path,
@@ -354,13 +393,15 @@ mod tests {
}
_ => panic!("Expected Symbol variant"),
}
assert_eq!(parsed.to_uri().to_string(), symbol_uri);
let new_uri = parsed.to_uri().to_string();
assert!(new_uri.starts_with("zed:///agent/symbol/MySymbol"));
assert_eq!(MentionUri::parse(&new_uri).unwrap(), parsed);
}
#[test]
fn test_parse_selection_uri() {
let selection_uri = uri!("file:///path/to/file.rs#L5:15");
let parsed = MentionUri::parse(selection_uri).unwrap();
let old_uri = uri!("file:///path/to/file.rs#L5:15");
let parsed = MentionUri::parse(old_uri).unwrap();
match &parsed {
MentionUri::Selection {
abs_path: path,
@@ -375,7 +416,9 @@ mod tests {
}
_ => panic!("Expected Selection variant"),
}
assert_eq!(parsed.to_uri().to_string(), selection_uri);
let new_uri = parsed.to_uri().to_string();
assert!(new_uri.starts_with("zed:///agent/selection"));
assert_eq!(MentionUri::parse(&new_uri).unwrap(), parsed);
}
#[test]

View File

@@ -8,10 +8,7 @@ use language::{Anchor, Buffer, BufferEvent, DiskState, Point, ToPoint};
use project::{Project, ProjectItem, lsp_store::OpenLspBufferHandle};
use std::{cmp, ops::Range, sync::Arc};
use text::{Edit, Patch, Rope};
use util::{
RangeExt, ResultExt as _,
paths::{PathStyle, RemotePathBuf},
};
use util::{RangeExt, ResultExt as _};
/// Tracks actions performed by tools in a thread
pub struct ActionLog {
@@ -62,7 +59,13 @@ impl ActionLog {
let file_path = buffer
.read(cx)
.file()
.map(|file| RemotePathBuf::new(file.full_path(cx), PathStyle::Posix).to_proto())
.map(|file| {
let mut path = file.full_path(cx).to_string_lossy().into_owned();
if file.path_style(cx).is_windows() {
path = path.replace('\\', "/");
}
path
})
.unwrap_or_else(|| format!("buffer_{}", buffer.entity_id()));
let mut result = String::new();
@@ -2301,7 +2304,7 @@ mod tests {
.await;
fs.set_head_for_repo(
path!("/project/.git").as_ref(),
&[("file.txt".into(), "a\nb\nc\nd\ne\nf\ng\nh\ni\nj".into())],
&[("file.txt", "a\nb\nc\nd\ne\nf\ng\nh\ni\nj".into())],
"0000000",
);
cx.run_until_parked();
@@ -2384,7 +2387,7 @@ mod tests {
// - Ignores the last line edit (j stays as j)
fs.set_head_for_repo(
path!("/project/.git").as_ref(),
&[("file.txt".into(), "A\nb\nc\nf\nG\nh\ni\nj".into())],
&[("file.txt", "A\nb\nc\nf\nG\nh\ni\nj".into())],
"0000001",
);
cx.run_until_parked();
@@ -2415,10 +2418,7 @@ mod tests {
// Make another commit that accepts the NEW line but with different content
fs.set_head_for_repo(
path!("/project/.git").as_ref(),
&[(
"file.txt".into(),
"A\nb\nc\nf\nGGG\nh\nDIFFERENT\ni\nj".into(),
)],
&[("file.txt", "A\nb\nc\nf\nGGG\nh\nDIFFERENT\ni\nj".into())],
"0000002",
);
cx.run_until_parked();
@@ -2444,7 +2444,7 @@ mod tests {
// Final commit that accepts all remaining edits
fs.set_head_for_repo(
path!("/project/.git").as_ref(),
&[("file.txt".into(), "A\nb\nc\nf\nGGG\nh\nNEW\ni\nJ".into())],
&[("file.txt", "A\nb\nc\nf\nGGG\nh\nNEW\ni\nJ".into())],
"0000003",
);
cx.run_until_parked();

View File

@@ -9,12 +9,14 @@ pub mod tool_use;
pub use context::{AgentContext, ContextId, ContextLoadResult};
pub use context_store::ContextStore;
use fs::Fs;
use std::sync::Arc;
pub use thread::{
LastRestoreCheckpoint, Message, MessageCrease, MessageId, MessageSegment, Thread, ThreadError,
ThreadEvent, ThreadFeedback, ThreadId, ThreadSummary, TokenUsageRatio,
};
pub use thread_store::{SerializedThread, TextThreadStore, ThreadStore};
pub fn init(cx: &mut gpui::App) {
thread_store::init(cx);
pub fn init(fs: Arc<dyn Fs>, cx: &mut gpui::App) {
thread_store::init(fs, cx);
}

View File

@@ -18,6 +18,7 @@ use std::path::PathBuf;
use std::{ops::Range, path::Path, sync::Arc};
use text::{Anchor, OffsetRangeExt as _};
use util::markdown::MarkdownCodeBlock;
use util::rel_path::RelPath;
use util::{ResultExt as _, post_inc};
pub const RULES_ICON: IconName = IconName::Reader;
@@ -158,7 +159,7 @@ pub struct FileContextHandle {
#[derive(Debug, Clone)]
pub struct FileContext {
pub handle: FileContextHandle,
pub full_path: Arc<Path>,
pub full_path: String,
pub text: SharedString,
pub is_outline: bool,
}
@@ -186,7 +187,7 @@ impl FileContextHandle {
log::error!("file context missing path");
return Task::ready(None);
};
let full_path: Arc<Path> = file.full_path(cx).into();
let full_path = file.full_path(cx).to_string_lossy().into_owned();
let rope = buffer_ref.as_rope().clone();
let buffer = self.buffer.clone();
@@ -235,14 +236,14 @@ pub struct DirectoryContextHandle {
#[derive(Debug, Clone)]
pub struct DirectoryContext {
pub handle: DirectoryContextHandle,
pub full_path: Arc<Path>,
pub full_path: String,
pub descendants: Vec<DirectoryContextDescendant>,
}
#[derive(Debug, Clone)]
pub struct DirectoryContextDescendant {
/// Path within the directory.
pub rel_path: Arc<Path>,
pub rel_path: Arc<RelPath>,
pub fenced_codeblock: SharedString,
}
@@ -273,13 +274,16 @@ impl DirectoryContextHandle {
}
let directory_path = entry.path.clone();
let directory_full_path = worktree_ref.full_path(&directory_path).into();
let directory_full_path = worktree_ref
.full_path(&directory_path)
.to_string_lossy()
.to_string();
let file_paths = collect_files_in_path(worktree_ref, &directory_path);
let descendants_future = future::join_all(file_paths.into_iter().map(|path| {
let worktree_ref = worktree.read(cx);
let worktree_id = worktree_ref.id();
let full_path = worktree_ref.full_path(&path);
let full_path = worktree_ref.full_path(&path).to_string_lossy().into_owned();
let rel_path = path
.strip_prefix(&directory_path)
@@ -360,7 +364,7 @@ pub struct SymbolContextHandle {
#[derive(Debug, Clone)]
pub struct SymbolContext {
pub handle: SymbolContextHandle,
pub full_path: Arc<Path>,
pub full_path: String,
pub line_range: Range<Point>,
pub text: SharedString,
}
@@ -399,7 +403,7 @@ impl SymbolContextHandle {
log::error!("symbol context's file has no path");
return Task::ready(None);
};
let full_path = file.full_path(cx).into();
let full_path = file.full_path(cx).to_string_lossy().into_owned();
let line_range = self.enclosing_range.to_point(&buffer_ref.snapshot());
let text = self.text(cx);
let buffer = self.buffer.clone();
@@ -433,7 +437,7 @@ pub struct SelectionContextHandle {
#[derive(Debug, Clone)]
pub struct SelectionContext {
pub handle: SelectionContextHandle,
pub full_path: Arc<Path>,
pub full_path: String,
pub line_range: Range<Point>,
pub text: SharedString,
}
@@ -472,7 +476,7 @@ impl SelectionContextHandle {
let text = self.text(cx);
let buffer = self.buffer.clone();
let context = AgentContext::Selection(SelectionContext {
full_path: full_path.into(),
full_path: full_path.to_string_lossy().into_owned(),
line_range: self.line_range(cx),
text,
handle: self,
@@ -702,7 +706,7 @@ impl Display for RulesContext {
#[derive(Debug, Clone)]
pub struct ImageContext {
pub project_path: Option<ProjectPath>,
pub full_path: Option<Arc<Path>>,
pub full_path: Option<String>,
pub original_image: Arc<gpui::Image>,
// TODO: handle this elsewhere and remove `ignore-interior-mutability` opt-out in clippy.toml
// needed due to a false positive of `clippy::mutable_key_type`.
@@ -968,7 +972,7 @@ pub fn load_context(
})
}
fn collect_files_in_path(worktree: &Worktree, path: &Path) -> Vec<Arc<Path>> {
fn collect_files_in_path(worktree: &Worktree, path: &RelPath) -> Vec<Arc<RelPath>> {
let mut files = Vec::new();
for entry in worktree.child_entries(path) {
@@ -982,14 +986,17 @@ fn collect_files_in_path(worktree: &Worktree, path: &Path) -> Vec<Arc<Path>> {
files
}
fn codeblock_tag(full_path: &Path, line_range: Option<Range<Point>>) -> String {
fn codeblock_tag(full_path: &str, line_range: Option<Range<Point>>) -> String {
let mut result = String::new();
if let Some(extension) = full_path.extension().and_then(|ext| ext.to_str()) {
if let Some(extension) = Path::new(full_path)
.extension()
.and_then(|ext| ext.to_str())
{
let _ = write!(result, "{} ", extension);
}
let _ = write!(result, "{}", full_path.display());
let _ = write!(result, "{}", full_path);
if let Some(range) = line_range {
if range.start.row == range.end.row {

View File

@@ -14,7 +14,10 @@ use futures::{self, FutureExt};
use gpui::{App, Context, Entity, EventEmitter, Image, SharedString, Task, WeakEntity};
use language::{Buffer, File as _};
use language_model::LanguageModelImage;
use project::{Project, ProjectItem, ProjectPath, Symbol, image_store::is_image_file};
use project::{
Project, ProjectItem, ProjectPath, Symbol, image_store::is_image_file,
lsp_store::SymbolLocation,
};
use prompt_store::UserPromptId;
use ref_cast::RefCast as _;
use std::{
@@ -309,7 +312,7 @@ impl ContextStore {
let item = image_item.read(cx);
this.insert_image(
Some(item.project_path(cx)),
Some(item.file.full_path(cx).into()),
Some(item.file.full_path(cx).to_string_lossy().into_owned()),
item.image.clone(),
remove_if_exists,
cx,
@@ -325,7 +328,7 @@ impl ContextStore {
fn insert_image(
&mut self,
project_path: Option<ProjectPath>,
full_path: Option<Arc<Path>>,
full_path: Option<String>,
image: Arc<Image>,
remove_if_exists: bool,
cx: &mut Context<ContextStore>,
@@ -500,7 +503,7 @@ impl ContextStore {
let Some(context_path) = buffer.project_path(cx) else {
return false;
};
if context_path != symbol.path {
if symbol.path != SymbolLocation::InProject(context_path) {
return false;
}
let context_range = context.range.to_point_utf16(&buffer.snapshot());

View File

@@ -155,7 +155,7 @@ impl HistoryStore {
.iter()
.filter_map(|entry| match entry {
HistoryEntryId::Context(path) => path.file_name().map(|file| {
SerializedRecentOpen::ContextName(file.to_string_lossy().to_string())
SerializedRecentOpen::ContextName(file.to_string_lossy().into_owned())
}),
HistoryEntryId::Thread(id) => Some(SerializedRecentOpen::Thread(id.to_string())),
})

View File

@@ -234,7 +234,6 @@ impl MessageSegment {
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ProjectSnapshot {
pub worktree_snapshots: Vec<WorktreeSnapshot>,
pub unsaved_buffer_paths: Vec<String>,
pub timestamp: DateTime<Utc>,
}
@@ -2857,27 +2856,11 @@ impl Thread {
.map(|worktree| Self::worktree_snapshot(worktree, git_store.clone(), cx))
.collect();
cx.spawn(async move |_, cx| {
cx.spawn(async move |_, _| {
let worktree_snapshots = futures::future::join_all(worktree_snapshots).await;
let mut unsaved_buffers = Vec::new();
cx.update(|app_cx| {
let buffer_store = project.read(app_cx).buffer_store();
for buffer_handle in buffer_store.read(app_cx).buffers() {
let buffer = buffer_handle.read(app_cx);
if buffer.is_dirty()
&& let Some(file) = buffer.file()
{
let path = file.path().to_string_lossy().to_string();
unsaved_buffers.push(path);
}
}
})
.ok();
Arc::new(ProjectSnapshot {
worktree_snapshots,
unsaved_buffer_paths: unsaved_buffers,
timestamp: Utc::now(),
})
})
@@ -2892,7 +2875,7 @@ impl Thread {
// Get worktree path and snapshot
let worktree_info = cx.update(|app_cx| {
let worktree = worktree.read(app_cx);
let path = worktree.abs_path().to_string_lossy().to_string();
let path = worktree.abs_path().to_string_lossy().into_owned();
let snapshot = worktree.snapshot();
(path, snapshot)
});
@@ -3275,6 +3258,7 @@ mod tests {
use agent_settings::{AgentProfileId, AgentSettings};
use assistant_tool::ToolRegistry;
use assistant_tools;
use fs::Fs;
use futures::StreamExt;
use futures::future::BoxFuture;
use futures::stream::BoxStream;
@@ -3298,9 +3282,10 @@ mod tests {
#[gpui::test]
async fn test_message_with_context(cx: &mut TestAppContext) {
init_test_settings(cx);
let fs = init_test_settings(cx);
let project = create_test_project(
&fs,
cx,
json!({"code.rs": "fn main() {\n println!(\"Hello, world!\");\n}"}),
)
@@ -3375,9 +3360,10 @@ fn main() {{
#[gpui::test]
async fn test_only_include_new_contexts(cx: &mut TestAppContext) {
init_test_settings(cx);
let fs = init_test_settings(cx);
let project = create_test_project(
&fs,
cx,
json!({
"file1.rs": "fn function1() {}\n",
@@ -3531,9 +3517,10 @@ fn main() {{
#[gpui::test]
async fn test_message_without_files(cx: &mut TestAppContext) {
init_test_settings(cx);
let fs = init_test_settings(cx);
let project = create_test_project(
&fs,
cx,
json!({"code.rs": "fn main() {\n println!(\"Hello, world!\");\n}"}),
)
@@ -3610,9 +3597,10 @@ fn main() {{
#[gpui::test]
#[ignore] // turn this test on when project_notifications tool is re-enabled
async fn test_stale_buffer_notification(cx: &mut TestAppContext) {
init_test_settings(cx);
let fs = init_test_settings(cx);
let project = create_test_project(
&fs,
cx,
json!({"code.rs": "fn main() {\n println!(\"Hello, world!\");\n}"}),
)
@@ -3738,9 +3726,10 @@ fn main() {{
#[gpui::test]
async fn test_storing_profile_setting_per_thread(cx: &mut TestAppContext) {
init_test_settings(cx);
let fs = init_test_settings(cx);
let project = create_test_project(
&fs,
cx,
json!({"code.rs": "fn main() {\n println!(\"Hello, world!\");\n}"}),
)
@@ -3760,9 +3749,10 @@ fn main() {{
#[gpui::test]
async fn test_serializing_thread_profile(cx: &mut TestAppContext) {
init_test_settings(cx);
let fs = init_test_settings(cx);
let project = create_test_project(
&fs,
cx,
json!({"code.rs": "fn main() {\n println!(\"Hello, world!\");\n}"}),
)
@@ -3803,9 +3793,10 @@ fn main() {{
#[gpui::test]
async fn test_temperature_setting(cx: &mut TestAppContext) {
init_test_settings(cx);
let fs = init_test_settings(cx);
let project = create_test_project(
&fs,
cx,
json!({"code.rs": "fn main() {\n println!(\"Hello, world!\");\n}"}),
)
@@ -3897,9 +3888,9 @@ fn main() {{
#[gpui::test]
async fn test_thread_summary(cx: &mut TestAppContext) {
init_test_settings(cx);
let fs = init_test_settings(cx);
let project = create_test_project(cx, json!({})).await;
let project = create_test_project(&fs, cx, json!({})).await;
let (_, _thread_store, thread, _context_store, model) =
setup_test_environment(cx, project.clone()).await;
@@ -3982,9 +3973,9 @@ fn main() {{
#[gpui::test]
async fn test_thread_summary_error_set_manually(cx: &mut TestAppContext) {
init_test_settings(cx);
let fs = init_test_settings(cx);
let project = create_test_project(cx, json!({})).await;
let project = create_test_project(&fs, cx, json!({})).await;
let (_, _thread_store, thread, _context_store, model) =
setup_test_environment(cx, project.clone()).await;
@@ -4004,9 +3995,9 @@ fn main() {{
#[gpui::test]
async fn test_thread_summary_error_retry(cx: &mut TestAppContext) {
init_test_settings(cx);
let fs = init_test_settings(cx);
let project = create_test_project(cx, json!({})).await;
let project = create_test_project(&fs, cx, json!({})).await;
let (_, _thread_store, thread, _context_store, model) =
setup_test_environment(cx, project.clone()).await;
@@ -4158,9 +4149,9 @@ fn main() {{
#[gpui::test]
async fn test_retry_on_overloaded_error(cx: &mut TestAppContext) {
init_test_settings(cx);
let fs = init_test_settings(cx);
let project = create_test_project(cx, json!({})).await;
let project = create_test_project(&fs, cx, json!({})).await;
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
// Enable Burn Mode to allow retries
@@ -4236,9 +4227,9 @@ fn main() {{
#[gpui::test]
async fn test_retry_on_internal_server_error(cx: &mut TestAppContext) {
init_test_settings(cx);
let fs = init_test_settings(cx);
let project = create_test_project(cx, json!({})).await;
let project = create_test_project(&fs, cx, json!({})).await;
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
// Enable Burn Mode to allow retries
@@ -4318,9 +4309,9 @@ fn main() {{
#[gpui::test]
async fn test_exponential_backoff_on_retries(cx: &mut TestAppContext) {
init_test_settings(cx);
let fs = init_test_settings(cx);
let project = create_test_project(cx, json!({})).await;
let project = create_test_project(&fs, cx, json!({})).await;
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
// Enable Burn Mode to allow retries
@@ -4438,9 +4429,9 @@ fn main() {{
#[gpui::test]
async fn test_max_retries_exceeded(cx: &mut TestAppContext) {
init_test_settings(cx);
let fs = init_test_settings(cx);
let project = create_test_project(cx, json!({})).await;
let project = create_test_project(&fs, cx, json!({})).await;
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
// Enable Burn Mode to allow retries
@@ -4529,9 +4520,9 @@ fn main() {{
#[gpui::test]
async fn test_retry_message_removed_on_retry(cx: &mut TestAppContext) {
init_test_settings(cx);
let fs = init_test_settings(cx);
let project = create_test_project(cx, json!({})).await;
let project = create_test_project(&fs, cx, json!({})).await;
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
// Enable Burn Mode to allow retries
@@ -4702,9 +4693,9 @@ fn main() {{
#[gpui::test]
async fn test_successful_completion_clears_retry_state(cx: &mut TestAppContext) {
init_test_settings(cx);
let fs = init_test_settings(cx);
let project = create_test_project(cx, json!({})).await;
let project = create_test_project(&fs, cx, json!({})).await;
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
// Enable Burn Mode to allow retries
@@ -4868,9 +4859,9 @@ fn main() {{
#[gpui::test]
async fn test_rate_limit_retry_single_attempt(cx: &mut TestAppContext) {
init_test_settings(cx);
let fs = init_test_settings(cx);
let project = create_test_project(cx, json!({})).await;
let project = create_test_project(&fs, cx, json!({})).await;
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
// Enable Burn Mode to allow retries
@@ -5053,9 +5044,9 @@ fn main() {{
#[gpui::test]
async fn test_ui_only_messages_not_sent_to_model(cx: &mut TestAppContext) {
init_test_settings(cx);
let fs = init_test_settings(cx);
let project = create_test_project(cx, json!({})).await;
let project = create_test_project(&fs, cx, json!({})).await;
let (_, _, thread, _, model) = setup_test_environment(cx, project.clone()).await;
// Insert a regular user message
@@ -5153,9 +5144,9 @@ fn main() {{
#[gpui::test]
async fn test_no_retry_without_burn_mode(cx: &mut TestAppContext) {
init_test_settings(cx);
let fs = init_test_settings(cx);
let project = create_test_project(cx, json!({})).await;
let project = create_test_project(&fs, cx, json!({})).await;
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
// Ensure we're in Normal mode (not Burn mode)
@@ -5226,9 +5217,9 @@ fn main() {{
#[gpui::test]
async fn test_retry_canceled_on_stop(cx: &mut TestAppContext) {
init_test_settings(cx);
let fs = init_test_settings(cx);
let project = create_test_project(cx, json!({})).await;
let project = create_test_project(&fs, cx, json!({})).await;
let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await;
// Enable Burn Mode to allow retries
@@ -5334,7 +5325,8 @@ fn main() {{
cx.run_until_parked();
}
fn init_test_settings(cx: &mut TestAppContext) {
fn init_test_settings(cx: &mut TestAppContext) -> Arc<dyn Fs> {
let fs = FakeFs::new(cx.executor());
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
@@ -5342,7 +5334,7 @@ fn main() {{
Project::init_settings(cx);
AgentSettings::register(cx);
prompt_store::init(cx);
thread_store::init(cx);
thread_store::init(fs.clone(), cx);
workspace::init_settings(cx);
language_model::init_settings(cx);
ThemeSettings::register(cx);
@@ -5356,16 +5348,17 @@ fn main() {{
));
assistant_tools::init(http_client, cx);
});
fs
}
// Helper to create a test project with test files
async fn create_test_project(
fs: &Arc<dyn Fs>,
cx: &mut TestAppContext,
files: serde_json::Value,
) -> Entity<Project> {
let fs = FakeFs::new(cx.executor());
fs.insert_tree(path!("/test"), files).await;
Project::test(fs, [path!("/test").as_ref()], cx).await
fs.as_fake().insert_tree(path!("/test"), files).await;
Project::test(fs.clone(), [path!("/test").as_ref()], cx).await
}
async fn setup_test_environment(

View File

@@ -10,6 +10,7 @@ use assistant_tool::{Tool, ToolId, ToolWorkingSet};
use chrono::{DateTime, Utc};
use collections::HashMap;
use context_server::ContextServerId;
use fs::{Fs, RemoveOptions};
use futures::{
FutureExt as _, StreamExt as _,
channel::{mpsc, oneshot},
@@ -39,7 +40,7 @@ use std::{
rc::Rc,
sync::{Arc, Mutex},
};
use util::ResultExt as _;
use util::{ResultExt as _, rel_path::RelPath};
use zed_env_vars::ZED_STATELESS;
@@ -85,8 +86,8 @@ const RULES_FILE_NAMES: [&str; 9] = [
"GEMINI.md",
];
pub fn init(cx: &mut App) {
ThreadsDatabase::init(cx);
pub fn init(fs: Arc<dyn Fs>, cx: &mut App) {
ThreadsDatabase::init(fs, cx);
}
/// A system prompt shared by all threads created by this ThreadStore
@@ -234,7 +235,7 @@ impl ThreadStore {
if items.iter().any(|(path, _, _)| {
RULES_FILE_NAMES
.iter()
.any(|name| path.as_ref() == Path::new(name))
.any(|name| path.as_ref() == RelPath::unix(name).unwrap())
}) {
self.enqueue_system_prompt_reload();
}
@@ -327,7 +328,7 @@ impl ThreadStore {
cx: &mut App,
) -> Task<(WorktreeContext, Option<RulesLoadingError>)> {
let tree = worktree.read(cx);
let root_name = tree.root_name().into();
let root_name = tree.root_name_str().into();
let abs_path = tree.abs_path();
let mut context = WorktreeContext {
@@ -367,7 +368,7 @@ impl ThreadStore {
.into_iter()
.filter_map(|name| {
worktree
.entry_for_path(name)
.entry_for_path(RelPath::unix(name).unwrap())
.filter(|entry| entry.is_file())
.map(|entry| entry.path.clone())
})
@@ -869,13 +870,13 @@ impl ThreadsDatabase {
GlobalThreadsDatabase::global(cx).0.clone()
}
fn init(cx: &mut App) {
fn init(fs: Arc<dyn Fs>, cx: &mut App) {
let executor = cx.background_executor().clone();
let database_future = executor
.spawn({
let executor = executor.clone();
let threads_dir = paths::data_dir().join("threads");
async move { ThreadsDatabase::new(threads_dir, executor) }
async move { ThreadsDatabase::new(fs, threads_dir, executor).await }
})
.then(|result| future::ready(result.map(Arc::new).map_err(Arc::new)))
.boxed()
@@ -884,13 +885,17 @@ impl ThreadsDatabase {
cx.set_global(GlobalThreadsDatabase(database_future));
}
pub fn new(threads_dir: PathBuf, executor: BackgroundExecutor) -> Result<Self> {
std::fs::create_dir_all(&threads_dir)?;
pub async fn new(
fs: Arc<dyn Fs>,
threads_dir: PathBuf,
executor: BackgroundExecutor,
) -> Result<Self> {
fs.create_dir(&threads_dir).await?;
let sqlite_path = threads_dir.join("threads.db");
let mdb_path = threads_dir.join("threads-db.1.mdb");
let needs_migration_from_heed = mdb_path.exists();
let needs_migration_from_heed = fs.is_file(&mdb_path).await;
let connection = if *ZED_STATELESS {
Connection::open_memory(Some("THREAD_FALLBACK_DB"))
@@ -932,7 +937,14 @@ impl ThreadsDatabase {
.spawn(async move {
log::info!("Starting threads.db migration");
Self::migrate_from_heed(&mdb_path, db_connection, executor_clone)?;
std::fs::remove_dir_all(mdb_path)?;
fs.remove_dir(
&mdb_path,
RemoveOptions {
recursive: true,
ignore_if_not_exists: true,
},
)
.await?;
log::info!("threads.db migrated to sqlite");
Ok::<(), anyhow::Error>(())
})

View File

@@ -27,6 +27,7 @@ use std::path::{Path, PathBuf};
use std::rc::Rc;
use std::sync::Arc;
use util::ResultExt;
use util::rel_path::RelPath;
const RULES_FILE_NAMES: [&str; 9] = [
".rules",
@@ -434,7 +435,7 @@ impl NativeAgent {
cx: &mut App,
) -> Task<(WorktreeContext, Option<RulesLoadingError>)> {
let tree = worktree.read(cx);
let root_name = tree.root_name().into();
let root_name = tree.root_name_str().into();
let abs_path = tree.abs_path();
let mut context = WorktreeContext {
@@ -474,7 +475,7 @@ impl NativeAgent {
.into_iter()
.filter_map(|name| {
worktree
.entry_for_path(name)
.entry_for_path(RelPath::unix(name).unwrap())
.filter(|entry| entry.is_file())
.map(|entry| entry.path.clone())
})
@@ -558,7 +559,7 @@ impl NativeAgent {
if items.iter().any(|(path, _, _)| {
RULES_FILE_NAMES
.iter()
.any(|name| path.as_ref() == Path::new(name))
.any(|name| path.as_ref() == RelPath::unix(name).unwrap())
}) {
self.project_context_needs_refresh.send(()).ok();
}
@@ -1204,11 +1205,11 @@ mod tests {
use acp_thread::{AgentConnection, AgentModelGroupName, AgentModelInfo, MentionUri};
use fs::FakeFs;
use gpui::TestAppContext;
use indoc::indoc;
use indoc::formatdoc;
use language_model::fake_provider::FakeLanguageModel;
use serde_json::json;
use settings::SettingsStore;
use util::path;
use util::{path, rel_path::rel_path};
#[gpui::test]
async fn test_maintaining_project_context(cx: &mut TestAppContext) {
@@ -1258,14 +1259,17 @@ mod tests {
fs.insert_file("/a/.rules", Vec::new()).await;
cx.run_until_parked();
agent.read_with(cx, |agent, cx| {
let rules_entry = worktree.read(cx).entry_for_path(".rules").unwrap();
let rules_entry = worktree
.read(cx)
.entry_for_path(rel_path(".rules"))
.unwrap();
assert_eq!(
agent.project_context.read(cx).worktrees,
vec![WorktreeContext {
root_name: "a".into(),
abs_path: Path::new("/a").into(),
rules_file: Some(RulesFileContext {
path_in_worktree: Path::new(".rules").into(),
path_in_worktree: rel_path(".rules").into(),
text: "".into(),
project_entry_id: rules_entry.id.to_usize()
})
@@ -1498,13 +1502,17 @@ mod tests {
summary_model.end_last_completion_stream();
send.await.unwrap();
let uri = MentionUri::File {
abs_path: path!("/a/b.md").into(),
}
.to_uri();
acp_thread.read_with(cx, |thread, cx| {
assert_eq!(
thread.to_markdown(cx),
indoc! {"
formatdoc! {"
## User
What does [@b.md](file:///a/b.md) mean?
What does [@b.md]({uri}) mean?
## Assistant
@@ -1540,10 +1548,10 @@ mod tests {
acp_thread.read_with(cx, |thread, cx| {
assert_eq!(
thread.to_markdown(cx),
indoc! {"
formatdoc! {"
## User
What does [@b.md](file:///a/b.md) mean?
What does [@b.md]({uri}) mean?
## Assistant

View File

@@ -422,17 +422,15 @@ mod tests {
use agent::MessageSegment;
use agent::context::LoadedContext;
use client::Client;
use fs::FakeFs;
use fs::{FakeFs, Fs};
use gpui::AppContext;
use gpui::TestAppContext;
use http_client::FakeHttpClient;
use language_model::Role;
use project::Project;
use serde_json::json;
use settings::SettingsStore;
use util::test::TempTree;
fn init_test(cx: &mut TestAppContext) {
fn init_test(fs: Arc<dyn Fs>, cx: &mut TestAppContext) {
env_logger::try_init().ok();
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
@@ -443,7 +441,7 @@ mod tests {
let http_client = FakeHttpClient::with_404_response();
let clock = Arc::new(clock::FakeSystemClock::new());
let client = Client::new(clock, http_client, cx);
agent::init(cx);
agent::init(fs, cx);
agent_settings::init(cx);
language_model::init(client, cx);
});
@@ -451,10 +449,8 @@ mod tests {
#[gpui::test]
async fn test_retrieving_old_thread(cx: &mut TestAppContext) {
let tree = TempTree::new(json!({}));
util::paths::set_home_dir(tree.path().into());
init_test(cx);
let fs = FakeFs::new(cx.executor());
init_test(fs.clone(), cx);
let project = Project::test(fs, [], cx).await;
// Save a thread using the old agent.

View File

@@ -262,7 +262,7 @@ impl HistoryStore {
.iter()
.filter_map(|entry| match entry {
HistoryEntryId::TextThread(path) => path.file_name().map(|file| {
SerializedRecentOpen::TextThread(file.to_string_lossy().to_string())
SerializedRecentOpen::TextThread(file.to_string_lossy().into_owned())
}),
HistoryEntryId::AcpThread(id) => {
Some(SerializedRecentOpen::AcpThread(id.to_string()))

View File

@@ -879,27 +879,11 @@ impl Thread {
.map(|worktree| Self::worktree_snapshot(worktree, git_store.clone(), cx))
.collect();
cx.spawn(async move |_, cx| {
cx.spawn(async move |_, _| {
let worktree_snapshots = futures::future::join_all(worktree_snapshots).await;
let mut unsaved_buffers = Vec::new();
cx.update(|app_cx| {
let buffer_store = project.read(app_cx).buffer_store();
for buffer_handle in buffer_store.read(app_cx).buffers() {
let buffer = buffer_handle.read(app_cx);
if buffer.is_dirty()
&& let Some(file) = buffer.file()
{
let path = file.path().to_string_lossy().to_string();
unsaved_buffers.push(path);
}
}
})
.ok();
Arc::new(ProjectSnapshot {
worktree_snapshots,
unsaved_buffer_paths: unsaved_buffers,
timestamp: Utc::now(),
})
})
@@ -914,7 +898,7 @@ impl Thread {
// Get worktree path and snapshot
let worktree_info = cx.update(|app_cx| {
let worktree = worktree.read(app_cx);
let path = worktree.abs_path().to_string_lossy().to_string();
let path = worktree.abs_path().to_string_lossy().into_owned();
let snapshot = worktree.snapshot();
(path, snapshot)
});

View File

@@ -84,9 +84,7 @@ impl AgentTool for CopyPathTool {
.and_then(|project_path| project.entry_for_path(&project_path, cx))
{
Some(entity) => match project.find_project_path(&input.destination_path, cx) {
Some(project_path) => {
project.copy_entry(entity.id, None, project_path.path, cx)
}
Some(project_path) => project.copy_entry(entity.id, project_path, cx),
None => Task::ready(Err(anyhow!(
"Destination path {} was outside the project.",
input.destination_path

View File

@@ -6,7 +6,7 @@ use language::{DiagnosticSeverity, OffsetRangeExt};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{fmt::Write, path::Path, sync::Arc};
use std::{fmt::Write, sync::Arc};
use ui::SharedString;
use util::markdown::MarkdownInlineCode;
@@ -147,9 +147,7 @@ impl AgentTool for DiagnosticsTool {
has_diagnostics = true;
output.push_str(&format!(
"{}: {} error(s), {} warning(s)\n",
Path::new(worktree.read(cx).root_name())
.join(project_path.path)
.display(),
worktree.read(cx).absolutize(&project_path.path).display(),
summary.error_count,
summary.warning_count
));

View File

@@ -17,10 +17,12 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::Settings;
use smol::stream::StreamExt as _;
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use ui::SharedString;
use util::ResultExt;
use util::rel_path::RelPath;
const DEFAULT_UI_TEXT: &str = "Editing file";
@@ -148,12 +150,11 @@ impl EditFileTool {
// If any path component matches the local settings folder, then this could affect
// the editor in ways beyond the project source, so prompt.
let local_settings_folder = paths::local_settings_folder_relative_path();
let local_settings_folder = paths::local_settings_folder_name();
let path = Path::new(&input.path);
if path
.components()
.any(|component| component.as_os_str() == local_settings_folder.as_os_str())
{
if path.components().any(|component| {
component.as_os_str() == <_ as AsRef<OsStr>>::as_ref(&local_settings_folder)
}) {
return event_stream.authorize(
format!("{} (local settings)", input.display_description),
cx,
@@ -162,6 +163,7 @@ impl EditFileTool {
// It's also possible that the global config dir is configured to be inside the project,
// so check for that edge case too.
// TODO this is broken when remoting
if let Ok(canonical_path) = std::fs::canonicalize(&input.path)
&& canonical_path.starts_with(paths::config_dir())
{
@@ -216,9 +218,7 @@ impl AgentTool for EditFileTool {
.read(cx)
.short_full_path_for_project_path(&project_path, cx)
})
.unwrap_or(Path::new(&input.path).into())
.to_string_lossy()
.to_string()
.unwrap_or(input.path.to_string_lossy().into_owned())
.into(),
Err(raw_input) => {
if let Some(input) =
@@ -235,9 +235,7 @@ impl AgentTool for EditFileTool {
.read(cx)
.short_full_path_for_project_path(&project_path, cx)
})
.unwrap_or(Path::new(&input.path).into())
.to_string_lossy()
.to_string()
.unwrap_or(input.path)
.into();
}
@@ -478,7 +476,7 @@ impl AgentTool for EditFileTool {
) -> Result<()> {
event_stream.update_diff(cx.new(|cx| {
Diff::finalized(
output.input_path,
output.input_path.to_string_lossy().into_owned(),
Some(output.old_text.to_string()),
output.new_text,
self.language_registry.clone(),
@@ -542,10 +540,12 @@ fn resolve_path(
let file_name = input
.path
.file_name()
.and_then(|file_name| file_name.to_str())
.and_then(|file_name| RelPath::unix(file_name).ok())
.context("Can't create file: invalid filename")?;
let new_file_path = parent_project_path.map(|parent| ProjectPath {
path: Arc::from(parent.path.join(file_name)),
path: parent.path.join(file_name),
..parent
});
@@ -565,7 +565,7 @@ mod tests {
use prompt_store::ProjectContext;
use serde_json::json;
use settings::SettingsStore;
use util::path;
use util::{path, rel_path::rel_path};
#[gpui::test]
async fn test_edit_nonexistent_file(cx: &mut TestAppContext) {
@@ -614,13 +614,13 @@ mod tests {
let mode = &EditFileMode::Create;
let result = test_resolve_path(mode, "root/new.txt", cx);
assert_resolved_path_eq(result.await, "new.txt");
assert_resolved_path_eq(result.await, rel_path("new.txt"));
let result = test_resolve_path(mode, "new.txt", cx);
assert_resolved_path_eq(result.await, "new.txt");
assert_resolved_path_eq(result.await, rel_path("new.txt"));
let result = test_resolve_path(mode, "dir/new.txt", cx);
assert_resolved_path_eq(result.await, "dir/new.txt");
assert_resolved_path_eq(result.await, rel_path("dir/new.txt"));
let result = test_resolve_path(mode, "root/dir/subdir/existing.txt", cx);
assert_eq!(
@@ -642,10 +642,10 @@ mod tests {
let path_with_root = "root/dir/subdir/existing.txt";
let path_without_root = "dir/subdir/existing.txt";
let result = test_resolve_path(mode, path_with_root, cx);
assert_resolved_path_eq(result.await, path_without_root);
assert_resolved_path_eq(result.await, rel_path(path_without_root));
let result = test_resolve_path(mode, path_without_root, cx);
assert_resolved_path_eq(result.await, path_without_root);
assert_resolved_path_eq(result.await, rel_path(path_without_root));
let result = test_resolve_path(mode, "root/nonexistent.txt", cx);
assert_eq!(
@@ -690,14 +690,10 @@ mod tests {
cx.update(|cx| resolve_path(&input, project, cx))
}
fn assert_resolved_path_eq(path: anyhow::Result<ProjectPath>, expected: &str) {
let actual = path
.expect("Should return valid path")
.path
.to_str()
.unwrap()
.replace("\\", "/"); // Naive Windows paths normalization
assert_eq!(actual, expected);
#[track_caller]
fn assert_resolved_path_eq(path: anyhow::Result<ProjectPath>, expected: &RelPath) {
let actual = path.expect("Should return valid path").path;
assert_eq!(actual.as_ref(), expected);
}
#[gpui::test]
@@ -1408,8 +1404,8 @@ mod tests {
// Parent directory references - find_project_path resolves these
(
"project/../other",
false,
"Path with .. is resolved by find_project_path",
true,
"Path with .. that goes outside of root directory",
),
(
"project/./src/file.rs",
@@ -1437,16 +1433,18 @@ mod tests {
)
});
cx.run_until_parked();
if should_confirm {
stream_rx.expect_authorization().await;
} else {
auth.await.unwrap();
assert!(
stream_rx.try_next().is_err(),
"Failed for case: {} - path: {} - expected no confirmation but got one",
description,
path
);
auth.await.unwrap();
}
}
}

View File

@@ -156,10 +156,14 @@ impl AgentTool for FindPathTool {
}
fn search_paths(glob: &str, project: Entity<Project>, cx: &mut App) -> Task<Result<Vec<PathBuf>>> {
let path_matcher = match PathMatcher::new([
// Sometimes models try to search for "". In this case, return all paths in the project.
if glob.is_empty() { "*" } else { glob },
]) {
let path_style = project.read(cx).path_style(cx);
let path_matcher = match PathMatcher::new(
[
// Sometimes models try to search for "". In this case, return all paths in the project.
if glob.is_empty() { "*" } else { glob },
],
path_style,
) {
Ok(matcher) => matcher,
Err(err) => return Task::ready(Err(anyhow!("Invalid glob: {err}"))),
};
@@ -173,9 +177,8 @@ fn search_paths(glob: &str, project: Entity<Project>, cx: &mut App) -> Task<Resu
let mut results = Vec::new();
for snapshot in snapshots {
for entry in snapshot.entries(false, 0) {
let root_name = PathBuf::from(snapshot.root_name());
if path_matcher.is_match(root_name.join(&entry.path)) {
results.push(snapshot.abs_path().join(entry.path.as_ref()));
if path_matcher.is_match(snapshot.root_name().join(&entry.path).as_std_path()) {
results.push(snapshot.absolutize(&entry.path));
}
}
}

View File

@@ -110,12 +110,15 @@ impl AgentTool for GrepTool {
const CONTEXT_LINES: u32 = 2;
const MAX_ANCESTOR_LINES: u32 = 10;
let path_style = self.project.read(cx).path_style(cx);
let include_matcher = match PathMatcher::new(
input
.include_pattern
.as_ref()
.into_iter()
.collect::<Vec<_>>(),
path_style,
) {
Ok(matcher) => matcher,
Err(error) => {
@@ -132,7 +135,7 @@ impl AgentTool for GrepTool {
.iter()
.chain(global_settings.private_files.sources().iter());
match PathMatcher::new(exclude_patterns) {
match PathMatcher::new(exclude_patterns, path_style) {
Ok(matcher) => matcher,
Err(error) => {
return Task::ready(Err(anyhow!("invalid exclude pattern: {error}")));

View File

@@ -2,12 +2,12 @@ use crate::{AgentTool, ToolCallEventStream};
use agent_client_protocol::ToolKind;
use anyhow::{Result, anyhow};
use gpui::{App, Entity, SharedString, Task};
use project::{Project, WorktreeSettings};
use project::{Project, ProjectPath, WorktreeSettings};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::Settings;
use std::fmt::Write;
use std::{path::Path, sync::Arc};
use std::sync::Arc;
use util::markdown::MarkdownInlineCode;
/// Lists files and directories in a given path. Prefer the `grep` or `find_path` tools when searching the codebase.
@@ -86,13 +86,13 @@ impl AgentTool for ListDirectoryTool {
.read(cx)
.worktrees(cx)
.filter_map(|worktree| {
worktree.read(cx).root_entry().and_then(|entry| {
if entry.is_dir() {
entry.path.to_str()
} else {
None
}
})
let worktree = worktree.read(cx);
let root_entry = worktree.root_entry()?;
if root_entry.is_dir() {
Some(root_entry.path.display(worktree.path_style()))
} else {
None
}
})
.collect::<Vec<_>>()
.join("\n");
@@ -143,7 +143,7 @@ impl AgentTool for ListDirectoryTool {
}
let worktree_snapshot = worktree.read(cx).snapshot();
let worktree_root_name = worktree.read(cx).root_name().to_string();
let worktree_root_name = worktree.read(cx).root_name();
let Some(entry) = worktree_snapshot.entry_for_path(&project_path.path) else {
return Task::ready(Err(anyhow!("Path not found: {}", input.path)));
@@ -165,25 +165,17 @@ impl AgentTool for ListDirectoryTool {
continue;
}
if self
.project
.read(cx)
.find_project_path(&entry.path, cx)
.map(|project_path| {
let worktree_settings = WorktreeSettings::get(Some((&project_path).into()), cx);
worktree_settings.is_path_excluded(&project_path.path)
|| worktree_settings.is_path_private(&project_path.path)
})
.unwrap_or(false)
let project_path: ProjectPath = (worktree_snapshot.id(), entry.path.clone()).into();
if worktree_settings.is_path_excluded(&project_path.path)
|| worktree_settings.is_path_private(&project_path.path)
{
continue;
}
let full_path = Path::new(&worktree_root_name)
let full_path = worktree_root_name
.join(&entry.path)
.display()
.to_string();
.display(worktree_snapshot.path_style())
.into_owned();
if entry.is_dir() {
folders.push(full_path);
} else {

View File

@@ -98,7 +98,7 @@ impl AgentTool for MovePathTool {
.and_then(|project_path| project.entry_for_path(&project_path, cx))
{
Some(entity) => match project.find_project_path(&input.destination_path, cx) {
Some(project_path) => project.rename_entry(entity.id, project_path.path, cx),
Some(project_path) => project.rename_entry(entity.id, project_path, cx),
None => Task::ready(Err(anyhow!(
"Destination path {} was outside the project.",
input.destination_path

View File

@@ -104,7 +104,7 @@ mod tests {
async fn test_to_absolute_path(cx: &mut TestAppContext) {
init_test(cx);
let temp_dir = TempDir::new().expect("Failed to create temp directory");
let temp_path = temp_dir.path().to_string_lossy().to_string();
let temp_path = temp_dir.path().to_string_lossy().into_owned();
let fs = FakeFs::new(cx.executor());
fs.insert_tree(

View File

@@ -82,12 +82,12 @@ impl AgentTool for ReadFileTool {
{
match (input.start_line, input.end_line) {
(Some(start), Some(end)) => {
format!("Read file `{}` (lines {}-{})", path.display(), start, end,)
format!("Read file `{path}` (lines {}-{})", start, end,)
}
(Some(start), None) => {
format!("Read file `{}` (from line {})", path.display(), start)
format!("Read file `{path}` (from line {})", start)
}
_ => format!("Read file `{}`", path.display()),
_ => format!("Read file `{path}`"),
}
.into()
} else {
@@ -225,9 +225,12 @@ impl AgentTool for ReadFileTool {
Ok(result.into())
} else {
// No line ranges specified, so check file size to see if it's too big.
let buffer_content =
outline::get_buffer_content_or_outline(buffer.clone(), Some(&abs_path), cx)
.await?;
let buffer_content = outline::get_buffer_content_or_outline(
buffer.clone(),
Some(&abs_path.to_string_lossy()),
cx,
)
.await?;
action_log.update(cx, |log, cx| {
log.buffer_read(buffer.clone(), cx);

View File

@@ -99,6 +99,9 @@ pub fn load_proxy_env(cx: &mut App) -> HashMap<String, String> {
if let Some(no_proxy) = read_no_proxy_from_env() {
env.insert("NO_PROXY".to_owned(), no_proxy);
} else if proxy_url.is_some() {
// We sometimes need local MCP servers that we don't want to proxy
env.insert("NO_PROXY".to_owned(), "localhost,127.0.0.1".to_owned());
}
env

View File

@@ -62,7 +62,7 @@ impl AgentServer for ClaudeCode {
cx: &mut App,
) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>> {
let name = self.name();
let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().to_string());
let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().into_owned());
let is_remote = delegate.project.read(cx).is_via_remote_server();
let store = delegate.store.downgrade();
let extra_env = load_proxy_env(cx);

View File

@@ -67,7 +67,7 @@ impl crate::AgentServer for CustomAgentServer {
cx: &mut App,
) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>> {
let name = self.name();
let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().to_string());
let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().into_owned());
let is_remote = delegate.project.read(cx).is_via_remote_server();
let default_mode = self.default_mode(cx);
let store = delegate.store.downgrade();

View File

@@ -31,7 +31,7 @@ impl AgentServer for Gemini {
cx: &mut App,
) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>> {
let name = self.name();
let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().to_string());
let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().into_owned());
let is_remote = delegate.project.read(cx).is_via_remote_server();
let store = delegate.store.downgrade();
let mut extra_env = load_proxy_env(cx);

View File

@@ -1,5 +1,6 @@
use std::cell::RefCell;
use std::ops::Range;
use std::path::PathBuf;
use std::rc::Rc;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
@@ -13,7 +14,7 @@ use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{App, Entity, Task, WeakEntity};
use language::{Buffer, CodeLabel, HighlightId};
use lsp::CompletionContext;
use project::lsp_store::CompletionDocumentation;
use project::lsp_store::{CompletionDocumentation, SymbolLocation};
use project::{
Completion, CompletionDisplayOptions, CompletionIntent, CompletionResponse, Project,
ProjectPath, Symbol, WorktreeId,
@@ -22,6 +23,7 @@ use prompt_store::PromptStore;
use rope::Point;
use text::{Anchor, ToPoint as _};
use ui::prelude::*;
use util::rel_path::RelPath;
use workspace::Workspace;
use crate::AgentPanel;
@@ -187,7 +189,7 @@ impl ContextPickerCompletionProvider {
pub(crate) fn completion_for_path(
project_path: ProjectPath,
path_prefix: &str,
path_prefix: &RelPath,
is_recent: bool,
is_directory: bool,
source_range: Range<Anchor>,
@@ -195,10 +197,12 @@ impl ContextPickerCompletionProvider {
project: Entity<Project>,
cx: &mut App,
) -> Option<Completion> {
let path_style = project.read(cx).path_style(cx);
let (file_name, directory) =
crate::context_picker::file_context_picker::extract_file_name_and_directory(
&project_path.path,
path_prefix,
path_style,
);
let label =
@@ -250,7 +254,15 @@ impl ContextPickerCompletionProvider {
let label = CodeLabel::plain(symbol.name.clone(), None);
let abs_path = project.read(cx).absolute_path(&symbol.path, cx)?;
let abs_path = match &symbol.path {
SymbolLocation::InProject(project_path) => {
project.read(cx).absolute_path(&project_path, cx)?
}
SymbolLocation::OutsideProject {
abs_path,
signature: _,
} => PathBuf::from(abs_path.as_ref()),
};
let uri = MentionUri::Symbol {
abs_path,
name: symbol.name.clone(),

View File

@@ -48,7 +48,7 @@ use std::{
use text::OffsetRangeExt;
use theme::ThemeSettings;
use ui::{ButtonLike, TintColor, Toggleable, prelude::*};
use util::{ResultExt, debug_panic};
use util::{ResultExt, debug_panic, rel_path::RelPath};
use workspace::{Workspace, notifications::NotifyResultExt as _};
use zed_actions::agent::Chat;
@@ -76,7 +76,7 @@ pub enum MessageEditorEvent {
impl EventEmitter<MessageEditorEvent> for MessageEditor {}
const COMMAND_HINT_INLAY_ID: usize = 0;
const COMMAND_HINT_INLAY_ID: u32 = 0;
impl MessageEditor {
pub fn new(
@@ -452,9 +452,12 @@ impl MessageEditor {
.update(cx, |project, cx| project.open_buffer(project_path, cx));
cx.spawn(async move |_, cx| {
let buffer = buffer.await?;
let buffer_content =
outline::get_buffer_content_or_outline(buffer.clone(), Some(&abs_path), &cx)
.await?;
let buffer_content = outline::get_buffer_content_or_outline(
buffer.clone(),
Some(&abs_path.to_string_lossy()),
&cx,
)
.await?;
Ok(Mention::Text {
content: buffer_content.text,
@@ -947,6 +950,7 @@ impl MessageEditor {
window: &mut Window,
cx: &mut Context<Self>,
) {
let path_style = self.project.read(cx).path_style(cx);
let buffer = self.editor.read(cx).buffer().clone();
let Some(buffer) = buffer.read(cx).as_singleton() else {
return;
@@ -956,18 +960,15 @@ impl MessageEditor {
let Some(entry) = self.project.read(cx).entry_for_path(&path, cx) else {
continue;
};
let Some(abs_path) = self.project.read(cx).absolute_path(&path, cx) else {
let Some(worktree) = self.project.read(cx).worktree_for_id(path.worktree_id, cx) else {
continue;
};
let path_prefix = abs_path
.file_name()
.unwrap_or(path.path.as_os_str())
.display()
.to_string();
let abs_path = worktree.read(cx).absolutize(&path.path);
let (file_name, _) =
crate::context_picker::file_context_picker::extract_file_name_and_directory(
&path.path,
&path_prefix,
worktree.read(cx).root_name(),
path_style,
);
let uri = if entry.is_dir() {
@@ -1176,14 +1177,20 @@ fn full_mention_for_directory(
abs_path: &Path,
cx: &mut App,
) -> Task<Result<Mention>> {
fn collect_files_in_path(worktree: &Worktree, path: &Path) -> Vec<(Arc<Path>, PathBuf)> {
fn collect_files_in_path(worktree: &Worktree, path: &RelPath) -> Vec<(Arc<RelPath>, String)> {
let mut files = Vec::new();
for entry in worktree.child_entries(path) {
if entry.is_dir() {
files.extend(collect_files_in_path(worktree, &entry.path));
} else if entry.is_file() {
files.push((entry.path.clone(), worktree.full_path(&entry.path)));
files.push((
entry.path.clone(),
worktree
.full_path(&entry.path)
.to_string_lossy()
.to_string(),
));
}
}
@@ -1261,7 +1268,7 @@ fn full_mention_for_directory(
})
}
fn render_directory_contents(entries: Vec<(Arc<Path>, PathBuf, String)>) -> String {
fn render_directory_contents(entries: Vec<(Arc<RelPath>, String, String)>) -> String {
let mut output = String::new();
for (_relative_path, full_path, content) in entries {
let fence = codeblock_fence_for_path(Some(&full_path), None);
@@ -1595,7 +1602,7 @@ mod tests {
use serde_json::json;
use text::Point;
use ui::{App, Context, IntoElement, Render, SharedString, Window};
use util::{path, uri};
use util::{path, paths::PathStyle, rel_path::rel_path};
use workspace::{AppState, Item, Workspace};
use crate::acp::{
@@ -2105,16 +2112,18 @@ mod tests {
let mut cx = VisualTestContext::from_window(*window, cx);
let paths = vec![
path!("a/one.txt"),
path!("a/two.txt"),
path!("a/three.txt"),
path!("a/four.txt"),
path!("b/five.txt"),
path!("b/six.txt"),
path!("b/seven.txt"),
path!("b/eight.txt"),
rel_path("a/one.txt"),
rel_path("a/two.txt"),
rel_path("a/three.txt"),
rel_path("a/four.txt"),
rel_path("b/five.txt"),
rel_path("b/six.txt"),
rel_path("b/seven.txt"),
rel_path("b/eight.txt"),
];
let slash = PathStyle::local().separator();
let mut opened_editors = Vec::new();
for path in paths {
let buffer = workspace
@@ -2122,7 +2131,7 @@ mod tests {
workspace.open_path(
ProjectPath {
worktree_id,
path: Path::new(path).into(),
path: path.into(),
},
None,
false,
@@ -2183,10 +2192,10 @@ mod tests {
assert_eq!(
current_completion_labels(editor),
&[
"eight.txt dir/b/",
"seven.txt dir/b/",
"six.txt dir/b/",
"five.txt dir/b/",
format!("eight.txt dir{slash}b{slash}"),
format!("seven.txt dir{slash}b{slash}"),
format!("six.txt dir{slash}b{slash}"),
format!("five.txt dir{slash}b{slash}"),
]
);
editor.set_text("", window, cx);
@@ -2214,14 +2223,14 @@ mod tests {
assert_eq!(
current_completion_labels(editor),
&[
"eight.txt dir/b/",
"seven.txt dir/b/",
"six.txt dir/b/",
"five.txt dir/b/",
"Files & Directories",
"Symbols",
"Threads",
"Fetch"
format!("eight.txt dir{slash}b{slash}"),
format!("seven.txt dir{slash}b{slash}"),
format!("six.txt dir{slash}b{slash}"),
format!("five.txt dir{slash}b{slash}"),
"Files & Directories".into(),
"Symbols".into(),
"Threads".into(),
"Fetch".into()
]
);
});
@@ -2248,7 +2257,10 @@ mod tests {
editor.update(&mut cx, |editor, cx| {
assert_eq!(editor.text(cx), "Lorem @file one");
assert!(editor.has_visible_completions_menu());
assert_eq!(current_completion_labels(editor), vec!["one.txt dir/a/"]);
assert_eq!(
current_completion_labels(editor),
vec![format!("one.txt dir{slash}a{slash}")]
);
});
editor.update_in(&mut cx, |editor, window, cx| {
@@ -2256,7 +2268,11 @@ mod tests {
editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
});
let url_one = uri!("file:///dir/a/one.txt");
let url_one = MentionUri::File {
abs_path: path!("/dir/a/one.txt").into(),
}
.to_uri()
.to_string();
editor.update(&mut cx, |editor, cx| {
let text = editor.text(cx);
assert_eq!(text, format!("Lorem [@one.txt]({url_one}) "));
@@ -2361,7 +2377,11 @@ mod tests {
.into_values()
.collect::<Vec<_>>();
let url_eight = uri!("file:///dir/b/eight.txt");
let url_eight = MentionUri::File {
abs_path: path!("/dir/b/eight.txt").into(),
}
.to_uri()
.to_string();
{
let [_, (uri, Mention::Text { content, .. })] = contents.as_slice() else {
@@ -2460,6 +2480,12 @@ mod tests {
editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
});
let symbol = MentionUri::Symbol {
abs_path: path!("/dir/a/one.txt").into(),
name: "MySymbol".into(),
line_range: 0..=0,
};
let contents = message_editor
.update(&mut cx, |message_editor, cx| {
message_editor.mention_set().contents(
@@ -2479,12 +2505,7 @@ mod tests {
panic!("Unexpected mentions");
};
pretty_assertions::assert_eq!(content, "1");
pretty_assertions::assert_eq!(
uri,
&format!("{url_one}?symbol=MySymbol#L1:1")
.parse::<MentionUri>()
.unwrap()
);
pretty_assertions::assert_eq!(uri, &symbol);
}
cx.run_until_parked();
@@ -2492,7 +2513,10 @@ mod tests {
editor.read_with(&cx, |editor, cx| {
assert_eq!(
editor.text(cx),
format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) ")
format!(
"Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) ",
symbol.to_uri(),
)
);
});
@@ -2502,10 +2526,10 @@ mod tests {
editor.update(&mut cx, |editor, cx| {
assert_eq!(
editor.text(cx),
format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) @file x.png")
format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) @file x.png", symbol.to_uri())
);
assert!(editor.has_visible_completions_menu());
assert_eq!(current_completion_labels(editor), &["x.png dir/"]);
assert_eq!(current_completion_labels(editor), &[format!("x.png dir{slash}")]);
});
editor.update_in(&mut cx, |editor, window, cx| {
@@ -2531,7 +2555,10 @@ mod tests {
editor.read_with(&cx, |editor, cx| {
assert_eq!(
editor.text(cx),
format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) ")
format!(
"Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) ",
symbol.to_uri()
)
);
});
@@ -2541,10 +2568,10 @@ mod tests {
editor.update(&mut cx, |editor, cx| {
assert_eq!(
editor.text(cx),
format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) @file x.png")
format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) @file x.png", symbol.to_uri())
);
assert!(editor.has_visible_completions_menu());
assert_eq!(current_completion_labels(editor), &["x.png dir/"]);
assert_eq!(current_completion_labels(editor), &[format!("x.png dir{slash}")]);
});
editor.update_in(&mut cx, |editor, window, cx| {
@@ -2556,11 +2583,14 @@ mod tests {
// Mention was removed
editor.read_with(&cx, |editor, cx| {
assert_eq!(
editor.text(cx),
format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) ")
);
});
assert_eq!(
editor.text(cx),
format!(
"Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({}) ",
symbol.to_uri()
)
);
});
// Now getting the contents succeeds, because the invalid mention was removed
let contents = message_editor

View File

@@ -3704,29 +3704,32 @@ impl AcpThreadView {
|(index, (buffer, _diff))| {
let file = buffer.read(cx).file()?;
let path = file.path();
let path_style = file.path_style(cx);
let separator = file.path_style(cx).separator();
let file_path = path.parent().and_then(|parent| {
let parent_str = parent.to_string_lossy();
if parent_str.is_empty() {
if parent.is_empty() {
None
} else {
Some(
Label::new(format!("/{}{}", parent_str, std::path::MAIN_SEPARATOR_STR))
.color(Color::Muted)
.size(LabelSize::XSmall)
.buffer_font(cx),
Label::new(format!(
"{separator}{}{separator}",
parent.display(path_style)
))
.color(Color::Muted)
.size(LabelSize::XSmall)
.buffer_font(cx),
)
}
});
let file_name = path.file_name().map(|name| {
Label::new(name.to_string_lossy().to_string())
Label::new(name.to_string())
.size(LabelSize::XSmall)
.buffer_font(cx)
});
let file_icon = FileIcons::get_icon(path, cx)
let file_icon = FileIcons::get_icon(path.as_std_path(), cx)
.map(Icon::from_path)
.map(|icon| icon.color(Color::Muted).size(IconSize::Small))
.unwrap_or_else(|| {
@@ -4569,7 +4572,7 @@ impl AcpThreadView {
.read(cx)
.visible_worktrees(cx)
.next()
.map(|worktree| worktree.read(cx).root_name().to_string())
.map(|worktree| worktree.read(cx).root_name_str().to_string())
});
if let Some(screen_window) = cx

View File

@@ -264,7 +264,7 @@ pub fn init(
init_language_model_settings(cx);
}
assistant_slash_command::init(cx);
agent::init(cx);
agent::init(fs.clone(), cx);
agent_panel::init(cx);
context_server_configuration::init(language_registry.clone(), fs.clone(), cx);
TextThreadEditor::init(cx);

View File

@@ -33,6 +33,8 @@ use thread_context_picker::{
use ui::{
ButtonLike, ContextMenu, ContextMenuEntry, ContextMenuItem, Disclosure, TintColor, prelude::*,
};
use util::paths::PathStyle;
use util::rel_path::RelPath;
use workspace::{Workspace, notifications::NotifyResultExt};
use agent::{
@@ -228,12 +230,19 @@ impl ContextPicker {
let context_picker = cx.entity();
let menu = ContextMenu::build(window, cx, move |menu, _window, cx| {
let Some(workspace) = self.workspace.upgrade() else {
return menu;
};
let path_style = workspace.read(cx).path_style(cx);
let recent = self.recent_entries(cx);
let has_recent = !recent.is_empty();
let recent_entries = recent
.into_iter()
.enumerate()
.map(|(ix, entry)| self.recent_menu_item(context_picker.clone(), ix, entry));
.map(|(ix, entry)| {
self.recent_menu_item(context_picker.clone(), ix, entry, path_style)
})
.collect::<Vec<_>>();
let entries = self
.workspace
@@ -395,6 +404,7 @@ impl ContextPicker {
context_picker: Entity<ContextPicker>,
ix: usize,
entry: RecentEntry,
path_style: PathStyle,
) -> ContextMenuItem {
match entry {
RecentEntry::File {
@@ -413,6 +423,7 @@ impl ContextPicker {
&path,
&path_prefix,
false,
path_style,
context_store.clone(),
cx,
)
@@ -586,7 +597,7 @@ impl Render for ContextPicker {
pub(crate) enum RecentEntry {
File {
project_path: ProjectPath,
path_prefix: Arc<str>,
path_prefix: Arc<RelPath>,
},
Thread(ThreadContextEntry),
}

View File

@@ -13,6 +13,7 @@ use http_client::HttpClientWithUrl;
use itertools::Itertools;
use language::{Buffer, CodeLabel, HighlightId};
use lsp::CompletionContext;
use project::lsp_store::SymbolLocation;
use project::{
Completion, CompletionDisplayOptions, CompletionIntent, CompletionResponse, ProjectPath,
Symbol, WorktreeId,
@@ -22,6 +23,8 @@ use rope::Point;
use text::{Anchor, OffsetRangeExt, ToPoint};
use ui::prelude::*;
use util::ResultExt as _;
use util::paths::PathStyle;
use util::rel_path::RelPath;
use workspace::Workspace;
use agent::{
@@ -574,11 +577,12 @@ impl ContextPickerCompletionProvider {
fn completion_for_path(
project_path: ProjectPath,
path_prefix: &str,
path_prefix: &RelPath,
is_recent: bool,
is_directory: bool,
excerpt_id: ExcerptId,
source_range: Range<Anchor>,
path_style: PathStyle,
editor: Entity<Editor>,
context_store: Entity<ContextStore>,
cx: &App,
@@ -586,6 +590,7 @@ impl ContextPickerCompletionProvider {
let (file_name, directory) = super::file_context_picker::extract_file_name_and_directory(
&project_path.path,
path_prefix,
path_style,
);
let label =
@@ -657,17 +662,22 @@ impl ContextPickerCompletionProvider {
workspace: Entity<Workspace>,
cx: &mut App,
) -> Option<Completion> {
let path_style = workspace.read(cx).path_style(cx);
let SymbolLocation::InProject(symbol_path) = &symbol.path else {
return None;
};
let path_prefix = workspace
.read(cx)
.project()
.read(cx)
.worktree_for_id(symbol.path.worktree_id, cx)?
.worktree_for_id(symbol_path.worktree_id, cx)?
.read(cx)
.root_name();
let (file_name, directory) = super::file_context_picker::extract_file_name_and_directory(
&symbol.path.path,
&symbol_path.path,
path_prefix,
path_style,
);
let full_path = if let Some(directory) = directory {
format!("{}{}", directory, file_name)
@@ -768,6 +778,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
let text_thread_store = self.text_thread_store.clone();
let editor = self.editor.clone();
let http_client = workspace.read(cx).client().http_client();
let path_style = workspace.read(cx).path_style(cx);
let MentionCompletion { mode, argument, .. } = state;
let query = argument.unwrap_or_else(|| "".to_string());
@@ -834,6 +845,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
mat.is_dir,
excerpt_id,
source_range.clone(),
path_style,
editor.clone(),
context_store.clone(),
cx,
@@ -1064,7 +1076,7 @@ mod tests {
use serde_json::json;
use settings::SettingsStore;
use std::{ops::Deref, rc::Rc};
use util::path;
use util::{path, rel_path::rel_path};
use workspace::{AppState, Item};
#[test]
@@ -1215,16 +1227,18 @@ mod tests {
let mut cx = VisualTestContext::from_window(*window.deref(), cx);
let paths = vec![
path!("a/one.txt"),
path!("a/two.txt"),
path!("a/three.txt"),
path!("a/four.txt"),
path!("b/five.txt"),
path!("b/six.txt"),
path!("b/seven.txt"),
path!("b/eight.txt"),
rel_path("a/one.txt"),
rel_path("a/two.txt"),
rel_path("a/three.txt"),
rel_path("a/four.txt"),
rel_path("b/five.txt"),
rel_path("b/six.txt"),
rel_path("b/seven.txt"),
rel_path("b/eight.txt"),
];
let slash = PathStyle::local().separator();
let mut opened_editors = Vec::new();
for path in paths {
let buffer = workspace
@@ -1232,7 +1246,7 @@ mod tests {
workspace.open_path(
ProjectPath {
worktree_id,
path: Path::new(path).into(),
path: path.into(),
},
None,
false,
@@ -1308,13 +1322,13 @@ mod tests {
assert_eq!(
current_completion_labels(editor),
&[
"seven.txt dir/b/",
"six.txt dir/b/",
"five.txt dir/b/",
"four.txt dir/a/",
"Files & Directories",
"Symbols",
"Fetch"
format!("seven.txt dir{slash}b{slash}"),
format!("six.txt dir{slash}b{slash}"),
format!("five.txt dir{slash}b{slash}"),
format!("four.txt dir{slash}a{slash}"),
"Files & Directories".into(),
"Symbols".into(),
"Fetch".into()
]
);
});
@@ -1341,7 +1355,10 @@ mod tests {
editor.update(&mut cx, |editor, cx| {
assert_eq!(editor.text(cx), "Lorem @file one");
assert!(editor.has_visible_completions_menu());
assert_eq!(current_completion_labels(editor), vec!["one.txt dir/a/"]);
assert_eq!(
current_completion_labels(editor),
vec![format!("one.txt dir{slash}a{slash}")]
);
});
editor.update_in(&mut cx, |editor, window, cx| {
@@ -1350,7 +1367,10 @@ mod tests {
});
editor.update(&mut cx, |editor, cx| {
assert_eq!(editor.text(cx), "Lorem [@one.txt](@file:dir/a/one.txt) ");
assert_eq!(
editor.text(cx),
format!("Lorem [@one.txt](@file:dir{slash}a{slash}one.txt) ")
);
assert!(!editor.has_visible_completions_menu());
assert_eq!(
fold_ranges(editor, cx),
@@ -1361,7 +1381,10 @@ mod tests {
cx.simulate_input(" ");
editor.update(&mut cx, |editor, cx| {
assert_eq!(editor.text(cx), "Lorem [@one.txt](@file:dir/a/one.txt) ");
assert_eq!(
editor.text(cx),
format!("Lorem [@one.txt](@file:dir{slash}a{slash}one.txt) ")
);
assert!(!editor.has_visible_completions_menu());
assert_eq!(
fold_ranges(editor, cx),
@@ -1374,7 +1397,7 @@ mod tests {
editor.update(&mut cx, |editor, cx| {
assert_eq!(
editor.text(cx),
"Lorem [@one.txt](@file:dir/a/one.txt) Ipsum ",
format!("Lorem [@one.txt](@file:dir{slash}a{slash}one.txt) Ipsum "),
);
assert!(!editor.has_visible_completions_menu());
assert_eq!(
@@ -1388,7 +1411,7 @@ mod tests {
editor.update(&mut cx, |editor, cx| {
assert_eq!(
editor.text(cx),
"Lorem [@one.txt](@file:dir/a/one.txt) Ipsum @file ",
format!("Lorem [@one.txt](@file:dir{slash}a{slash}one.txt) Ipsum @file "),
);
assert!(editor.has_visible_completions_menu());
assert_eq!(
@@ -1406,7 +1429,7 @@ mod tests {
editor.update(&mut cx, |editor, cx| {
assert_eq!(
editor.text(cx),
"Lorem [@one.txt](@file:dir/a/one.txt) Ipsum [@seven.txt](@file:dir/b/seven.txt) "
format!("Lorem [@one.txt](@file:dir{slash}a{slash}one.txt) Ipsum [@seven.txt](@file:dir{slash}b{slash}seven.txt) ")
);
assert!(!editor.has_visible_completions_menu());
assert_eq!(
@@ -1423,7 +1446,7 @@ mod tests {
editor.update(&mut cx, |editor, cx| {
assert_eq!(
editor.text(cx),
"Lorem [@one.txt](@file:dir/a/one.txt) Ipsum [@seven.txt](@file:dir/b/seven.txt) \n@"
format!("Lorem [@one.txt](@file:dir{slash}a{slash}one.txt) Ipsum [@seven.txt](@file:dir{slash}b{slash}seven.txt) \n@")
);
assert!(editor.has_visible_completions_menu());
assert_eq!(
@@ -1444,7 +1467,7 @@ mod tests {
editor.update(&mut cx, |editor, cx| {
assert_eq!(
editor.text(cx),
"Lorem [@one.txt](@file:dir/a/one.txt) Ipsum [@seven.txt](@file:dir/b/seven.txt) \n[@six.txt](@file:dir/b/six.txt) "
format!("Lorem [@one.txt](@file:dir{slash}a{slash}one.txt) Ipsum [@seven.txt](@file:dir{slash}b{slash}seven.txt) \n[@six.txt](@file:dir{slash}b{slash}six.txt) ")
);
assert!(!editor.has_visible_completions_menu());
assert_eq!(

View File

@@ -1,4 +1,3 @@
use std::path::Path;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
@@ -10,7 +9,7 @@ use gpui::{
use picker::{Picker, PickerDelegate};
use project::{PathMatchCandidateSet, ProjectPath, WorktreeId};
use ui::{ListItem, Tooltip, prelude::*};
use util::ResultExt as _;
use util::{ResultExt as _, paths::PathStyle, rel_path::RelPath};
use workspace::Workspace;
use crate::context_picker::ContextPicker;
@@ -161,6 +160,8 @@ impl PickerDelegate for FileContextPickerDelegate {
cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
let FileMatch { mat, .. } = &self.matches.get(ix)?;
let workspace = self.workspace.upgrade()?;
let path_style = workspace.read(cx).path_style(cx);
Some(
ListItem::new(ix)
@@ -172,6 +173,7 @@ impl PickerDelegate for FileContextPickerDelegate {
&mat.path,
&mat.path_prefix,
mat.is_dir,
path_style,
self.context_store.clone(),
cx,
)),
@@ -214,14 +216,13 @@ pub(crate) fn search_files(
let file_matches = project.worktrees(cx).flat_map(|worktree| {
let worktree = worktree.read(cx);
let path_prefix: Arc<str> = worktree.root_name().into();
worktree.entries(false, 0).map(move |entry| FileMatch {
mat: PathMatch {
score: 0.,
positions: Vec::new(),
worktree_id: worktree.id().to_usize(),
path: entry.path.clone(),
path_prefix: path_prefix.clone(),
path_prefix: worktree.root_name().into(),
distance_to_relative_ancestor: 0,
is_dir: entry.is_dir(),
},
@@ -269,51 +270,31 @@ pub(crate) fn search_files(
}
pub fn extract_file_name_and_directory(
path: &Path,
path_prefix: &str,
path: &RelPath,
path_prefix: &RelPath,
path_style: PathStyle,
) -> (SharedString, Option<SharedString>) {
if path == Path::new("") {
(
SharedString::from(
path_prefix
.trim_end_matches(std::path::MAIN_SEPARATOR)
.to_string(),
),
None,
)
} else {
let file_name = path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string()
.into();
let mut directory = path_prefix
.trim_end_matches(std::path::MAIN_SEPARATOR)
.to_string();
if !directory.ends_with('/') {
directory.push('/');
}
if let Some(parent) = path.parent().filter(|parent| parent != &Path::new("")) {
directory.push_str(&parent.to_string_lossy());
directory.push('/');
}
(file_name, Some(directory.into()))
}
let full_path = path_prefix.join(path);
let file_name = full_path.file_name().unwrap_or_default();
let display_path = full_path.display(path_style);
let (directory, file_name) = display_path.split_at(display_path.len() - file_name.len());
(
file_name.to_string().into(),
Some(SharedString::new(directory)).filter(|dir| !dir.is_empty()),
)
}
pub fn render_file_context_entry(
id: ElementId,
worktree_id: WorktreeId,
path: &Arc<Path>,
path_prefix: &Arc<str>,
path: &Arc<RelPath>,
path_prefix: &Arc<RelPath>,
is_directory: bool,
path_style: PathStyle,
context_store: WeakEntity<ContextStore>,
cx: &App,
) -> Stateful<Div> {
let (file_name, directory) = extract_file_name_and_directory(path, path_prefix);
let (file_name, directory) = extract_file_name_and_directory(path, path_prefix, path_style);
let added = context_store.upgrade().and_then(|context_store| {
let project_path = ProjectPath {
@@ -330,9 +311,9 @@ pub fn render_file_context_entry(
});
let file_icon = if is_directory {
FileIcons::get_folder_icon(false, path, cx)
FileIcons::get_folder_icon(false, path.as_std_path(), cx)
} else {
FileIcons::get_icon(path, cx)
FileIcons::get_icon(path.as_std_path(), cx)
}
.map(Icon::from_path)
.unwrap_or_else(|| Icon::new(IconName::File));

View File

@@ -2,13 +2,14 @@ use std::cmp::Reverse;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use anyhow::Result;
use anyhow::{Result, anyhow};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
App, AppContext, DismissEvent, Entity, FocusHandle, Focusable, Stateful, Task, WeakEntity,
};
use ordered_float::OrderedFloat;
use picker::{Picker, PickerDelegate};
use project::lsp_store::SymbolLocation;
use project::{DocumentSymbol, Symbol};
use ui::{ListItem, prelude::*};
use util::ResultExt as _;
@@ -191,7 +192,10 @@ pub(crate) fn add_symbol(
) -> Task<Result<(Option<AgentContextHandle>, bool)>> {
let project = workspace.read(cx).project().clone();
let open_buffer_task = project.update(cx, |project, cx| {
project.open_buffer(symbol.path.clone(), cx)
let SymbolLocation::InProject(symbol_path) = &symbol.path else {
return Task::ready(Err(anyhow!("can't add symbol from outside of project")));
};
project.open_buffer(symbol_path.clone(), cx)
});
cx.spawn(async move |cx| {
let buffer = open_buffer_task.await?;
@@ -291,10 +295,11 @@ pub(crate) fn search_symbols(
.map(|(id, symbol)| {
StringMatchCandidate::new(id, symbol.label.filter_text())
})
.partition(|candidate| {
project
.entry_for_path(&symbols[candidate.id].path, cx)
.is_some_and(|e| !e.is_ignored)
.partition(|candidate| match &symbols[candidate.id].path {
SymbolLocation::InProject(project_path) => project
.entry_for_path(project_path, cx)
.is_some_and(|e| !e.is_ignored),
SymbolLocation::OutsideProject { .. } => false,
})
})
.log_err()
@@ -360,13 +365,18 @@ fn compute_symbol_entries(
}
pub fn render_symbol_context_entry(id: ElementId, entry: &SymbolEntry) -> Stateful<Div> {
let path = entry
.symbol
.path
.path
.file_name()
.map(|s| s.to_string_lossy())
.unwrap_or_default();
let path = match &entry.symbol.path {
SymbolLocation::InProject(project_path) => {
project_path.path.file_name().unwrap_or_default().into()
}
SymbolLocation::OutsideProject {
abs_path,
signature: _,
} => abs_path
.file_name()
.map(|f| f.to_string_lossy())
.unwrap_or_default(),
};
let symbol_location = format!("{} L{}", path, entry.symbol.range.start.0.row + 1);
h_flex()

View File

@@ -238,7 +238,7 @@ impl TerminalInlineAssistant {
let latest_output = terminal.last_n_non_empty_lines(DEFAULT_CONTEXT_LINES);
let working_directory = terminal
.working_directory()
.map(|path| path.to_string_lossy().to_string());
.map(|path| path.to_string_lossy().into_owned());
(latest_output, working_directory)
})
.ok()

View File

@@ -1431,10 +1431,14 @@ impl TextThreadEditor {
else {
continue;
};
let worktree_root_name = worktree.read(cx).root_name().to_string();
let mut full_path = PathBuf::from(worktree_root_name.clone());
full_path.push(&project_path.path);
file_slash_command_args.push(full_path.to_string_lossy().to_string());
let path_style = worktree.read(cx).path_style();
let full_path = worktree
.read(cx)
.root_name()
.join(&project_path.path)
.display(path_style)
.into_owned();
file_slash_command_args.push(full_path);
}
let cmd_name = FileSlashCommand.name();

View File

@@ -17,6 +17,7 @@ use agent::context::{
FileContextHandle, ImageContext, ImageStatus, RulesContextHandle, SelectionContextHandle,
SymbolContextHandle, TextThreadContextHandle, ThreadContextHandle,
};
use util::paths::PathStyle;
#[derive(IntoElement)]
pub enum ContextPill {
@@ -303,33 +304,54 @@ impl AddedContext {
cx: &App,
) -> Option<AddedContext> {
match handle {
AgentContextHandle::File(handle) => Self::pending_file(handle, cx),
AgentContextHandle::File(handle) => {
Self::pending_file(handle, project.path_style(cx), cx)
}
AgentContextHandle::Directory(handle) => Self::pending_directory(handle, project, cx),
AgentContextHandle::Symbol(handle) => Self::pending_symbol(handle, cx),
AgentContextHandle::Selection(handle) => Self::pending_selection(handle, cx),
AgentContextHandle::Symbol(handle) => {
Self::pending_symbol(handle, project.path_style(cx), cx)
}
AgentContextHandle::Selection(handle) => {
Self::pending_selection(handle, project.path_style(cx), cx)
}
AgentContextHandle::FetchedUrl(handle) => Some(Self::fetched_url(handle)),
AgentContextHandle::Thread(handle) => Some(Self::pending_thread(handle, cx)),
AgentContextHandle::TextThread(handle) => Some(Self::pending_text_thread(handle, cx)),
AgentContextHandle::Rules(handle) => Self::pending_rules(handle, prompt_store, cx),
AgentContextHandle::Image(handle) => Some(Self::image(handle, model, cx)),
AgentContextHandle::Image(handle) => {
Some(Self::image(handle, model, project.path_style(cx), cx))
}
}
}
fn pending_file(handle: FileContextHandle, cx: &App) -> Option<AddedContext> {
let full_path = handle.buffer.read(cx).file()?.full_path(cx);
Some(Self::file(handle, &full_path, cx))
fn pending_file(
handle: FileContextHandle,
path_style: PathStyle,
cx: &App,
) -> Option<AddedContext> {
let full_path = handle
.buffer
.read(cx)
.file()?
.full_path(cx)
.to_string_lossy()
.to_string();
Some(Self::file(handle, &full_path, path_style, cx))
}
fn file(handle: FileContextHandle, full_path: &Path, cx: &App) -> AddedContext {
let full_path_string: SharedString = full_path.to_string_lossy().into_owned().into();
let (name, parent) =
extract_file_name_and_directory_from_full_path(full_path, &full_path_string);
fn file(
handle: FileContextHandle,
full_path: &str,
path_style: PathStyle,
cx: &App,
) -> AddedContext {
let (name, parent) = extract_file_name_and_directory_from_full_path(full_path, path_style);
AddedContext {
kind: ContextKind::File,
name,
parent,
tooltip: Some(full_path_string),
icon_path: FileIcons::get_icon(full_path, cx),
tooltip: Some(SharedString::new(full_path)),
icon_path: FileIcons::get_icon(Path::new(full_path), cx),
status: ContextStatus::Ready,
render_hover: None,
handle: AgentContextHandle::File(handle),
@@ -343,19 +365,24 @@ impl AddedContext {
) -> Option<AddedContext> {
let worktree = project.worktree_for_entry(handle.entry_id, cx)?.read(cx);
let entry = worktree.entry_for_id(handle.entry_id)?;
let full_path = worktree.full_path(&entry.path);
Some(Self::directory(handle, &full_path))
let full_path = worktree
.full_path(&entry.path)
.to_string_lossy()
.to_string();
Some(Self::directory(handle, &full_path, project.path_style(cx)))
}
fn directory(handle: DirectoryContextHandle, full_path: &Path) -> AddedContext {
let full_path_string: SharedString = full_path.to_string_lossy().into_owned().into();
let (name, parent) =
extract_file_name_and_directory_from_full_path(full_path, &full_path_string);
fn directory(
handle: DirectoryContextHandle,
full_path: &str,
path_style: PathStyle,
) -> AddedContext {
let (name, parent) = extract_file_name_and_directory_from_full_path(full_path, path_style);
AddedContext {
kind: ContextKind::Directory,
name,
parent,
tooltip: Some(full_path_string),
tooltip: Some(SharedString::new(full_path)),
icon_path: None,
status: ContextStatus::Ready,
render_hover: None,
@@ -363,9 +390,17 @@ impl AddedContext {
}
}
fn pending_symbol(handle: SymbolContextHandle, cx: &App) -> Option<AddedContext> {
let excerpt =
ContextFileExcerpt::new(&handle.full_path(cx)?, handle.enclosing_line_range(cx), cx);
fn pending_symbol(
handle: SymbolContextHandle,
path_style: PathStyle,
cx: &App,
) -> Option<AddedContext> {
let excerpt = ContextFileExcerpt::new(
&handle.full_path(cx)?.to_string_lossy(),
handle.enclosing_line_range(cx),
path_style,
cx,
);
Some(AddedContext {
kind: ContextKind::Symbol,
name: handle.symbol.clone(),
@@ -383,8 +418,17 @@ impl AddedContext {
})
}
fn pending_selection(handle: SelectionContextHandle, cx: &App) -> Option<AddedContext> {
let excerpt = ContextFileExcerpt::new(&handle.full_path(cx)?, handle.line_range(cx), cx);
fn pending_selection(
handle: SelectionContextHandle,
path_style: PathStyle,
cx: &App,
) -> Option<AddedContext> {
let excerpt = ContextFileExcerpt::new(
&handle.full_path(cx)?.to_string_lossy(),
handle.line_range(cx),
path_style,
cx,
);
Some(AddedContext {
kind: ContextKind::Selection,
name: excerpt.file_name_and_range.clone(),
@@ -485,13 +529,13 @@ impl AddedContext {
fn image(
context: ImageContext,
model: Option<&Arc<dyn language_model::LanguageModel>>,
path_style: PathStyle,
cx: &App,
) -> AddedContext {
let (name, parent, icon_path) = if let Some(full_path) = context.full_path.as_ref() {
let full_path_string: SharedString = full_path.to_string_lossy().into_owned().into();
let (name, parent) =
extract_file_name_and_directory_from_full_path(full_path, &full_path_string);
let icon_path = FileIcons::get_icon(full_path, cx);
extract_file_name_and_directory_from_full_path(full_path, path_style);
let icon_path = FileIcons::get_icon(Path::new(full_path), cx);
(name, parent, icon_path)
} else {
("Image".into(), None, None)
@@ -540,19 +584,20 @@ impl AddedContext {
}
fn extract_file_name_and_directory_from_full_path(
path: &Path,
name_fallback: &SharedString,
path: &str,
path_style: PathStyle,
) -> (SharedString, Option<SharedString>) {
let name = path
.file_name()
.map(|n| n.to_string_lossy().into_owned().into())
.unwrap_or_else(|| name_fallback.clone());
let parent = path
.parent()
.and_then(|p| p.file_name())
.map(|n| n.to_string_lossy().into_owned().into());
(name, parent)
let (parent, file_name) = path_style.split(path);
let parent = parent.and_then(|parent| {
let parent = parent.trim_end_matches(path_style.separator());
let (_, parent) = path_style.split(parent);
if parent.is_empty() {
None
} else {
Some(SharedString::new(parent))
}
});
(SharedString::new(file_name), parent)
}
#[derive(Debug, Clone)]
@@ -564,25 +609,25 @@ struct ContextFileExcerpt {
}
impl ContextFileExcerpt {
pub fn new(full_path: &Path, line_range: Range<Point>, cx: &App) -> Self {
let full_path_string = full_path.to_string_lossy().into_owned();
let file_name = full_path
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| full_path_string.clone());
pub fn new(full_path: &str, line_range: Range<Point>, path_style: PathStyle, cx: &App) -> Self {
let (parent, file_name) = path_style.split(full_path);
let line_range_text = format!(" ({}-{})", line_range.start.row + 1, line_range.end.row + 1);
let mut full_path_and_range = full_path_string;
let mut full_path_and_range = full_path.to_owned();
full_path_and_range.push_str(&line_range_text);
let mut file_name_and_range = file_name;
let mut file_name_and_range = file_name.to_owned();
file_name_and_range.push_str(&line_range_text);
let parent_name = full_path
.parent()
.and_then(|p| p.file_name())
.map(|n| n.to_string_lossy().into_owned().into());
let parent_name = parent.and_then(|parent| {
let parent = parent.trim_end_matches(path_style.separator());
let (_, parent) = path_style.split(parent);
if parent.is_empty() {
None
} else {
Some(SharedString::new(parent))
}
});
let icon_path = FileIcons::get_icon(full_path, cx);
let icon_path = FileIcons::get_icon(Path::new(full_path), cx);
ContextFileExcerpt {
file_name_and_range: file_name_and_range.into(),
@@ -690,6 +735,7 @@ impl Component for AddedContext {
image_task: Task::ready(Some(LanguageModelImage::empty())).shared(),
},
None,
PathStyle::local(),
cx,
),
);
@@ -710,6 +756,7 @@ impl Component for AddedContext {
.shared(),
},
None,
PathStyle::local(),
cx,
),
);
@@ -725,6 +772,7 @@ impl Component for AddedContext {
image_task: Task::ready(None).shared(),
},
None,
PathStyle::local(),
cx,
),
);
@@ -767,7 +815,8 @@ mod tests {
full_path: None,
};
let added_context = AddedContext::image(image_context, Some(&model), cx);
let added_context =
AddedContext::image(image_context, Some(&model), PathStyle::local(), cx);
assert!(matches!(
added_context.status,
@@ -790,7 +839,7 @@ mod tests {
full_path: None,
};
let added_context = AddedContext::image(image_context, None, cx);
let added_context = AddedContext::image(image_context, None, PathStyle::local(), cx);
assert!(
matches!(added_context.status, ContextStatus::Ready),

View File

@@ -18,7 +18,6 @@ default = []
client.workspace = true
cloud_llm_client.workspace = true
component.workspace = true
feature_flags.workspace = true
gpui.workspace = true
language_model.workspace = true
serde.workspace = true

View File

@@ -18,7 +18,6 @@ pub use young_account_banner::YoungAccountBanner;
use std::sync::Arc;
use client::{Client, UserStore, zed_urls};
use feature_flags::{BillingV2FeatureFlag, FeatureFlagAppExt as _};
use gpui::{AnyElement, Entity, IntoElement, ParentElement};
use ui::{Divider, RegisterComponent, Tooltip, prelude::*};
@@ -85,7 +84,7 @@ impl ZedAiOnboarding {
self
}
fn render_sign_in_disclaimer(&self, cx: &mut App) -> AnyElement {
fn render_sign_in_disclaimer(&self, _cx: &mut App) -> AnyElement {
let signing_in = matches!(self.sign_in_status, SignInStatus::SigningIn);
v_flex()
@@ -96,7 +95,7 @@ impl ZedAiOnboarding {
.color(Color::Muted)
.mb_2(),
)
.child(PlanDefinitions.pro_plan(cx.has_flag::<BillingV2FeatureFlag>(), false))
.child(PlanDefinitions.pro_plan(true, false))
.child(
Button::new("sign_in", "Try Zed Pro for Free")
.disabled(signing_in)
@@ -120,7 +119,7 @@ impl ZedAiOnboarding {
.max_w_full()
.gap_1()
.child(Headline::new("Welcome to Zed AI"))
.child(YoungAccountBanner::new(is_v2))
.child(YoungAccountBanner)
.child(
v_flex()
.mt_2()
@@ -307,7 +306,7 @@ impl RenderOnce for ZedAiOnboarding {
fn render(self, _window: &mut ui::Window, cx: &mut App) -> impl IntoElement {
if matches!(self.sign_in_status, SignInStatus::SignedIn) {
match self.plan {
None => self.render_free_plan_state(cx.has_flag::<BillingV2FeatureFlag>(), cx),
None => self.render_free_plan_state(true, cx),
Some(plan @ (Plan::V1(PlanV1::ZedFree) | Plan::V2(PlanV2::ZedFree))) => {
self.render_free_plan_state(plan.is_v2(), cx)
}

View File

@@ -2,7 +2,6 @@ use std::sync::Arc;
use client::{Client, UserStore, zed_urls};
use cloud_llm_client::{Plan, PlanV1, PlanV2};
use feature_flags::{BillingV2FeatureFlag, FeatureFlagAppExt};
use gpui::{AnyElement, App, Entity, IntoElement, RenderOnce, Window};
use ui::{CommonAnimationExt, Divider, Vector, VectorName, prelude::*};
@@ -50,9 +49,7 @@ impl AiUpsellCard {
impl RenderOnce for AiUpsellCard {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let is_v2_plan = self
.user_plan
.map_or(cx.has_flag::<BillingV2FeatureFlag>(), |plan| plan.is_v2());
let is_v2_plan = self.user_plan.map_or(true, |plan| plan.is_v2());
let pro_section = v_flex()
.flex_grow()
@@ -175,7 +172,7 @@ impl RenderOnce for AiUpsellCard {
.child(Label::new("Try Zed AI").size(LabelSize::Large))
.map(|this| {
if self.account_too_young {
this.child(YoungAccountBanner::new(is_v2_plan)).child(
this.child(YoungAccountBanner).child(
v_flex()
.mt_2()
.gap_1()

View File

@@ -2,30 +2,17 @@ use gpui::{IntoElement, ParentElement};
use ui::{Banner, prelude::*};
#[derive(IntoElement)]
pub struct YoungAccountBanner {
is_v2: bool,
}
impl YoungAccountBanner {
pub fn new(is_v2: bool) -> Self {
Self { is_v2 }
}
}
pub struct YoungAccountBanner;
impl RenderOnce for YoungAccountBanner {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
const YOUNG_ACCOUNT_DISCLAIMER: &str = "To prevent abuse of our service, GitHub accounts created fewer than 30 days ago are not eligible for free plan usage or Pro plan free trial. You can request an exception by reaching out to billing-support@zed.dev";
const YOUNG_ACCOUNT_DISCLAIMER_V2: &str = "To prevent abuse of our service, GitHub accounts created fewer than 30 days ago are not eligible for the Pro trial. You can request an exception by reaching out to billing-support@zed.dev";
const YOUNG_ACCOUNT_DISCLAIMER: &str = "To prevent abuse of our service, GitHub accounts created fewer than 30 days ago are not eligible for the Pro trial. You can request an exception by reaching out to billing-support@zed.dev";
let label = div()
.w_full()
.text_sm()
.text_color(cx.theme().colors().text_muted)
.child(if self.is_v2 {
YOUNG_ACCOUNT_DISCLAIMER_V2
} else {
YOUNG_ACCOUNT_DISCLAIMER
});
.child(YOUNG_ACCOUNT_DISCLAIMER);
div()
.max_w_full()

View File

@@ -63,12 +63,14 @@ impl TryFrom<&str> for EncryptedPassword {
if padded_length != len {
value.resize(padded_length as usize, 0);
}
unsafe {
CryptProtectMemory(
value.as_mut_ptr() as _,
len,
CRYPTPROTECTMEMORY_SAME_PROCESS,
)?;
if len != 0 {
unsafe {
CryptProtectMemory(
value.as_mut_ptr() as _,
len,
CRYPTPROTECTMEMORY_SAME_PROCESS,
)?;
}
}
Ok(Self(value, len))
}
@@ -91,19 +93,22 @@ pub(crate) fn decrypt(mut password: EncryptedPassword) -> Result<String> {
password.0.len(),
CRYPTPROTECTMEMORY_BLOCK_SIZE
);
unsafe {
CryptUnprotectMemory(
password.0.as_mut_ptr() as _,
password.1,
CRYPTPROTECTMEMORY_SAME_PROCESS,
)
.context("while decrypting a SSH password")?
};
if password.1 != 0 {
unsafe {
CryptUnprotectMemory(
password.0.as_mut_ptr() as _,
password.1,
CRYPTPROTECTMEMORY_SAME_PROCESS,
)
.context("while decrypting a SSH password")?
};
{
// Remove padding
_ = password.0.drain(password.1 as usize..);
{
// Remove padding
_ = password.0.drain(password.1 as usize..);
}
}
Ok(String::from_utf8(std::mem::take(&mut password.0))?)
}
#[cfg(not(windows))]

View File

@@ -2669,7 +2669,7 @@ impl AssistantContext {
}
pub fn summarize(&mut self, mut replace_old: bool, cx: &mut Context<Self>) {
let Some(model) = LanguageModelRegistry::read_global(cx).default_model() else {
let Some(model) = LanguageModelRegistry::read_global(cx).thread_summary_model() else {
return;
};

View File

@@ -1329,13 +1329,12 @@ fn setup_context_editor_with_fake_model(
cx.update(|cx| {
init_test(cx);
LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
registry.set_default_model(
Some(ConfiguredModel {
provider: fake_provider.clone(),
model: fake_model.clone(),
}),
cx,
)
let configured_model = ConfiguredModel {
provider: fake_provider.clone(),
model: fake_model.clone(),
};
registry.set_default_model(Some(configured_model.clone()), cx);
registry.set_thread_summary_model(Some(configured_model), cx);
})
});

View File

@@ -25,6 +25,7 @@ parking_lot.workspace = true
serde.workspace = true
serde_json.workspace = true
ui.workspace = true
util.workspace = true
workspace.workspace = true
workspace-hack.workspace = true

View File

@@ -1,12 +1,11 @@
use std::path::PathBuf;
use std::sync::{Arc, atomic::AtomicBool};
use anyhow::Result;
use async_trait::async_trait;
use extension::{Extension, ExtensionHostProxy, ExtensionSlashCommandProxy, WorktreeDelegate};
use gpui::{App, Task, WeakEntity, Window};
use language::{BufferSnapshot, LspAdapterDelegate};
use std::sync::{Arc, atomic::AtomicBool};
use ui::prelude::*;
use util::rel_path::RelPath;
use workspace::Workspace;
use crate::{
@@ -51,10 +50,10 @@ impl WorktreeDelegate for WorktreeDelegateAdapter {
}
fn root_path(&self) -> String {
self.0.worktree_root_path().to_string_lossy().to_string()
self.0.worktree_root_path().to_string_lossy().into_owned()
}
async fn read_text_file(&self, path: PathBuf) -> Result<String> {
async fn read_text_file(&self, path: &RelPath) -> Result<String> {
self.0.read_text_file(path).await
}
@@ -62,7 +61,7 @@ impl WorktreeDelegate for WorktreeDelegateAdapter {
self.0
.which(binary_name.as_ref())
.await
.map(|path| path.to_string_lossy().to_string())
.map(|path| path.to_string_lossy().into_owned())
}
async fn shell_env(&self) -> Vec<(String, String)> {

View File

@@ -41,6 +41,9 @@ worktree.workspace = true
workspace-hack.workspace = true
[dev-dependencies]
fs = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, features = ["test-support"] }
pretty_assertions.workspace = true
settings.workspace = true
project = { workspace = true, features = ["test-support"] }
settings = { workspace = true, features = ["test-support"] }
zlog.workspace = true

View File

@@ -0,0 +1,159 @@
use anyhow::{Context as _, Result, anyhow};
use assistant_slash_command::{
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
SlashCommandResult,
};
use fs::Fs;
use gpui::{App, Entity, Task, WeakEntity};
use language::{BufferSnapshot, LspAdapterDelegate};
use project::{Project, ProjectPath};
use std::{
fmt::Write,
path::Path,
sync::{Arc, atomic::AtomicBool},
};
use ui::prelude::*;
use util::rel_path::RelPath;
use workspace::Workspace;
pub struct CargoWorkspaceSlashCommand;
impl CargoWorkspaceSlashCommand {
async fn build_message(fs: Arc<dyn Fs>, path_to_cargo_toml: &Path) -> Result<String> {
let buffer = fs.load(path_to_cargo_toml).await?;
let cargo_toml: cargo_toml::Manifest = toml::from_str(&buffer)?;
let mut message = String::new();
writeln!(message, "You are in a Rust project.")?;
if let Some(workspace) = cargo_toml.workspace {
writeln!(
message,
"The project is a Cargo workspace with the following members:"
)?;
for member in workspace.members {
writeln!(message, "- {member}")?;
}
if !workspace.default_members.is_empty() {
writeln!(message, "The default members are:")?;
for member in workspace.default_members {
writeln!(message, "- {member}")?;
}
}
if !workspace.dependencies.is_empty() {
writeln!(
message,
"The following workspace dependencies are installed:"
)?;
for dependency in workspace.dependencies.keys() {
writeln!(message, "- {dependency}")?;
}
}
} else if let Some(package) = cargo_toml.package {
writeln!(
message,
"The project name is \"{name}\".",
name = package.name
)?;
let description = package
.description
.as_ref()
.and_then(|description| description.get().ok().cloned());
if let Some(description) = description.as_ref() {
writeln!(message, "It describes itself as \"{description}\".")?;
}
if !cargo_toml.dependencies.is_empty() {
writeln!(message, "The following dependencies are installed:")?;
for dependency in cargo_toml.dependencies.keys() {
writeln!(message, "- {dependency}")?;
}
}
}
Ok(message)
}
fn path_to_cargo_toml(project: Entity<Project>, cx: &mut App) -> Option<Arc<Path>> {
let worktree = project.read(cx).worktrees(cx).next()?;
let worktree = worktree.read(cx);
let entry = worktree.entry_for_path(RelPath::new("Cargo.toml").unwrap())?;
let path = ProjectPath {
worktree_id: worktree.id(),
path: entry.path.clone(),
};
Some(Arc::from(
project.read(cx).absolute_path(&path, cx)?.as_path(),
))
}
}
impl SlashCommand for CargoWorkspaceSlashCommand {
fn name(&self) -> String {
"cargo-workspace".into()
}
fn description(&self) -> String {
"insert project workspace metadata".into()
}
fn menu_text(&self) -> String {
"Insert Project Workspace Metadata".into()
}
fn complete_argument(
self: Arc<Self>,
_arguments: &[String],
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakEntity<Workspace>>,
_window: &mut Window,
_cx: &mut App,
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Err(anyhow!("this command does not require argument")))
}
fn requires_argument(&self) -> bool {
false
}
fn run(
self: Arc<Self>,
_arguments: &[String],
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
_context_buffer: BufferSnapshot,
workspace: WeakEntity<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
_window: &mut Window,
cx: &mut App,
) -> Task<SlashCommandResult> {
let output = workspace.update(cx, |workspace, cx| {
let project = workspace.project().clone();
let fs = workspace.project().read(cx).fs().clone();
let path = Self::path_to_cargo_toml(project, cx);
let output = cx.background_spawn(async move {
let path = path.with_context(|| "Cargo.toml not found")?;
Self::build_message(fs, &path).await
});
cx.foreground_executor().spawn(async move {
let text = output.await?;
let range = 0..text.len();
Ok(SlashCommandOutput {
text,
sections: vec![SlashCommandOutputSection {
range,
icon: IconName::FileTree,
label: "Project".into(),
metadata: None,
}],
run_commands_in_text: false,
}
.into_event_stream())
})
});
output.unwrap_or_else(|error| Task::ready(Err(error)))
}
}

View File

@@ -13,12 +13,12 @@ use project::{DiagnosticSummary, PathMatchCandidateSet, Project};
use rope::Point;
use std::{
fmt::Write,
path::{Path, PathBuf},
path::Path,
sync::{Arc, atomic::AtomicBool},
};
use ui::prelude::*;
use util::ResultExt;
use util::paths::PathMatcher;
use util::paths::{PathMatcher, PathStyle};
use util::{ResultExt, rel_path::RelPath};
use workspace::Workspace;
use crate::create_label_for_command;
@@ -36,7 +36,7 @@ impl DiagnosticsSlashCommand {
if query.is_empty() {
let workspace = workspace.read(cx);
let entries = workspace.recent_navigation_history(Some(10), cx);
let path_prefix: Arc<str> = Arc::default();
let path_prefix: Arc<RelPath> = RelPath::empty().into();
Task::ready(
entries
.into_iter()
@@ -125,6 +125,7 @@ impl SlashCommand for DiagnosticsSlashCommand {
let Some(workspace) = workspace.and_then(|workspace| workspace.upgrade()) else {
return Task::ready(Err(anyhow!("workspace was dropped")));
};
let path_style = workspace.read(cx).project().read(cx).path_style(cx);
let query = arguments.last().cloned().unwrap_or_default();
let paths = self.search_paths(query.clone(), cancellation_flag.clone(), &workspace, cx);
@@ -134,11 +135,11 @@ impl SlashCommand for DiagnosticsSlashCommand {
.await
.into_iter()
.map(|path_match| {
format!(
"{}{}",
path_match.path_prefix,
path_match.path.to_string_lossy()
)
path_match
.path_prefix
.join(&path_match.path)
.display(path_style)
.to_string()
})
.collect();
@@ -183,9 +184,11 @@ impl SlashCommand for DiagnosticsSlashCommand {
return Task::ready(Err(anyhow!("workspace was dropped")));
};
let options = Options::parse(arguments);
let project = workspace.read(cx).project();
let path_style = project.read(cx).path_style(cx);
let options = Options::parse(arguments, path_style);
let task = collect_diagnostics(workspace.read(cx).project().clone(), options, cx);
let task = collect_diagnostics(project.clone(), options, cx);
window.spawn(cx, async move |_| {
task.await?
@@ -204,14 +207,14 @@ struct Options {
const INCLUDE_WARNINGS_ARGUMENT: &str = "--include-warnings";
impl Options {
fn parse(arguments: &[String]) -> Self {
fn parse(arguments: &[String], path_style: PathStyle) -> Self {
let mut include_warnings = false;
let mut path_matcher = None;
for arg in arguments {
if arg == INCLUDE_WARNINGS_ARGUMENT {
include_warnings = true;
} else {
path_matcher = PathMatcher::new(&[arg.to_owned()]).log_err();
path_matcher = PathMatcher::new(&[arg.to_owned()], path_style).log_err();
}
}
Self {
@@ -237,21 +240,15 @@ fn collect_diagnostics(
None
};
let path_style = project.read(cx).path_style(cx);
let glob_is_exact_file_match = if let Some(path) = options
.path_matcher
.as_ref()
.and_then(|pm| pm.sources().first())
{
PathBuf::try_from(path)
.ok()
.and_then(|path| {
project.read(cx).worktrees(cx).find_map(|worktree| {
let worktree = worktree.read(cx);
let worktree_root_path = Path::new(worktree.root_name());
let relative_path = path.strip_prefix(worktree_root_path).ok()?;
worktree.absolutize(relative_path).ok()
})
})
project
.read(cx)
.find_project_path(Path::new(path), cx)
.is_some()
} else {
false
@@ -263,9 +260,8 @@ fn collect_diagnostics(
.diagnostic_summaries(false, cx)
.flat_map(|(path, _, summary)| {
let worktree = project.read(cx).worktree_for_id(path.worktree_id, cx)?;
let mut path_buf = PathBuf::from(worktree.read(cx).root_name());
path_buf.push(&path.path);
Some((path, path_buf, summary))
let full_path = worktree.read(cx).root_name().join(&path.path);
Some((path, full_path, summary))
})
.collect();
@@ -281,7 +277,7 @@ fn collect_diagnostics(
let mut project_summary = DiagnosticSummary::default();
for (project_path, path, summary) in diagnostic_summaries {
if let Some(path_matcher) = &options.path_matcher
&& !path_matcher.is_match(&path)
&& !path_matcher.is_match(&path.as_std_path())
{
continue;
}
@@ -294,7 +290,7 @@ fn collect_diagnostics(
}
let last_end = output.text.len();
let file_path = path.to_string_lossy().to_string();
let file_path = path.display(path_style).to_string();
if !glob_is_exact_file_match {
writeln!(&mut output.text, "{file_path}").unwrap();
}

View File

@@ -14,11 +14,11 @@ use smol::stream::StreamExt;
use std::{
fmt::Write,
ops::{Range, RangeInclusive},
path::{Path, PathBuf},
path::Path,
sync::{Arc, atomic::AtomicBool},
};
use ui::prelude::*;
use util::ResultExt;
use util::{ResultExt, rel_path::RelPath};
use workspace::Workspace;
use worktree::ChildEntriesOptions;
@@ -48,7 +48,7 @@ impl FileSlashCommand {
include_dirs: true,
include_ignored: false,
};
let entries = worktree.child_entries_with_options(Path::new(""), options);
let entries = worktree.child_entries_with_options(RelPath::empty(), options);
entries.map(move |entry| {
(
project::ProjectPath {
@@ -61,19 +61,18 @@ impl FileSlashCommand {
}))
.collect::<Vec<_>>();
let path_prefix: Arc<str> = Arc::default();
let path_prefix: Arc<RelPath> = RelPath::empty().into();
Task::ready(
entries
.into_iter()
.filter_map(|(entry, is_dir)| {
let worktree = project.worktree_for_id(entry.worktree_id, cx)?;
let mut full_path = PathBuf::from(worktree.read(cx).root_name());
full_path.push(&entry.path);
let full_path = worktree.read(cx).root_name().join(&entry.path);
Some(PathMatch {
score: 0.,
positions: Vec::new(),
worktree_id: entry.worktree_id.to_usize(),
path: full_path.into(),
path: full_path,
path_prefix: path_prefix.clone(),
distance_to_relative_ancestor: 0,
is_dir,
@@ -149,6 +148,8 @@ impl SlashCommand for FileSlashCommand {
return Task::ready(Err(anyhow!("workspace was dropped")));
};
let path_style = workspace.read(cx).path_style(cx);
let paths = self.search_paths(
arguments.last().cloned().unwrap_or_default(),
cancellation_flag,
@@ -161,14 +162,14 @@ impl SlashCommand for FileSlashCommand {
.await
.into_iter()
.filter_map(|path_match| {
let text = format!(
"{}{}",
path_match.path_prefix,
path_match.path.to_string_lossy()
);
let text = path_match
.path_prefix
.join(&path_match.path)
.display(path_style)
.to_string();
let mut label = CodeLabel::default();
let file_name = path_match.path.file_name()?.to_string_lossy();
let file_name = path_match.path.file_name()?;
let label_text = if path_match.is_dir {
format!("{}/ ", file_name)
} else {
@@ -247,14 +248,13 @@ fn collect_files(
cx.spawn(async move |cx| {
for snapshot in snapshots {
let worktree_id = snapshot.id();
let mut directory_stack: Vec<Arc<Path>> = Vec::new();
let mut folded_directory_names_stack = Vec::new();
let path_style = snapshot.path_style();
let mut directory_stack: Vec<Arc<RelPath>> = Vec::new();
let mut folded_directory_names: Arc<RelPath> = RelPath::empty().into();
let mut is_top_level_directory = true;
for entry in snapshot.entries(false, 0) {
let mut path_including_worktree_name = PathBuf::new();
path_including_worktree_name.push(snapshot.root_name());
path_including_worktree_name.push(&entry.path);
let path_including_worktree_name = snapshot.root_name().join(&entry.path);
if !matchers
.iter()
@@ -277,13 +277,7 @@ fn collect_files(
)))?;
}
let filename = entry
.path
.file_name()
.unwrap_or_default()
.to_str()
.unwrap_or_default()
.to_string();
let filename = entry.path.file_name().unwrap_or_default().to_string();
if entry.is_dir() {
// Auto-fold directories that contain no files
@@ -292,24 +286,23 @@ fn collect_files(
if child_entries.next().is_none() && child.kind.is_dir() {
if is_top_level_directory {
is_top_level_directory = false;
folded_directory_names_stack.push(
path_including_worktree_name.to_string_lossy().to_string(),
);
folded_directory_names =
folded_directory_names.join(&path_including_worktree_name);
} else {
folded_directory_names_stack.push(filename.to_string());
folded_directory_names =
folded_directory_names.join(RelPath::unix(&filename).unwrap());
}
continue;
}
} else {
// Skip empty directories
folded_directory_names_stack.clear();
folded_directory_names = RelPath::empty().into();
continue;
}
let prefix_paths = folded_directory_names_stack.drain(..).as_slice().join("/");
if prefix_paths.is_empty() {
if folded_directory_names.is_empty() {
let label = if is_top_level_directory {
is_top_level_directory = false;
path_including_worktree_name.to_string_lossy().to_string()
path_including_worktree_name.display(path_style).to_string()
} else {
filename
};
@@ -320,28 +313,23 @@ fn collect_files(
}))?;
events_tx.unbounded_send(Ok(SlashCommandEvent::Content(
SlashCommandContent::Text {
text: label,
text: label.to_string(),
run_commands_in_text: false,
},
)))?;
directory_stack.push(entry.path.clone());
} else {
// todo(windows)
// Potential bug: this assumes that the path separator is always `\` on Windows
let entry_name = format!(
"{}{}{}",
prefix_paths,
std::path::MAIN_SEPARATOR_STR,
&filename
);
let entry_name =
folded_directory_names.join(RelPath::unix(&filename).unwrap());
let entry_name = entry_name.display(path_style);
events_tx.unbounded_send(Ok(SlashCommandEvent::StartSection {
icon: IconName::Folder,
label: entry_name.clone().into(),
label: entry_name.to_string().into(),
metadata: None,
}))?;
events_tx.unbounded_send(Ok(SlashCommandEvent::Content(
SlashCommandContent::Text {
text: entry_name,
text: entry_name.to_string(),
run_commands_in_text: false,
},
)))?;
@@ -356,7 +344,7 @@ fn collect_files(
} else if entry.is_file() {
let Some(open_buffer_task) = project_handle
.update(cx, |project, cx| {
project.open_buffer((worktree_id, &entry.path), cx)
project.open_buffer((worktree_id, entry.path.clone()), cx)
})
.ok()
else {
@@ -367,7 +355,7 @@ fn collect_files(
let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot())?;
append_buffer_to_output(
&snapshot,
Some(&path_including_worktree_name),
Some(path_including_worktree_name.display(path_style).as_ref()),
&mut output,
)
.log_err();
@@ -392,18 +380,18 @@ fn collect_files(
}
pub fn codeblock_fence_for_path(
path: Option<&Path>,
path: Option<&str>,
row_range: Option<RangeInclusive<u32>>,
) -> String {
let mut text = String::new();
write!(text, "```").unwrap();
if let Some(path) = path {
if let Some(extension) = path.extension().and_then(|ext| ext.to_str()) {
if let Some(extension) = Path::new(path).extension().and_then(|ext| ext.to_str()) {
write!(text, "{} ", extension).unwrap();
}
write!(text, "{}", path.display()).unwrap();
write!(text, "{path}").unwrap();
} else {
write!(text, "untitled").unwrap();
}
@@ -423,12 +411,12 @@ pub struct FileCommandMetadata {
pub fn build_entry_output_section(
range: Range<usize>,
path: Option<&Path>,
path: Option<&str>,
is_directory: bool,
line_range: Option<Range<u32>>,
) -> SlashCommandOutputSection<usize> {
let mut label = if let Some(path) = path {
path.to_string_lossy().to_string()
path.to_string()
} else {
"untitled".to_string()
};
@@ -451,7 +439,7 @@ pub fn build_entry_output_section(
} else {
path.and_then(|path| {
serde_json::to_value(FileCommandMetadata {
path: path.to_string_lossy().to_string(),
path: path.to_string(),
})
.ok()
})
@@ -462,10 +450,9 @@ pub fn build_entry_output_section(
/// This contains a small fork of the util::paths::PathMatcher, that is stricter about the prefix
/// check. Only subpaths pass the prefix check, rather than any prefix.
mod custom_path_matcher {
use std::{fmt::Debug as _, path::Path};
use globset::{Glob, GlobSet, GlobSetBuilder};
use util::paths::SanitizedPath;
use std::fmt::Debug as _;
use util::{paths::SanitizedPath, rel_path::RelPath};
#[derive(Clone, Debug, Default)]
pub struct PathMatcher {
@@ -492,12 +479,12 @@ mod custom_path_matcher {
pub fn new(globs: &[String]) -> Result<Self, globset::Error> {
let globs = globs
.iter()
.map(|glob| Glob::new(&SanitizedPath::new(glob).to_glob_string()))
.map(|glob| Glob::new(&SanitizedPath::new(glob).to_string()))
.collect::<Result<Vec<_>, _>>()?;
let sources = globs.iter().map(|glob| glob.glob().to_owned()).collect();
let sources_with_trailing_slash = globs
.iter()
.map(|glob| glob.glob().to_string() + std::path::MAIN_SEPARATOR_STR)
.map(|glob| glob.glob().to_string() + "/")
.collect();
let mut glob_builder = GlobSetBuilder::new();
for single_glob in globs {
@@ -511,16 +498,13 @@ mod custom_path_matcher {
})
}
pub fn is_match<P: AsRef<Path>>(&self, other: P) -> bool {
let other_path = other.as_ref();
pub fn is_match(&self, other: &RelPath) -> bool {
self.sources
.iter()
.zip(self.sources_with_trailing_slash.iter())
.any(|(source, with_slash)| {
let as_bytes = other_path.as_os_str().as_encoded_bytes();
// todo(windows)
// Potential bug: this assumes that the path separator is always `\` on Windows
let with_slash = if source.ends_with(std::path::MAIN_SEPARATOR_STR) {
let as_bytes = other.as_unix_str().as_bytes();
let with_slash = if source.ends_with('/') {
source.as_bytes()
} else {
with_slash.as_bytes()
@@ -528,13 +512,13 @@ mod custom_path_matcher {
as_bytes.starts_with(with_slash) || as_bytes.ends_with(source.as_bytes())
})
|| self.glob.is_match(other_path)
|| self.check_with_end_separator(other_path)
|| self.glob.is_match(other.as_std_path())
|| self.check_with_end_separator(other)
}
fn check_with_end_separator(&self, path: &Path) -> bool {
let path_str = path.to_string_lossy();
let separator = std::path::MAIN_SEPARATOR_STR;
fn check_with_end_separator(&self, path: &RelPath) -> bool {
let path_str = path.as_unix_str();
let separator = "/";
if path_str.ends_with(separator) {
false
} else {
@@ -546,7 +530,7 @@ mod custom_path_matcher {
pub fn append_buffer_to_output(
buffer: &BufferSnapshot,
path: Option<&Path>,
path: Option<&str>,
output: &mut SlashCommandOutput,
) -> Result<()> {
let prev_len = output.text.len();

View File

@@ -137,7 +137,9 @@ pub fn selections_creases(
None
};
let language_name = language_name.as_deref().unwrap_or("");
let filename = snapshot.file_at(range.start).map(|file| file.full_path(cx));
let filename = snapshot
.file_at(range.start)
.map(|file| file.full_path(cx).to_string_lossy().into_owned());
let text = if language_name == "markdown" {
selected_text
.lines()
@@ -187,9 +189,9 @@ pub fn selections_creases(
let start_line = range.start.row + 1;
let end_line = range.end.row + 1;
if start_line == end_line {
format!("{}, Line {}", path.display(), start_line)
format!("{path}, Line {start_line}")
} else {
format!("{}, Lines {} to {}", path.display(), start_line, end_line)
format!("{path}, Lines {start_line} to {end_line}")
}
} else {
"Quoted selection".to_string()

View File

@@ -7,8 +7,8 @@ use editor::Editor;
use gpui::{AppContext as _, Task, WeakEntity};
use language::{BufferSnapshot, LspAdapterDelegate};
use std::sync::Arc;
use std::{path::Path, sync::atomic::AtomicBool};
use ui::{App, IconName, Window};
use std::sync::atomic::AtomicBool;
use ui::{App, IconName, SharedString, Window};
use workspace::Workspace;
pub struct OutlineSlashCommand;
@@ -67,13 +67,13 @@ impl SlashCommand for OutlineSlashCommand {
};
let snapshot = buffer.read(cx).snapshot();
let path = snapshot.resolve_file_path(cx, true);
let path = snapshot.resolve_file_path(true, cx);
cx.background_spawn(async move {
let outline = snapshot.outline(None);
let path = path.as_deref().unwrap_or(Path::new("untitled"));
let mut outline_text = format!("Symbols for {}:\n", path.display());
let path = path.as_deref().unwrap_or("untitled");
let mut outline_text = format!("Symbols for {path}:\n");
for item in &outline.path_candidates {
outline_text.push_str("- ");
outline_text.push_str(&item.string);
@@ -84,7 +84,7 @@ impl SlashCommand for OutlineSlashCommand {
sections: vec![SlashCommandOutputSection {
range: 0..outline_text.len(),
icon: IconName::ListTree,
label: path.to_string_lossy().to_string().into(),
label: SharedString::new(path),
metadata: None,
}],
text: outline_text,

View File

@@ -8,12 +8,9 @@ use editor::Editor;
use futures::future::join_all;
use gpui::{Task, WeakEntity};
use language::{BufferSnapshot, CodeLabel, HighlightId, LspAdapterDelegate};
use std::{
path::PathBuf,
sync::{Arc, atomic::AtomicBool},
};
use std::sync::{Arc, atomic::AtomicBool};
use ui::{ActiveTheme, App, Window, prelude::*};
use util::ResultExt;
use util::{ResultExt, paths::PathStyle};
use workspace::Workspace;
use crate::file_command::append_buffer_to_output;
@@ -72,35 +69,42 @@ impl SlashCommand for TabSlashCommand {
return Task::ready(Ok(Vec::new()));
}
let active_item_path = workspace.as_ref().and_then(|workspace| {
workspace
.update(cx, |workspace, cx| {
let snapshot = active_item_buffer(workspace, cx).ok()?;
snapshot.resolve_file_path(cx, true)
})
.ok()
.flatten()
let Some(workspace) = workspace.and_then(|workspace| workspace.upgrade()) else {
return Task::ready(Err(anyhow::anyhow!("no workspace")));
};
let active_item_path = workspace.update(cx, |workspace, cx| {
let snapshot = active_item_buffer(workspace, cx).ok()?;
snapshot.resolve_file_path(true, cx)
});
let path_style = workspace.read(cx).path_style(cx);
let current_query = arguments.last().cloned().unwrap_or_default();
let tab_items_search =
tab_items_for_queries(workspace, &[current_query], cancel, false, window, cx);
let tab_items_search = tab_items_for_queries(
Some(workspace.downgrade()),
&[current_query],
cancel,
false,
window,
cx,
);
let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
window.spawn(cx, async move |_| {
let tab_items = tab_items_search.await?;
let run_command = tab_items.len() == 1;
let tab_completion_items = tab_items.into_iter().filter_map(|(path, ..)| {
let path_string = path.as_deref()?.to_string_lossy().to_string();
if argument_set.contains(&path_string) {
let path = path?;
if argument_set.contains(&path) {
return None;
}
if active_item_path.is_some() && active_item_path == path {
if active_item_path.as_ref() == Some(&path) {
return None;
}
let label = create_tab_completion_label(path.as_ref()?, comment_id);
let label = create_tab_completion_label(&path, path_style, comment_id);
Some(ArgumentCompletion {
label,
new_text: path_string,
new_text: path,
replace_previous_arguments: false,
after_completion: run_command.into(),
})
@@ -109,8 +113,9 @@ impl SlashCommand for TabSlashCommand {
let active_item_completion = active_item_path
.as_deref()
.map(|active_item_path| {
let path_string = active_item_path.to_string_lossy().to_string();
let label = create_tab_completion_label(active_item_path, comment_id);
let path_string = active_item_path.to_string();
let label =
create_tab_completion_label(active_item_path, path_style, comment_id);
ArgumentCompletion {
label,
new_text: path_string,
@@ -169,7 +174,7 @@ fn tab_items_for_queries(
strict_match: bool,
window: &mut Window,
cx: &mut App,
) -> Task<anyhow::Result<Vec<(Option<PathBuf>, BufferSnapshot, usize)>>> {
) -> Task<anyhow::Result<Vec<(Option<String>, BufferSnapshot, usize)>>> {
let empty_query = queries.is_empty() || queries.iter().all(|query| query.trim().is_empty());
let queries = queries.to_owned();
window.spawn(cx, async move |cx| {
@@ -179,7 +184,7 @@ fn tab_items_for_queries(
.update(cx, |workspace, cx| {
if strict_match && empty_query {
let snapshot = active_item_buffer(workspace, cx)?;
let full_path = snapshot.resolve_file_path(cx, true);
let full_path = snapshot.resolve_file_path(true, cx);
return anyhow::Ok(vec![(full_path, snapshot, 0)]);
}
@@ -201,7 +206,7 @@ fn tab_items_for_queries(
&& visited_buffers.insert(buffer.read(cx).remote_id())
{
let snapshot = buffer.read(cx).snapshot();
let full_path = snapshot.resolve_file_path(cx, true);
let full_path = snapshot.resolve_file_path(true, cx);
open_buffers.push((full_path, snapshot, *timestamp));
}
}
@@ -224,10 +229,7 @@ fn tab_items_for_queries(
let match_candidates = open_buffers
.iter()
.enumerate()
.filter_map(|(id, (full_path, ..))| {
let path_string = full_path.as_deref()?.to_string_lossy().to_string();
Some((id, path_string))
})
.filter_map(|(id, (full_path, ..))| Some((id, full_path.clone()?)))
.fold(HashMap::default(), |mut candidates, (id, path_string)| {
candidates
.entry(path_string)
@@ -249,8 +251,7 @@ fn tab_items_for_queries(
.iter()
.enumerate()
.filter_map(|(id, (full_path, ..))| {
let path_string = full_path.as_deref()?.to_string_lossy().to_string();
Some(fuzzy::StringMatchCandidate::new(id, &path_string))
Some(fuzzy::StringMatchCandidate::new(id, full_path.as_ref()?))
})
.collect::<Vec<_>>();
let mut processed_matches = HashSet::default();
@@ -302,21 +303,15 @@ fn active_item_buffer(
}
fn create_tab_completion_label(
path: &std::path::Path,
path: &str,
path_style: PathStyle,
comment_id: Option<HighlightId>,
) -> CodeLabel {
let file_name = path
.file_name()
.map(|f| f.to_string_lossy())
.unwrap_or_default();
let parent_path = path
.parent()
.map(|p| p.to_string_lossy())
.unwrap_or_default();
let (parent_path, file_name) = path_style.split(path);
let mut label = CodeLabel::default();
label.push_str(&file_name, None);
label.push_str(file_name, None);
label.push_str(" ", None);
label.push_str(&parent_path, comment_id);
label.push_str(parent_path.unwrap_or_default(), comment_id);
label.filter_range = 0..file_name.len();
label
}

View File

@@ -5,7 +5,6 @@ use language::{Buffer, OutlineItem, ParseStatus};
use project::Project;
use regex::Regex;
use std::fmt::Write;
use std::path::Path;
use text::Point;
/// For files over this size, instead of reading them (or including them in context),
@@ -143,7 +142,7 @@ pub struct BufferContent {
/// For smaller files, returns the full content.
pub async fn get_buffer_content_or_outline(
buffer: Entity<Buffer>,
path: Option<&Path>,
path: Option<&str>,
cx: &AsyncApp,
) -> Result<BufferContent> {
let file_size = buffer.read_with(cx, |buffer, _| buffer.text().len())?;
@@ -170,15 +169,10 @@ pub async fn get_buffer_content_or_outline(
let text = if let Some(path) = path {
format!(
"# File outline for {} (file too large to show full content)\n\n{}",
path.display(),
outline_text
"# File outline for {path} (file too large to show full content)\n\n{outline_text}",
)
} else {
format!(
"# File outline (file too large to show full content)\n\n{}",
outline_text
)
format!("# File outline (file too large to show full content)\n\n{outline_text}",)
};
Ok(BufferContent {
text,

View File

@@ -96,9 +96,7 @@ impl Tool for CopyPathTool {
.and_then(|project_path| project.entry_for_path(&project_path, cx))
{
Some(entity) => match project.find_project_path(&input.destination_path, cx) {
Some(project_path) => {
project.copy_entry(entity.id, None, project_path.path, cx)
}
Some(project_path) => project.copy_entry(entity.id, project_path, cx),
None => Task::ready(Err(anyhow!(
"Destination path {} was outside the project.",
input.destination_path

View File

@@ -8,7 +8,7 @@ use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchem
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{fmt::Write, path::Path, sync::Arc};
use std::{fmt::Write, sync::Arc};
use ui::IconName;
use util::markdown::MarkdownInlineCode;
@@ -150,9 +150,7 @@ impl Tool for DiagnosticsTool {
has_diagnostics = true;
output.push_str(&format!(
"{}: {} error(s), {} warning(s)\n",
Path::new(worktree.read(cx).root_name())
.join(project_path.path)
.display(),
worktree.read(cx).absolutize(&project_path.path).display(),
summary.error_count,
summary.warning_count
));

View File

@@ -26,13 +26,13 @@ use language_model::{
use project::{AgentLocation, Project};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{cmp, iter, mem, ops::Range, path::PathBuf, pin::Pin, sync::Arc, task::Poll};
use std::{cmp, iter, mem, ops::Range, pin::Pin, sync::Arc, task::Poll};
use streaming_diff::{CharOperation, StreamingDiff};
use streaming_fuzzy_matcher::StreamingFuzzyMatcher;
#[derive(Serialize)]
struct CreateFilePromptTemplate {
path: Option<PathBuf>,
path: Option<String>,
edit_description: String,
}
@@ -42,7 +42,7 @@ impl Template for CreateFilePromptTemplate {
#[derive(Serialize)]
struct EditFileXmlPromptTemplate {
path: Option<PathBuf>,
path: Option<String>,
edit_description: String,
}
@@ -52,7 +52,7 @@ impl Template for EditFileXmlPromptTemplate {
#[derive(Serialize)]
struct EditFileDiffFencedPromptTemplate {
path: Option<PathBuf>,
path: Option<String>,
edit_description: String,
}
@@ -115,7 +115,7 @@ impl EditAgent {
let conversation = conversation.clone();
let output = cx.spawn(async move |cx| {
let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot())?;
let path = cx.update(|cx| snapshot.resolve_file_path(cx, true))?;
let path = cx.update(|cx| snapshot.resolve_file_path(true, cx))?;
let prompt = CreateFilePromptTemplate {
path,
edit_description,
@@ -229,7 +229,7 @@ impl EditAgent {
let edit_format = self.edit_format;
let output = cx.spawn(async move |cx| {
let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot())?;
let path = cx.update(|cx| snapshot.resolve_file_path(cx, true))?;
let path = cx.update(|cx| snapshot.resolve_file_path(true, cx))?;
let prompt = match edit_format {
EditFormat::XmlTags => EditFileXmlPromptTemplate {
path,

View File

@@ -38,6 +38,7 @@ use settings::Settings;
use std::{
cmp::Reverse,
collections::HashSet,
ffi::OsStr,
ops::Range,
path::{Path, PathBuf},
sync::Arc,
@@ -45,7 +46,7 @@ use std::{
};
use theme::ThemeSettings;
use ui::{CommonAnimationExt, Disclosure, Tooltip, prelude::*};
use util::ResultExt;
use util::{ResultExt, rel_path::RelPath};
use workspace::Workspace;
pub struct EditFileTool;
@@ -146,11 +147,11 @@ impl Tool for EditFileTool {
// If any path component matches the local settings folder, then this could affect
// the editor in ways beyond the project source, so prompt.
let local_settings_folder = paths::local_settings_folder_relative_path();
let local_settings_folder = paths::local_settings_folder_name();
let path = Path::new(&input.path);
if path
.components()
.any(|component| component.as_os_str() == local_settings_folder.as_os_str())
.any(|c| c.as_os_str() == <str as AsRef<OsStr>>::as_ref(local_settings_folder))
{
return true;
}
@@ -195,10 +196,10 @@ impl Tool for EditFileTool {
let mut description = input.display_description.clone();
// Add context about why confirmation may be needed
let local_settings_folder = paths::local_settings_folder_relative_path();
let local_settings_folder = paths::local_settings_folder_name();
if path
.components()
.any(|c| c.as_os_str() == local_settings_folder.as_os_str())
.any(|c| c.as_os_str() == <str as AsRef<OsStr>>::as_ref(local_settings_folder))
{
description.push_str(" (local settings)");
} else if let Ok(canonical_path) = std::fs::canonicalize(&input.path)
@@ -377,7 +378,7 @@ impl Tool for EditFileTool {
.await;
let output = EditFileToolOutput {
original_path: project_path.path.to_path_buf(),
original_path: project_path.path.as_std_path().to_owned(),
new_text,
old_text,
raw_output: Some(agent_output),
@@ -549,10 +550,11 @@ fn resolve_path(
let file_name = input
.path
.file_name()
.and_then(|file_name| file_name.to_str())
.context("Can't create file: invalid filename")?;
let new_file_path = parent_project_path.map(|parent| ProjectPath {
path: Arc::from(parent.path.join(file_name)),
path: parent.path.join(RelPath::unix(file_name).unwrap()),
..parent
});
@@ -1236,7 +1238,7 @@ mod tests {
use serde_json::json;
use settings::SettingsStore;
use std::fs;
use util::path;
use util::{path, rel_path::rel_path};
#[gpui::test]
async fn test_edit_nonexistent_file(cx: &mut TestAppContext) {
@@ -1355,14 +1357,10 @@ mod tests {
cx.update(|cx| resolve_path(&input, project, cx))
}
#[track_caller]
fn assert_resolved_path_eq(path: anyhow::Result<ProjectPath>, expected: &str) {
let actual = path
.expect("Should return valid path")
.path
.to_str()
.unwrap()
.replace("\\", "/"); // Naive Windows paths normalization
assert_eq!(actual, expected);
let actual = path.expect("Should return valid path").path;
assert_eq!(actual.as_ref(), rel_path(expected));
}
#[test]
@@ -1976,25 +1974,22 @@ mod tests {
let project = Project::test(fs.clone(), [path!("/home/user/myproject").as_ref()], cx).await;
// Get the actual local settings folder name
let local_settings_folder = paths::local_settings_folder_relative_path();
let local_settings_folder = paths::local_settings_folder_name();
// Test various config path patterns
let test_cases = vec![
(
format!("{}/settings.json", local_settings_folder.display()),
format!("{local_settings_folder}/settings.json"),
true,
"Top-level local settings file".to_string(),
),
(
format!(
"myproject/{}/settings.json",
local_settings_folder.display()
),
format!("myproject/{local_settings_folder}/settings.json"),
true,
"Local settings in project path".to_string(),
),
(
format!("src/{}/config.toml", local_settings_folder.display()),
format!("src/{local_settings_folder}/config.toml"),
true,
"Local settings in subdirectory".to_string(),
),
@@ -2205,12 +2200,7 @@ mod tests {
("", false, "Empty path is treated as project root"),
// Root directory
("/", true, "Root directory should be outside project"),
// Parent directory references - find_project_path resolves these
(
"project/../other",
false,
"Path with .. is resolved by find_project_path",
),
("project/../other", true, "Path with .. is outside project"),
(
"project/./src/file.rs",
false,

View File

@@ -161,10 +161,13 @@ impl Tool for FindPathTool {
}
fn search_paths(glob: &str, project: Entity<Project>, cx: &mut App) -> Task<Result<Vec<PathBuf>>> {
let path_matcher = match PathMatcher::new([
// Sometimes models try to search for "". In this case, return all paths in the project.
if glob.is_empty() { "*" } else { glob },
]) {
let path_matcher = match PathMatcher::new(
[
// Sometimes models try to search for "". In this case, return all paths in the project.
if glob.is_empty() { "*" } else { glob },
],
project.read(cx).path_style(cx),
) {
Ok(matcher) => matcher,
Err(err) => return Task::ready(Err(anyhow!("Invalid glob: {err}"))),
};
@@ -178,10 +181,15 @@ fn search_paths(glob: &str, project: Entity<Project>, cx: &mut App) -> Task<Resu
Ok(snapshots
.iter()
.flat_map(|snapshot| {
let root_name = PathBuf::from(snapshot.root_name());
snapshot
.entries(false, 0)
.map(move |entry| root_name.join(&entry.path))
.map(move |entry| {
snapshot
.root_name()
.join(&entry.path)
.as_std_path()
.to_path_buf()
})
.filter(|path| path_matcher.is_match(&path))
})
.collect())
@@ -254,7 +262,7 @@ impl ToolCard for FindPathToolCard {
.children(self.paths.iter().enumerate().map(|(index, path)| {
let path_clone = path.clone();
let workspace_clone = workspace.clone();
let button_label = path.to_string_lossy().to_string();
let button_label = path.to_string_lossy().into_owned();
Button::new(("path", index), button_label)
.icon(IconName::ArrowUpRight)

View File

@@ -125,6 +125,7 @@ impl Tool for GrepTool {
.as_ref()
.into_iter()
.collect::<Vec<_>>(),
project.read(cx).path_style(cx),
) {
Ok(matcher) => matcher,
Err(error) => {
@@ -141,7 +142,7 @@ impl Tool for GrepTool {
.iter()
.chain(global_settings.private_files.sources().iter());
match PathMatcher::new(exclude_patterns) {
match PathMatcher::new(exclude_patterns, project.read(cx).path_style(cx)) {
Ok(matcher) => matcher,
Err(error) => {
return Task::ready(Err(anyhow!("invalid exclude pattern: {error}"))).into();

View File

@@ -4,11 +4,11 @@ use anyhow::{Result, anyhow};
use assistant_tool::{Tool, ToolResult};
use gpui::{AnyWindowHandle, App, Entity, Task};
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
use project::{Project, WorktreeSettings};
use project::{Project, ProjectPath, WorktreeSettings};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::Settings;
use std::{fmt::Write, path::Path, sync::Arc};
use std::{fmt::Write, sync::Arc};
use ui::IconName;
use util::markdown::MarkdownInlineCode;
@@ -86,6 +86,7 @@ impl Tool for ListDirectoryTool {
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let path_style = project.read(cx).path_style(cx);
let input = match serde_json::from_value::<ListDirectoryToolInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
@@ -100,7 +101,7 @@ impl Tool for ListDirectoryTool {
.filter_map(|worktree| {
worktree.read(cx).root_entry().and_then(|entry| {
if entry.is_dir() {
entry.path.to_str()
Some(entry.path.display(path_style))
} else {
None
}
@@ -158,7 +159,6 @@ impl Tool for ListDirectoryTool {
}
let worktree_snapshot = worktree.read(cx).snapshot();
let worktree_root_name = worktree.read(cx).root_name().to_string();
let Some(entry) = worktree_snapshot.entry_for_path(&project_path.path) else {
return Task::ready(Err(anyhow!("Path not found: {}", input.path))).into();
@@ -180,23 +180,22 @@ impl Tool for ListDirectoryTool {
continue;
}
if project
.read(cx)
.find_project_path(&entry.path, cx)
.map(|project_path| {
let worktree_settings = WorktreeSettings::get(Some((&project_path).into()), cx);
let project_path = ProjectPath {
worktree_id: worktree_snapshot.id(),
path: entry.path.clone(),
};
let worktree_settings = WorktreeSettings::get(Some((&project_path).into()), cx);
worktree_settings.is_path_excluded(&project_path.path)
|| worktree_settings.is_path_private(&project_path.path)
})
.unwrap_or(false)
if worktree_settings.is_path_excluded(&project_path.path)
|| worktree_settings.is_path_private(&project_path.path)
{
continue;
}
let full_path = Path::new(&worktree_root_name)
let full_path = worktree_snapshot
.root_name()
.join(&entry.path)
.display()
.display(worktree_snapshot.path_style())
.to_string();
if entry.is_dir() {
folders.push(full_path);

View File

@@ -108,7 +108,7 @@ impl Tool for MovePathTool {
.and_then(|project_path| project.entry_for_path(&project_path, cx))
{
Some(entity) => match project.find_project_path(&input.destination_path, cx) {
Some(project_path) => project.rename_entry(entity.id, project_path.path, cx),
Some(project_path) => project.rename_entry(entity.id, project_path, cx),
None => Task::ready(Err(anyhow!(
"Destination path {} was outside the project.",
input.destination_path

View File

@@ -104,7 +104,7 @@ mod tests {
async fn test_to_absolute_path(cx: &mut TestAppContext) {
init_test(cx);
let temp_dir = TempDir::new().expect("Failed to create temp directory");
let temp_path = temp_dir.path().to_string_lossy().to_string();
let temp_path = temp_dir.path().to_string_lossy().into_owned();
let fs = FakeFs::new(cx.executor());
fs.insert_tree(

View File

@@ -261,9 +261,8 @@ impl Tool for ReadFileTool {
Ok(result)
} else {
// No line ranges specified, so check file size to see if it's too big.
let path_buf = std::path::PathBuf::from(&file_path);
let buffer_content =
outline::get_buffer_content_or_outline(buffer.clone(), Some(&path_buf), cx)
outline::get_buffer_content_or_outline(buffer.clone(), Some(&file_path), cx)
.await?;
action_log.update(cx, |log, cx| {

View File

@@ -139,18 +139,25 @@ impl Tool for TerminalTool {
env
});
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()), &[])
}
};
let Some(window) = window else {
// Headless setup, a test or eval. Our terminal subsystem requires a workspace,
// so bypass it and provide a convincing imitation using a pty.
let task = cx.background_spawn(async move {
let env = env.await;
let pty_system = native_pty_system();
let (command, args) = ShellBuilder::new(
remote_shell.as_deref(),
&Shell::Program(get_default_system_shell()),
)
.redirect_stdin_to_dev_null()
.build(Some(input.command.clone()), &[]);
let (command, args) = build_cmd();
let mut cmd = CommandBuilder::new(command);
cmd.args(args);
for (k, v) in env {
@@ -187,16 +194,10 @@ impl Tool for TerminalTool {
};
};
let command = input.command.clone();
let terminal = cx.spawn({
let project = project.downgrade();
async move |cx| {
let (command, args) = ShellBuilder::new(
remote_shell.as_deref(),
&Shell::Program(get_default_system_shell()),
)
.redirect_stdin_to_dev_null()
.build(Some(input.command), &[]);
let (command, args) = build_cmd();
let env = env.await;
project
.update(cx, |project, cx| {
@@ -215,18 +216,18 @@ impl Tool for TerminalTool {
}
});
let command_markdown =
cx.new(|cx| Markdown::new(format!("```bash\n{}\n```", command).into(), None, None, cx));
let card = cx.new(|cx| {
TerminalToolCard::new(
command_markdown.clone(),
working_dir.clone(),
cx.entity_id(),
let command_markdown = cx.new(|cx| {
Markdown::new(
format!("```bash\n{}\n```", input.command).into(),
None,
None,
cx,
)
});
let card =
cx.new(|cx| TerminalToolCard::new(command_markdown, working_dir, cx.entity_id(), cx));
let output = cx.spawn({
let card = card.clone();
async move |cx| {
@@ -267,7 +268,7 @@ impl Tool for TerminalTool {
let previous_len = content.len();
let (processed_content, finished_with_empty_output) = process_content(
&content,
&command,
&input.command,
exit_status.map(portable_pty::ExitStatus::from),
);

View File

@@ -18,9 +18,11 @@ async-tar.workspace = true
collections.workspace = true
crossbeam.workspace = true
gpui.workspace = true
denoise = { path = "../denoise" }
log.workspace = true
parking_lot.workspace = true
rodio = { workspace = true, features = [ "wav", "playback", "wav_output" ] }
rubato = "0.16.2"
serde.workspace = true
settings.workspace = true
smol.workspace = true

View File

@@ -9,7 +9,7 @@ mod non_windows_and_freebsd_deps {
pub(super) use log::info;
pub(super) use parking_lot::Mutex;
pub(super) use rodio::cpal::Sample;
pub(super) use rodio::source::{LimitSettings, UniformSourceIterator};
pub(super) use rodio::source::LimitSettings;
pub(super) use std::sync::Arc;
}
@@ -31,18 +31,20 @@ pub use rodio_ext::RodioExt;
use crate::audio_settings::LIVE_SETTINGS;
// NOTE: We used to use WebRTC's mixer which only supported
// 16kHz, 32kHz and 48kHz. As 48 is the most common "next step up"
// for audio output devices like speakers/bluetooth, we just hard-code
// this; and downsample when we need to.
// We are migrating to 16kHz sample rate from 48kHz. In the future
// once we are reasonably sure most users have upgraded we will
// remove the LEGACY parameters.
//
// Since most noise cancelling requires 16kHz we will move to
// that in the future.
pub const SAMPLE_RATE: NonZero<u32> = nz!(48000);
pub const CHANNEL_COUNT: NonZero<u16> = nz!(2);
// We migrate to 16kHz because it is sufficient for speech and required
// by the denoiser and future Speech to Text layers.
pub const SAMPLE_RATE: NonZero<u32> = nz!(16000);
pub const CHANNEL_COUNT: NonZero<u16> = nz!(1);
pub const BUFFER_SIZE: usize = // echo canceller and livekit want 10ms of audio
(SAMPLE_RATE.get() as usize / 100) * CHANNEL_COUNT.get() as usize;
pub const LEGACY_SAMPLE_RATE: NonZero<u32> = nz!(48000);
pub const LEGACY_CHANNEL_COUNT: NonZero<u16> = nz!(2);
pub const REPLAY_DURATION: Duration = Duration::from_secs(30);
pub fn init(cx: &mut App) {
@@ -53,6 +55,7 @@ pub fn init(cx: &mut App) {
#[derive(Debug, Copy, Clone, Eq, Hash, PartialEq)]
pub enum Sound {
Joined,
GuestJoined,
Leave,
Mute,
Unmute,
@@ -65,6 +68,7 @@ impl Sound {
fn file(&self) -> &'static str {
match self {
Self::Joined => "joined_call",
Self::GuestJoined => "guest_joined_call",
Self::Leave => "leave_call",
Self::Mute => "mute",
Self::Unmute => "unmute",
@@ -106,11 +110,16 @@ impl Global for Audio {}
impl Audio {
fn ensure_output_exists(&mut self) -> Result<&Mixer> {
#[cfg(debug_assertions)]
log::warn!(
"Audio does not sound correct without optimizations. Use a release build to debug audio issues"
);
if self.output_handle.is_none() {
self.output_handle = Some(
OutputStreamBuilder::open_default_stream()
.context("Could not open default output stream")?,
);
let output_handle = OutputStreamBuilder::open_default_stream()
.context("Could not open default output stream")?;
info!("Output stream: {:?}", output_handle);
self.output_handle = Some(output_handle);
if let Some(output_handle) = &self.output_handle {
let (mixer, source) = rodio::mixer::mixer(CHANNEL_COUNT, SAMPLE_RATE);
// or the mixer will end immediately as its empty.
@@ -160,13 +169,20 @@ impl Audio {
let stream = rodio::microphone::MicrophoneBuilder::new()
.default_device()?
.default_config()?
.prefer_sample_rates([SAMPLE_RATE, SAMPLE_RATE.saturating_mul(nz!(2))])
// .prefer_channel_counts([nz!(1), nz!(2)])
.prefer_sample_rates([
SAMPLE_RATE, // sample rates trivially resamplable to `SAMPLE_RATE`
SAMPLE_RATE.saturating_mul(nz!(2)),
SAMPLE_RATE.saturating_mul(nz!(3)),
SAMPLE_RATE.saturating_mul(nz!(4)),
])
.prefer_channel_counts([nz!(1), nz!(2), nz!(3), nz!(4)])
.prefer_buffer_sizes(512..)
.open_stream()?;
info!("Opened microphone: {:?}", stream.config());
let (replay, stream) = UniformSourceIterator::new(stream, CHANNEL_COUNT, SAMPLE_RATE)
let stream = stream
.possibly_disconnected_channels_to_mono()
.constant_samplerate(SAMPLE_RATE)
.limit(LimitSettings::live_performance())
.process_buffer::<BUFFER_SIZE, _>(move |buffer| {
let mut int_buffer: [i16; _] = buffer.map(|s| s.to_sample());
@@ -187,15 +203,27 @@ impl Audio {
}
}
})
.automatic_gain_control(1.0, 4.0, 0.0, 5.0)
.denoise()
.context("Could not set up denoiser")?
.automatic_gain_control(0.90, 1.0, 0.0, 5.0)
.periodic_access(Duration::from_millis(100), move |agc_source| {
agc_source.set_enabled(LIVE_SETTINGS.control_input_volume.load(Ordering::Relaxed));
})
.replayable(REPLAY_DURATION)?;
agc_source
.set_enabled(LIVE_SETTINGS.auto_microphone_volume.load(Ordering::Relaxed));
let denoise = agc_source.inner_mut();
denoise.set_enabled(LIVE_SETTINGS.denoise.load(Ordering::Relaxed));
});
let stream = if voip_parts.legacy_audio_compatible {
stream.constant_params(LEGACY_CHANNEL_COUNT, LEGACY_SAMPLE_RATE)
} else {
stream.constant_params(CHANNEL_COUNT, SAMPLE_RATE)
};
let (replay, stream) = stream.replayable(REPLAY_DURATION)?;
voip_parts
.replays
.add_voip_stream("local microphone".to_string(), replay);
Ok(stream)
}
@@ -206,9 +234,10 @@ impl Audio {
cx: &mut App,
) -> anyhow::Result<()> {
let (replay_source, source) = source
.automatic_gain_control(1.0, 4.0, 0.0, 5.0)
.constant_params(CHANNEL_COUNT, SAMPLE_RATE)
.automatic_gain_control(0.90, 1.0, 0.0, 5.0)
.periodic_access(Duration::from_millis(100), move |agc_source| {
agc_source.set_enabled(LIVE_SETTINGS.control_input_volume.load(Ordering::Relaxed));
agc_source.set_enabled(LIVE_SETTINGS.auto_speaker_volume.load(Ordering::Relaxed));
})
.replayable(REPLAY_DURATION)
.expect("REPLAY_DURATION is longer than 100ms");
@@ -269,6 +298,7 @@ impl Audio {
pub struct VoipParts {
echo_canceller: Arc<Mutex<apm::AudioProcessingModule>>,
replays: replays::Replays,
legacy_audio_compatible: bool,
}
#[cfg(not(any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd")))]
@@ -277,8 +307,12 @@ impl VoipParts {
let (apm, replays) = cx.try_read_default_global::<Audio, _>(|audio, _| {
(Arc::clone(&audio.echo_canceller), audio.replays.clone())
})?;
let legacy_audio_compatible =
AudioSettings::try_read_global(cx, |settings| settings.legacy_audio_compatible)
.unwrap_or(true);
Ok(Self {
legacy_audio_compatible,
echo_canceller: apm,
replays,
})

View File

@@ -6,18 +6,38 @@ use settings::{Settings, SettingsStore};
#[derive(Clone, Debug)]
pub struct AudioSettings {
/// Opt into the new audio system.
///
/// You need to rejoin a call for this setting to apply
pub rodio_audio: bool, // default is false
/// Requires 'rodio_audio: true'
///
/// Use the new audio systems automatic gain control for your microphone.
/// This affects how loud you sound to others.
pub control_input_volume: bool,
/// Automatically increase or decrease you microphone's volume. This affects how
/// loud you sound to others.
///
/// Recommended: off (default)
/// Microphones are too quite in zed, until everyone is on experimental
/// audio and has auto speaker volume on this will make you very loud
/// compared to other speakers.
pub auto_microphone_volume: bool,
/// Requires 'rodio_audio: true'
///
/// Use the new audio systems automatic gain control on everyone in the
/// call. This makes call members who are too quite louder and those who are
/// too loud quieter. This only affects how things sound for you.
pub control_output_volume: bool,
/// Automatically increate or decrease the volume of other call members.
/// This only affects how things sound for you.
pub auto_speaker_volume: bool,
/// Requires 'rodio_audio: true'
///
/// Remove background noises. Works great for typing, cars, dogs, AC. Does
/// not work well on music.
pub denoise: bool,
/// Requires 'rodio_audio: true'
///
/// Use audio parameters compatible with the previous versions of
/// experimental audio and non-experimental audio. When this is false you
/// will sound strange to anyone not on the latest experimental audio. In
/// the future we will migrate by setting this to false
///
/// You need to rejoin a call for this setting to apply
pub legacy_audio_compatible: bool,
}
/// Configuration of audio in Zed
@@ -25,46 +45,66 @@ impl Settings for AudioSettings {
fn from_settings(content: &settings::SettingsContent, _cx: &mut App) -> Self {
let audio = &content.audio.as_ref().unwrap();
AudioSettings {
control_input_volume: audio.control_input_volume.unwrap(),
control_output_volume: audio.control_output_volume.unwrap(),
rodio_audio: audio.rodio_audio.unwrap(),
auto_microphone_volume: audio.auto_microphone_volume.unwrap(),
auto_speaker_volume: audio.auto_speaker_volume.unwrap(),
denoise: audio.denoise.unwrap(),
legacy_audio_compatible: audio.legacy_audio_compatible.unwrap(),
}
}
fn import_from_vscode(
_vscode: &settings::VsCodeSettings,
_current: &mut settings::SettingsContent,
) {
}
}
/// See docs on [LIVE_SETTINGS]
pub(crate) struct LiveSettings {
pub(crate) control_input_volume: AtomicBool,
pub(crate) control_output_volume: AtomicBool,
pub(crate) auto_microphone_volume: AtomicBool,
pub(crate) auto_speaker_volume: AtomicBool,
pub(crate) denoise: AtomicBool,
}
impl LiveSettings {
pub(crate) fn initialize(&self, cx: &mut App) {
cx.observe_global::<SettingsStore>(move |cx| {
LIVE_SETTINGS.control_input_volume.store(
AudioSettings::get_global(cx).control_input_volume,
LIVE_SETTINGS.auto_microphone_volume.store(
AudioSettings::get_global(cx).auto_microphone_volume,
Ordering::Relaxed,
);
LIVE_SETTINGS.control_output_volume.store(
AudioSettings::get_global(cx).control_output_volume,
LIVE_SETTINGS.auto_speaker_volume.store(
AudioSettings::get_global(cx).auto_speaker_volume,
Ordering::Relaxed,
);
let denoise_enabled = AudioSettings::get_global(cx).denoise;
#[cfg(debug_assertions)]
{
static DENOISE_WARNING_SEND: AtomicBool = AtomicBool::new(false);
if denoise_enabled && !DENOISE_WARNING_SEND.load(Ordering::Relaxed) {
DENOISE_WARNING_SEND.store(true, Ordering::Relaxed);
log::warn!("Denoise does not work on debug builds, not enabling")
}
}
#[cfg(not(debug_assertions))]
LIVE_SETTINGS
.denoise
.store(denoise_enabled, Ordering::Relaxed);
})
.detach();
let init_settings = AudioSettings::get_global(cx);
LIVE_SETTINGS
.control_input_volume
.store(init_settings.control_input_volume, Ordering::Relaxed);
.auto_microphone_volume
.store(init_settings.auto_microphone_volume, Ordering::Relaxed);
LIVE_SETTINGS
.control_output_volume
.store(init_settings.control_output_volume, Ordering::Relaxed);
.auto_speaker_volume
.store(init_settings.auto_speaker_volume, Ordering::Relaxed);
let denoise_enabled = AudioSettings::get_global(cx).denoise;
#[cfg(debug_assertions)]
if denoise_enabled {
log::warn!("Denoise does not work on debug builds, not enabling")
}
#[cfg(not(debug_assertions))]
LIVE_SETTINGS
.denoise
.store(denoise_enabled, Ordering::Relaxed);
}
}
@@ -73,6 +113,7 @@ impl LiveSettings {
/// real time and must each run in a dedicated OS thread, therefore we can not
/// use the background executor.
pub(crate) static LIVE_SETTINGS: LiveSettings = LiveSettings {
control_input_volume: AtomicBool::new(true),
control_output_volume: AtomicBool::new(true),
auto_microphone_volume: AtomicBool::new(true),
auto_speaker_volume: AtomicBool::new(true),
denoise: AtomicBool::new(true),
};

View File

@@ -1,18 +1,20 @@
use std::{
sync::{
Arc, Mutex,
atomic::{AtomicBool, Ordering},
},
time::Duration,
};
use std::{num::NonZero, time::Duration};
use crossbeam::queue::ArrayQueue;
use rodio::{ChannelCount, Sample, SampleRate, Source};
use denoise::{Denoiser, DenoiserError};
use log::warn;
use rodio::{ChannelCount, Sample, SampleRate, Source, conversions::ChannelCountConverter, nz};
#[derive(Debug, thiserror::Error)]
#[error("Replay duration is too short must be >= 100ms")]
pub struct ReplayDurationTooShort;
use crate::rodio_ext::resample::FixedResampler;
pub use replayable::{Replay, ReplayDurationTooShort, Replayable};
mod replayable;
mod resample;
const MAX_CHANNELS: usize = 8;
// These all require constant sources (so the span is infinitely long)
// this is not guaranteed by rodio however we know it to be true in all our
// applications. Rodio desperately needs a constant source concept.
pub trait RodioExt: Source + Sized {
fn process_buffer<const N: usize, F>(self, callback: F) -> ProcessBuffer<N, Self, F>
where
@@ -25,6 +27,14 @@ pub trait RodioExt: Source + Sized {
duration: Duration,
) -> Result<(Replay, Replayable<Self>), ReplayDurationTooShort>;
fn take_samples(self, n: usize) -> TakeSamples<Self>;
fn denoise(self) -> Result<Denoiser<Self>, DenoiserError>;
fn constant_params(
self,
channel_count: ChannelCount,
sample_rate: SampleRate,
) -> ConstantChannelCount<FixedResampler<Self>>;
fn constant_samplerate(self, sample_rate: SampleRate) -> FixedResampler<Self>;
fn possibly_disconnected_channels_to_mono(self) -> ToMono<Self>;
}
impl<S: Source> RodioExt for S {
@@ -62,38 +72,7 @@ impl<S: Source> RodioExt for S {
self,
duration: Duration,
) -> Result<(Replay, Replayable<Self>), ReplayDurationTooShort> {
if duration < Duration::from_millis(100) {
return Err(ReplayDurationTooShort);
}
let samples_per_second = self.sample_rate().get() as usize * self.channels().get() as usize;
let samples_to_queue = duration.as_secs_f64() * samples_per_second as f64;
let samples_to_queue =
(samples_to_queue as usize).next_multiple_of(self.channels().get().into());
let chunk_size =
(samples_per_second.div_ceil(10)).next_multiple_of(self.channels().get() as usize);
let chunks_to_queue = samples_to_queue.div_ceil(chunk_size);
let is_active = Arc::new(AtomicBool::new(true));
let queue = Arc::new(ReplayQueue::new(chunks_to_queue, chunk_size));
Ok((
Replay {
rx: Arc::clone(&queue),
buffer: Vec::new().into_iter(),
sleep_duration: duration / 2,
sample_rate: self.sample_rate(),
channel_count: self.channels(),
source_is_active: is_active.clone(),
},
Replayable {
tx: queue,
inner: self,
buffer: Vec::with_capacity(chunk_size),
chunk_size,
is_active,
},
))
replayable::replayable(self, duration)
}
fn take_samples(self, n: usize) -> TakeSamples<S> {
TakeSamples {
@@ -101,8 +80,149 @@ impl<S: Source> RodioExt for S {
left_to_take: n,
}
}
fn denoise(self) -> Result<Denoiser<Self>, DenoiserError> {
let res = Denoiser::try_new(self);
res
}
fn constant_params(
self,
channel_count: ChannelCount,
sample_rate: SampleRate,
) -> ConstantChannelCount<FixedResampler<Self>> {
ConstantChannelCount::new(self.constant_samplerate(sample_rate), channel_count)
}
fn constant_samplerate(self, sample_rate: SampleRate) -> FixedResampler<Self> {
FixedResampler::new(self, sample_rate)
}
fn possibly_disconnected_channels_to_mono(self) -> ToMono<Self> {
ToMono::new(self)
}
}
pub struct ConstantChannelCount<S: Source> {
inner: ChannelCountConverter<S>,
channels: ChannelCount,
sample_rate: SampleRate,
}
impl<S: Source> ConstantChannelCount<S> {
fn new(source: S, target_channels: ChannelCount) -> Self {
let input_channels = source.channels();
let sample_rate = source.sample_rate();
let inner = ChannelCountConverter::new(source, input_channels, target_channels);
Self {
sample_rate,
inner,
channels: target_channels,
}
}
}
impl<S: Source> Iterator for ConstantChannelCount<S> {
type Item = rodio::Sample;
fn next(&mut self) -> Option<Self::Item> {
self.inner.next()
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.inner.size_hint()
}
}
impl<S: Source> Source for ConstantChannelCount<S> {
fn current_span_len(&self) -> Option<usize> {
None
}
fn channels(&self) -> ChannelCount {
self.channels
}
fn sample_rate(&self) -> SampleRate {
self.sample_rate
}
fn total_duration(&self) -> Option<Duration> {
None // not supported (not used by us)
}
}
const TYPICAL_NOISE_FLOOR: Sample = 1e-3;
/// constant source, only works on a single span
pub struct ToMono<S> {
inner: S,
input_channel_count: ChannelCount,
connected_channels: ChannelCount,
/// running mean of second channel 'volume'
means: [f32; MAX_CHANNELS],
}
impl<S: Source> ToMono<S> {
fn new(input: S) -> Self {
let channels = input
.channels()
.min(const { NonZero::<u16>::new(MAX_CHANNELS as u16).unwrap() });
if channels < input.channels() {
warn!("Ignoring input channels {}..", channels.get());
}
Self {
connected_channels: channels,
input_channel_count: channels,
inner: input,
means: [TYPICAL_NOISE_FLOOR; MAX_CHANNELS],
}
}
}
impl<S: Source> Source for ToMono<S> {
fn current_span_len(&self) -> Option<usize> {
None
}
fn channels(&self) -> ChannelCount {
rodio::nz!(1)
}
fn sample_rate(&self) -> SampleRate {
self.inner.sample_rate()
}
fn total_duration(&self) -> Option<Duration> {
self.inner.total_duration()
}
}
fn update_mean(mean: &mut f32, sample: Sample) {
const HISTORY: f32 = 500.0;
*mean *= (HISTORY - 1.0) / HISTORY;
*mean += sample.abs() / HISTORY;
}
impl<S: Source> Iterator for ToMono<S> {
type Item = Sample;
fn next(&mut self) -> Option<Self::Item> {
let mut mono_sample = 0f32;
let mut active_channels = 0;
for channel in 0..self.input_channel_count.get() as usize {
let sample = self.inner.next()?;
mono_sample += sample;
update_mean(&mut self.means[channel], sample);
if self.means[channel] > TYPICAL_NOISE_FLOOR / 10.0 {
active_channels += 1;
}
}
mono_sample /= self.connected_channels.get() as f32;
self.connected_channels = NonZero::new(active_channels).unwrap_or(nz!(1));
Some(mono_sample)
}
}
/// constant source, only works on a single span
pub struct TakeSamples<S> {
inner: S,
left_to_take: usize,
@@ -147,52 +267,7 @@ impl<S: Source> Source for TakeSamples<S> {
}
}
#[derive(Debug)]
struct ReplayQueue {
inner: ArrayQueue<Vec<Sample>>,
normal_chunk_len: usize,
/// The last chunk in the queue may be smaller than
/// the normal chunk size. This is always equal to the
/// size of the last element in the queue.
/// (so normally chunk_size)
last_chunk: Mutex<Vec<Sample>>,
}
impl ReplayQueue {
fn new(queue_len: usize, chunk_size: usize) -> Self {
Self {
inner: ArrayQueue::new(queue_len),
normal_chunk_len: chunk_size,
last_chunk: Mutex::new(Vec::new()),
}
}
/// Returns the length in samples
fn len(&self) -> usize {
self.inner.len().saturating_sub(1) * self.normal_chunk_len
+ self
.last_chunk
.lock()
.expect("Self::push_last can not poison this lock")
.len()
}
fn pop(&self) -> Option<Vec<Sample>> {
self.inner.pop() // removes element that was inserted first
}
fn push_last(&self, mut samples: Vec<Sample>) {
let mut last_chunk = self
.last_chunk
.lock()
.expect("Self::len can not poison this lock");
std::mem::swap(&mut *last_chunk, &mut samples);
}
fn push_normal(&self, samples: Vec<Sample>) {
let _pushed_out_of_ringbuf = self.inner.force_push(samples);
}
}
/// constant source, only works on a single span
pub struct ProcessBuffer<const N: usize, S, F>
where
S: Source + Sized,
@@ -260,6 +335,7 @@ where
}
}
/// constant source, only works on a single span
pub struct InspectBuffer<const N: usize, S, F>
where
S: Source + Sized,
@@ -324,145 +400,15 @@ where
}
}
#[derive(Debug)]
pub struct Replayable<S: Source> {
inner: S,
buffer: Vec<Sample>,
chunk_size: usize,
tx: Arc<ReplayQueue>,
is_active: Arc<AtomicBool>,
}
impl<S: Source> Iterator for Replayable<S> {
type Item = Sample;
fn next(&mut self) -> Option<Self::Item> {
if let Some(sample) = self.inner.next() {
self.buffer.push(sample);
// If the buffer is full send it
if self.buffer.len() == self.chunk_size {
self.tx.push_normal(std::mem::take(&mut self.buffer));
}
Some(sample)
} else {
let last_chunk = std::mem::take(&mut self.buffer);
self.tx.push_last(last_chunk);
self.is_active.store(false, Ordering::Relaxed);
None
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.inner.size_hint()
}
}
impl<S: Source> Source for Replayable<S> {
fn current_span_len(&self) -> Option<usize> {
self.inner.current_span_len()
}
fn channels(&self) -> ChannelCount {
self.inner.channels()
}
fn sample_rate(&self) -> SampleRate {
self.inner.sample_rate()
}
fn total_duration(&self) -> Option<Duration> {
self.inner.total_duration()
}
}
#[derive(Debug)]
pub struct Replay {
rx: Arc<ReplayQueue>,
buffer: std::vec::IntoIter<Sample>,
sleep_duration: Duration,
sample_rate: SampleRate,
channel_count: ChannelCount,
source_is_active: Arc<AtomicBool>,
}
impl Replay {
pub fn source_is_active(&self) -> bool {
// - source could return None and not drop
// - source could be dropped before returning None
self.source_is_active.load(Ordering::Relaxed) && Arc::strong_count(&self.rx) < 2
}
/// Duration of what is in the buffer and can be returned without blocking.
pub fn duration_ready(&self) -> Duration {
let samples_per_second = self.channels().get() as u32 * self.sample_rate().get();
let seconds_queued = self.samples_ready() as f64 / samples_per_second as f64;
Duration::from_secs_f64(seconds_queued)
}
/// Number of samples in the buffer and can be returned without blocking.
pub fn samples_ready(&self) -> usize {
self.rx.len() + self.buffer.len()
}
}
impl Iterator for Replay {
type Item = Sample;
fn next(&mut self) -> Option<Self::Item> {
if let Some(sample) = self.buffer.next() {
return Some(sample);
}
loop {
if let Some(new_buffer) = self.rx.pop() {
self.buffer = new_buffer.into_iter();
return self.buffer.next();
}
if !self.source_is_active() {
return None;
}
// The queue does not support blocking on a next item. We want this queue as it
// is quite fast and provides a fixed size. We know how many samples are in a
// buffer so if we do not get one now we must be getting one after `sleep_duration`.
std::thread::sleep(self.sleep_duration);
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
((self.rx.len() + self.buffer.len()), None)
}
}
impl Source for Replay {
fn current_span_len(&self) -> Option<usize> {
None // source is not compatible with spans
}
fn channels(&self) -> ChannelCount {
self.channel_count
}
fn sample_rate(&self) -> SampleRate {
self.sample_rate
}
fn total_duration(&self) -> Option<Duration> {
None
}
}
#[cfg(test)]
mod tests {
use rodio::{nz, static_buffer::StaticSamplesBuffer};
use super::*;
const SAMPLES: [Sample; 5] = [0.0, 1.0, 2.0, 3.0, 4.0];
pub const SAMPLES: [Sample; 5] = [0.0, 1.0, 2.0, 3.0, 4.0];
fn test_source() -> StaticSamplesBuffer {
pub fn test_source() -> StaticSamplesBuffer {
StaticSamplesBuffer::new(nz!(1), nz!(1), &SAMPLES)
}
@@ -525,74 +471,4 @@ mod tests {
assert_eq!(yielded, SAMPLES.len())
}
}
mod instant_replay {
use super::*;
#[test]
fn continues_after_history() {
let input = test_source();
let (mut replay, mut source) = input
.replayable(Duration::from_secs(3))
.expect("longer than 100ms");
source.by_ref().take(3).count();
let yielded: Vec<Sample> = replay.by_ref().take(3).collect();
assert_eq!(&yielded, &SAMPLES[0..3],);
source.count();
let yielded: Vec<Sample> = replay.collect();
assert_eq!(&yielded, &SAMPLES[3..5],);
}
#[test]
fn keeps_only_latest() {
let input = test_source();
let (mut replay, mut source) = input
.replayable(Duration::from_secs(2))
.expect("longer than 100ms");
source.by_ref().take(5).count(); // get all items but do not end the source
let yielded: Vec<Sample> = replay.by_ref().take(2).collect();
assert_eq!(&yielded, &SAMPLES[3..5]);
source.count(); // exhaust source
assert_eq!(replay.next(), None);
}
#[test]
fn keeps_correct_amount_of_seconds() {
let input = StaticSamplesBuffer::new(nz!(1), nz!(16_000), &[0.0; 40_000]);
let (replay, mut source) = input
.replayable(Duration::from_secs(2))
.expect("longer than 100ms");
// exhaust but do not yet end source
source.by_ref().take(40_000).count();
// take all samples we can without blocking
let ready = replay.samples_ready();
let n_yielded = replay.take_samples(ready).count();
let max = source.sample_rate().get() * source.channels().get() as u32 * 2;
let margin = 16_000 / 10; // 100ms
assert!(n_yielded as u32 >= max - margin);
}
#[test]
fn samples_ready() {
let input = StaticSamplesBuffer::new(nz!(1), nz!(16_000), &[0.0; 40_000]);
let (mut replay, source) = input
.replayable(Duration::from_secs(2))
.expect("longer than 100ms");
assert_eq!(replay.by_ref().samples_ready(), 0);
source.take(8000).count(); // half a second
let margin = 16_000 / 10; // 100ms
let ready = replay.samples_ready();
assert!(ready >= 8000 - margin);
}
}
}

View File

@@ -0,0 +1,308 @@
use std::{
sync::{
Arc, Mutex,
atomic::{AtomicBool, Ordering},
},
time::Duration,
};
use crossbeam::queue::ArrayQueue;
use rodio::{ChannelCount, Sample, SampleRate, Source};
#[derive(Debug, thiserror::Error)]
#[error("Replay duration is too short must be >= 100ms")]
pub struct ReplayDurationTooShort;
pub fn replayable<S: Source>(
source: S,
duration: Duration,
) -> Result<(Replay, Replayable<S>), ReplayDurationTooShort> {
if duration < Duration::from_millis(100) {
return Err(ReplayDurationTooShort);
}
let samples_per_second = source.sample_rate().get() as usize * source.channels().get() as usize;
let samples_to_queue = duration.as_secs_f64() * samples_per_second as f64;
let samples_to_queue =
(samples_to_queue as usize).next_multiple_of(source.channels().get().into());
let chunk_size =
(samples_per_second.div_ceil(10)).next_multiple_of(source.channels().get() as usize);
let chunks_to_queue = samples_to_queue.div_ceil(chunk_size);
let is_active = Arc::new(AtomicBool::new(true));
let queue = Arc::new(ReplayQueue::new(chunks_to_queue, chunk_size));
Ok((
Replay {
rx: Arc::clone(&queue),
buffer: Vec::new().into_iter(),
sleep_duration: duration / 2,
sample_rate: source.sample_rate(),
channel_count: source.channels(),
source_is_active: is_active.clone(),
},
Replayable {
tx: queue,
inner: source,
buffer: Vec::with_capacity(chunk_size),
chunk_size,
is_active,
},
))
}
/// constant source, only works on a single span
#[derive(Debug)]
struct ReplayQueue {
inner: ArrayQueue<Vec<Sample>>,
normal_chunk_len: usize,
/// The last chunk in the queue may be smaller than
/// the normal chunk size. This is always equal to the
/// size of the last element in the queue.
/// (so normally chunk_size)
last_chunk: Mutex<Vec<Sample>>,
}
impl ReplayQueue {
fn new(queue_len: usize, chunk_size: usize) -> Self {
Self {
inner: ArrayQueue::new(queue_len),
normal_chunk_len: chunk_size,
last_chunk: Mutex::new(Vec::new()),
}
}
/// Returns the length in samples
fn len(&self) -> usize {
self.inner.len().saturating_sub(1) * self.normal_chunk_len
+ self
.last_chunk
.lock()
.expect("Self::push_last can not poison this lock")
.len()
}
fn pop(&self) -> Option<Vec<Sample>> {
self.inner.pop() // removes element that was inserted first
}
fn push_last(&self, mut samples: Vec<Sample>) {
let mut last_chunk = self
.last_chunk
.lock()
.expect("Self::len can not poison this lock");
std::mem::swap(&mut *last_chunk, &mut samples);
}
fn push_normal(&self, samples: Vec<Sample>) {
let _pushed_out_of_ringbuf = self.inner.force_push(samples);
}
}
/// constant source, only works on a single span
#[derive(Debug)]
pub struct Replayable<S: Source> {
inner: S,
buffer: Vec<Sample>,
chunk_size: usize,
tx: Arc<ReplayQueue>,
is_active: Arc<AtomicBool>,
}
impl<S: Source> Iterator for Replayable<S> {
type Item = Sample;
fn next(&mut self) -> Option<Self::Item> {
if let Some(sample) = self.inner.next() {
self.buffer.push(sample);
// If the buffer is full send it
if self.buffer.len() == self.chunk_size {
self.tx.push_normal(std::mem::take(&mut self.buffer));
}
Some(sample)
} else {
let last_chunk = std::mem::take(&mut self.buffer);
self.tx.push_last(last_chunk);
self.is_active.store(false, Ordering::Relaxed);
None
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.inner.size_hint()
}
}
impl<S: Source> Source for Replayable<S> {
fn current_span_len(&self) -> Option<usize> {
self.inner.current_span_len()
}
fn channels(&self) -> ChannelCount {
self.inner.channels()
}
fn sample_rate(&self) -> SampleRate {
self.inner.sample_rate()
}
fn total_duration(&self) -> Option<Duration> {
self.inner.total_duration()
}
}
/// constant source, only works on a single span
#[derive(Debug)]
pub struct Replay {
rx: Arc<ReplayQueue>,
buffer: std::vec::IntoIter<Sample>,
sleep_duration: Duration,
sample_rate: SampleRate,
channel_count: ChannelCount,
source_is_active: Arc<AtomicBool>,
}
impl Replay {
pub fn source_is_active(&self) -> bool {
// - source could return None and not drop
// - source could be dropped before returning None
self.source_is_active.load(Ordering::Relaxed) && Arc::strong_count(&self.rx) < 2
}
/// Duration of what is in the buffer and can be returned without blocking.
pub fn duration_ready(&self) -> Duration {
let samples_per_second = self.channels().get() as u32 * self.sample_rate().get();
let seconds_queued = self.samples_ready() as f64 / samples_per_second as f64;
Duration::from_secs_f64(seconds_queued)
}
/// Number of samples in the buffer and can be returned without blocking.
pub fn samples_ready(&self) -> usize {
self.rx.len() + self.buffer.len()
}
}
impl Iterator for Replay {
type Item = Sample;
fn next(&mut self) -> Option<Self::Item> {
if let Some(sample) = self.buffer.next() {
return Some(sample);
}
loop {
if let Some(new_buffer) = self.rx.pop() {
self.buffer = new_buffer.into_iter();
return self.buffer.next();
}
if !self.source_is_active() {
return None;
}
// The queue does not support blocking on a next item. We want this queue as it
// is quite fast and provides a fixed size. We know how many samples are in a
// buffer so if we do not get one now we must be getting one after `sleep_duration`.
std::thread::sleep(self.sleep_duration);
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
((self.rx.len() + self.buffer.len()), None)
}
}
impl Source for Replay {
fn current_span_len(&self) -> Option<usize> {
None // source is not compatible with spans
}
fn channels(&self) -> ChannelCount {
self.channel_count
}
fn sample_rate(&self) -> SampleRate {
self.sample_rate
}
fn total_duration(&self) -> Option<Duration> {
None
}
}
#[cfg(test)]
mod tests {
use rodio::{nz, static_buffer::StaticSamplesBuffer};
use super::*;
use crate::{
RodioExt,
rodio_ext::tests::{SAMPLES, test_source},
};
#[test]
fn continues_after_history() {
let input = test_source();
let (mut replay, mut source) = input
.replayable(Duration::from_secs(3))
.expect("longer than 100ms");
source.by_ref().take(3).count();
let yielded: Vec<Sample> = replay.by_ref().take(3).collect();
assert_eq!(&yielded, &SAMPLES[0..3],);
source.count();
let yielded: Vec<Sample> = replay.collect();
assert_eq!(&yielded, &SAMPLES[3..5],);
}
#[test]
fn keeps_only_latest() {
let input = test_source();
let (mut replay, mut source) = input
.replayable(Duration::from_secs(2))
.expect("longer than 100ms");
source.by_ref().take(5).count(); // get all items but do not end the source
let yielded: Vec<Sample> = replay.by_ref().take(2).collect();
assert_eq!(&yielded, &SAMPLES[3..5]);
source.count(); // exhaust source
assert_eq!(replay.next(), None);
}
#[test]
fn keeps_correct_amount_of_seconds() {
let input = StaticSamplesBuffer::new(nz!(1), nz!(16_000), &[0.0; 40_000]);
let (replay, mut source) = input
.replayable(Duration::from_secs(2))
.expect("longer than 100ms");
// exhaust but do not yet end source
source.by_ref().take(40_000).count();
// take all samples we can without blocking
let ready = replay.samples_ready();
let n_yielded = replay.take_samples(ready).count();
let max = source.sample_rate().get() * source.channels().get() as u32 * 2;
let margin = 16_000 / 10; // 100ms
assert!(n_yielded as u32 >= max - margin);
}
#[test]
fn samples_ready() {
let input = StaticSamplesBuffer::new(nz!(1), nz!(16_000), &[0.0; 40_000]);
let (mut replay, source) = input
.replayable(Duration::from_secs(2))
.expect("longer than 100ms");
assert_eq!(replay.by_ref().samples_ready(), 0);
source.take(8000).count(); // half a second
let margin = 16_000 / 10; // 100ms
let ready = replay.samples_ready();
assert!(ready >= 8000 - margin);
}
}

View File

@@ -0,0 +1,98 @@
use std::time::Duration;
use rodio::{Sample, SampleRate, Source};
use rubato::{FftFixedInOut, Resampler};
pub struct FixedResampler<S> {
input: S,
next_channel: usize,
next_frame: usize,
output_buffer: Vec<Vec<Sample>>,
input_buffer: Vec<Vec<Sample>>,
target_sample_rate: SampleRate,
resampler: FftFixedInOut<Sample>,
}
impl<S: Source> FixedResampler<S> {
pub fn new(input: S, target_sample_rate: SampleRate) -> Self {
let chunk_size_in =
Duration::from_millis(50).as_secs_f32() * input.sample_rate().get() as f32;
let chunk_size_in = chunk_size_in.ceil() as usize;
let resampler = FftFixedInOut::new(
input.sample_rate().get() as usize,
target_sample_rate.get() as usize,
chunk_size_in,
input.channels().get() as usize,
)
.expect(
"sample rates are non zero, and we are not changing it so there is no resample ratio",
);
Self {
next_channel: 0,
next_frame: 0,
output_buffer: resampler.output_buffer_allocate(true),
input_buffer: resampler.input_buffer_allocate(false),
target_sample_rate,
resampler,
input,
}
}
}
impl<S: Source> Source for FixedResampler<S> {
fn current_span_len(&self) -> Option<usize> {
None
}
fn channels(&self) -> rodio::ChannelCount {
self.input.channels()
}
fn sample_rate(&self) -> rodio::SampleRate {
self.target_sample_rate
}
fn total_duration(&self) -> Option<std::time::Duration> {
self.input.total_duration()
}
}
impl<S: Source> FixedResampler<S> {
fn next_sample(&mut self) -> Option<Sample> {
let sample = self.output_buffer[self.next_channel]
.get(self.next_frame)
.copied();
self.next_channel = (self.next_channel + 1) % self.input.channels().get() as usize;
self.next_frame += 1;
sample
}
}
impl<S: Source> Iterator for FixedResampler<S> {
type Item = Sample;
fn next(&mut self) -> Option<Self::Item> {
if let Some(sample) = self.next_sample() {
return Some(sample);
}
for input_channel in &mut self.input_buffer {
input_channel.clear();
}
for _ in 0..self.resampler.input_frames_next() {
for input_channel in &mut self.input_buffer {
input_channel.push(self.input.next()?);
}
}
self.resampler
.process_into_buffer(&mut self.input_buffer, &mut self.output_buffer, None).expect("Input and output buffer channels are correct as they have been set by the resampler. The buffer for each channel is the same length. The buffer length is what is requested the resampler.");
self.next_frame = 0;
self.next_sample()
}
}

View File

@@ -3,7 +3,8 @@ use client::{Client, TelemetrySettings};
use db::RELEASE_CHANNEL;
use db::kvp::KEY_VALUE_STORE;
use gpui::{
App, AppContext as _, AsyncApp, Context, Entity, Global, SemanticVersion, Task, Window, actions,
App, AppContext as _, AsyncApp, BackgroundExecutor, Context, Entity, Global, SemanticVersion,
Task, Window, actions,
};
use http_client::{AsyncBody, HttpClient, HttpClientWithUrl};
use paths::remote_servers_dir;
@@ -12,6 +13,7 @@ use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsStore};
use smol::{fs, io::AsyncReadExt};
use smol::{fs::File, process::Command};
use std::mem;
use std::{
env::{
self,
@@ -84,31 +86,37 @@ pub struct JsonRelease {
pub url: String,
}
struct MacOsUnmounter {
struct MacOsUnmounter<'a> {
mount_path: PathBuf,
background_executor: &'a BackgroundExecutor,
}
impl Drop for MacOsUnmounter {
impl Drop for MacOsUnmounter<'_> {
fn drop(&mut self) {
let unmount_output = std::process::Command::new("hdiutil")
.args(["detach", "-force"])
.arg(&self.mount_path)
.output();
match unmount_output {
Ok(output) if output.status.success() => {
log::info!("Successfully unmounted the disk image");
}
Ok(output) => {
log::error!(
"Failed to unmount disk image: {:?}",
String::from_utf8_lossy(&output.stderr)
);
}
Err(error) => {
log::error!("Error while trying to unmount disk image: {:?}", error);
}
}
let mount_path = mem::take(&mut self.mount_path);
self.background_executor
.spawn(async move {
let unmount_output = Command::new("hdiutil")
.args(["detach", "-force"])
.arg(&mount_path)
.output()
.await;
match unmount_output {
Ok(output) if output.status.success() => {
log::info!("Successfully unmounted the disk image");
}
Ok(output) => {
log::error!(
"Failed to unmount disk image: {:?}",
String::from_utf8_lossy(&output.stderr)
);
}
Err(error) => {
log::error!("Error while trying to unmount disk image: {:?}", error);
}
}
})
.detach();
}
}
@@ -302,10 +310,10 @@ impl AutoUpdater {
// the app after an update, we use `set_restart_path` to run the auto
// update helper instead of the app, so that it can overwrite the app
// and then spawn the new binary.
let quit_subscription = Some(cx.on_app_quit(|_, _| async move {
#[cfg(target_os = "windows")]
finalize_auto_update_on_quit();
}));
#[cfg(target_os = "windows")]
let quit_subscription = Some(cx.on_app_quit(|_, _| finalize_auto_update_on_quit()));
#[cfg(not(target_os = "windows"))]
let quit_subscription = None;
cx.on_app_restart(|this, _| {
this.quit_subscription.take();
@@ -896,6 +904,7 @@ async fn install_release_macos(
// Create an MacOsUnmounter that will be dropped (and thus unmount the disk) when this function exits
let _unmounter = MacOsUnmounter {
mount_path: mount_path.clone(),
background_executor: cx.background_executor(),
};
let output = Command::new("rsync")
@@ -933,11 +942,12 @@ async fn install_release_windows(downloaded_installer: PathBuf) -> Result<Option
let helper_path = std::env::current_exe()?
.parent()
.context("No parent dir for Zed.exe")?
.join("tools\\auto_update_helper.exe");
.join("tools")
.join("auto_update_helper.exe");
Ok(Some(helper_path))
}
pub fn finalize_auto_update_on_quit() {
pub async fn finalize_auto_update_on_quit() {
let Some(installer_path) = std::env::current_exe()
.ok()
.and_then(|p| p.parent().map(|p| p.join("updates")))
@@ -950,12 +960,14 @@ pub fn finalize_auto_update_on_quit() {
if flag_file.exists()
&& let Some(helper) = installer_path
.parent()
.map(|p| p.join("tools\\auto_update_helper.exe"))
.map(|p| p.join("tools").join("auto_update_helper.exe"))
{
let mut command = std::process::Command::new(helper);
let mut command = smol::process::Command::new(helper);
command.arg("--launch");
command.arg("false");
let _ = command.spawn();
if let Ok(mut cmd) = command.spawn() {
_ = cmd.status().await;
}
}
}

View File

@@ -160,6 +160,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();

View File

@@ -111,13 +111,13 @@ impl sum_tree::Item for PendingHunk {
}
impl sum_tree::Summary for DiffHunkSummary {
type Context = text::BufferSnapshot;
type Context<'a> = &'a text::BufferSnapshot;
fn zero(_cx: &Self::Context) -> Self {
fn zero(_cx: Self::Context<'_>) -> Self {
Default::default()
}
fn add_summary(&mut self, other: &Self, buffer: &Self::Context) {
fn add_summary(&mut self, other: &Self, buffer: Self::Context<'_>) {
self.buffer_range.start = self
.buffer_range
.start

View File

@@ -23,8 +23,8 @@ use livekit_client::{self as livekit, AudioStream, TrackSid};
use postage::{sink::Sink, stream::Stream, watch};
use project::Project;
use settings::Settings as _;
use std::{future::Future, mem, rc::Rc, sync::Arc, time::Duration};
use util::{ResultExt, TryFutureExt, post_inc};
use std::{future::Future, mem, rc::Rc, sync::Arc, time::Duration, time::Instant};
use util::{ResultExt, TryFutureExt, paths::PathStyle, post_inc};
pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
@@ -86,6 +86,7 @@ pub struct Room {
room_update_completed_rx: watch::Receiver<Option<()>>,
pending_room_update: Option<Task<()>>,
maintain_connection: Option<Task<Option<()>>>,
created: Instant,
}
impl EventEmitter<Event> for Room {}
@@ -157,6 +158,7 @@ impl Room {
maintain_connection: Some(maintain_connection),
room_update_completed_tx,
room_update_completed_rx,
created: cx.background_executor().now(),
}
}
@@ -827,7 +829,17 @@ impl Room {
},
);
Audio::play_sound(Sound::Joined, cx);
// When joining a room start_room_connection gets
// called but we have already played the join sound.
// Dont play extra sounds over that.
if this.created.elapsed() > Duration::from_millis(100) {
if let proto::ChannelRole::Guest = role {
Audio::play_sound(Sound::GuestJoined, cx);
} else {
Audio::play_sound(Sound::Joined, cx);
}
}
if let Some(livekit_participants) = &livekit_participants
&& let Some(livekit_participant) = livekit_participants
.get(&ParticipantIdentity(user.id.to_string()))
@@ -1163,6 +1175,7 @@ impl Room {
room_id: self.id(),
worktrees: project.read(cx).worktree_metadata_protos(cx),
is_ssh_project: project.read(cx).is_via_remote_server(),
windows_paths: Some(project.read(cx).path_style(cx) == PathStyle::Windows),
});
cx.spawn(async move |this, cx| {

View File

@@ -1,3 +1,4 @@
#![allow(clippy::disallowed_methods, reason = "build scripts are exempt")]
use std::process::Command;
fn main() {

View File

@@ -1,3 +1,7 @@
#![allow(
clippy::disallowed_methods,
reason = "We are not in an async environment, so std::process::Command is fine"
)]
#![cfg_attr(
any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
allow(dead_code)
@@ -139,7 +143,7 @@ fn parse_path_with_position(argument_str: &str) -> anyhow::Result<String> {
}
.with_context(|| format!("parsing as path with position {argument_str}"))?,
};
Ok(canonicalized.to_string(|path| path.to_string_lossy().to_string()))
Ok(canonicalized.to_string(|path| path.to_string_lossy().into_owned()))
}
fn parse_path_in_wsl(source: &str, wsl: &str) -> Result<String> {
@@ -316,12 +320,12 @@ fn main() -> Result<()> {
urls.push(path.to_string());
} else if path == "-" && args.paths_with_position.len() == 1 {
let file = NamedTempFile::new()?;
paths.push(file.path().to_string_lossy().to_string());
paths.push(file.path().to_string_lossy().into_owned());
let (file, _) = file.keep()?;
stdin_tmp_file = Some(file);
} else if let Some(file) = anonymous_fd(path) {
let tmp_file = NamedTempFile::new()?;
paths.push(tmp_file.path().to_string_lossy().to_string());
paths.push(tmp_file.path().to_string_lossy().into_owned());
let (tmp_file, _) = tmp_file.keep()?;
anonymous_fd_tmp_files.push((file, tmp_file));
} else if let Some(wsl) = wsl {

View File

@@ -405,7 +405,7 @@ impl Telemetry {
let mut project_types: HashSet<&str> = HashSet::new();
for (path, _, _) in updated_entries_set.iter() {
let Some(file_name) = path.file_name().and_then(|f| f.to_str()) else {
let Some(file_name) = path.file_name() else {
continue;
};
@@ -601,6 +601,7 @@ mod tests {
use http_client::FakeHttpClient;
use std::collections::HashMap;
use telemetry_events::FlexibleEvent;
use util::rel_path::RelPath;
use worktree::{PathChange, ProjectEntryId, WorktreeId};
#[gpui::test]
@@ -855,12 +856,12 @@ mod tests {
let entries: Vec<_> = file_paths
.into_iter()
.enumerate()
.map(|(i, path)| {
(
Arc::from(std::path::Path::new(path)),
.filter_map(|(i, path)| {
Some((
Arc::from(RelPath::unix(path).ok()?),
ProjectEntryId::from_proto(i as u64 + 1),
PathChange::Added,
)
))
})
.collect();
let updated_entries: UpdatedEntriesSet = Arc::from(entries.as_slice());

View File

@@ -5,6 +5,9 @@ publish.workspace = true
edition.workspace = true
license = "Apache-2.0"
[features]
test-support = []
[lints]
workspace = true

View File

@@ -55,6 +55,9 @@ pub const CLIENT_SUPPORTS_STATUS_MESSAGES_HEADER_NAME: &str =
pub const SERVER_SUPPORTS_STATUS_MESSAGES_HEADER_NAME: &str =
"x-zed-server-supports-status-messages";
/// The name of the header used by the client to indicate that it supports receiving xAI models.
pub const CLIENT_SUPPORTS_X_AI_HEADER_NAME: &str = "x-zed-client-supports-x-ai";
#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum UsageLimit {
@@ -144,6 +147,7 @@ pub enum LanguageModelProvider {
Anthropic,
OpenAi,
Google,
XAi,
}
#[derive(Debug, Clone, Serialize, Deserialize)]

View File

@@ -1,6 +1,11 @@
use chrono::Duration;
use serde::{Deserialize, Serialize};
use std::{ops::Range, path::PathBuf};
use std::{
ops::Range,
path::{Path, PathBuf},
sync::Arc,
};
use strum::EnumIter;
use uuid::Uuid;
use crate::PredictEditsGitInfo;
@@ -10,7 +15,7 @@ use crate::PredictEditsGitInfo;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PredictEditsRequest {
pub excerpt: String,
pub excerpt_path: PathBuf,
pub excerpt_path: Arc<Path>,
/// Within file
pub excerpt_range: Range<usize>,
/// Within `excerpt`
@@ -32,10 +37,48 @@ pub struct PredictEditsRequest {
// Only available to staff
#[serde(default)]
pub debug_info: bool,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub prompt_max_bytes: Option<usize>,
#[serde(default)]
pub prompt_format: PromptFormat,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, EnumIter)]
pub enum PromptFormat {
MarkedExcerpt,
LabeledSections,
/// Prompt format intended for use via zeta_cli
OnlySnippets,
}
impl PromptFormat {
pub const DEFAULT: PromptFormat = PromptFormat::LabeledSections;
}
impl Default for PromptFormat {
fn default() -> Self {
Self::DEFAULT
}
}
impl PromptFormat {
pub fn iter() -> impl Iterator<Item = Self> {
<Self as strum::IntoEnumIterator>::iter()
}
}
impl std::fmt::Display for PromptFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PromptFormat::MarkedExcerpt => write!(f, "Marked Excerpt"),
PromptFormat::LabeledSections => write!(f, "Labeled Sections"),
PromptFormat::OnlySnippets => write!(f, "Only Snippets"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(any(test, feature = "test-support"), derive(PartialEq))]
#[serde(tag = "event")]
pub enum Event {
BufferChange {
@@ -59,7 +102,7 @@ pub struct Signature {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReferencedDeclaration {
pub path: PathBuf,
pub path: Arc<Path>,
pub text: String,
pub text_is_truncated: bool,
/// Range of `text` within file, possibly truncated according to `text_is_truncated`
@@ -69,13 +112,13 @@ pub struct ReferencedDeclaration {
/// Index within `signatures`.
#[serde(skip_serializing_if = "Option::is_none", default)]
pub parent_index: Option<usize>,
pub score_components: ScoreComponents,
pub score_components: DeclarationScoreComponents,
pub signature_score: f32,
pub declaration_score: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScoreComponents {
pub struct DeclarationScoreComponents {
pub is_same_file: bool,
pub is_referenced_nearby: bool,
pub is_referenced_in_breadcrumb: bool,
@@ -85,12 +128,12 @@ pub struct ScoreComponents {
pub reference_line_distance: u32,
pub declaration_line_distance: u32,
pub declaration_line_distance_rank: usize,
pub containing_range_vs_item_jaccard: f32,
pub containing_range_vs_signature_jaccard: f32,
pub excerpt_vs_item_jaccard: f32,
pub excerpt_vs_signature_jaccard: f32,
pub adjacent_vs_item_jaccard: f32,
pub adjacent_vs_signature_jaccard: f32,
pub containing_range_vs_item_weighted_overlap: f32,
pub containing_range_vs_signature_weighted_overlap: f32,
pub excerpt_vs_item_weighted_overlap: f32,
pub excerpt_vs_signature_weighted_overlap: f32,
pub adjacent_vs_item_weighted_overlap: f32,
pub adjacent_vs_signature_weighted_overlap: f32,
}
@@ -117,7 +160,7 @@ pub struct DebugInfo {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Edit {
pub path: PathBuf,
pub path: Arc<Path>,
pub range: Range<usize>,
pub content: String,
}

View File

@@ -1,29 +1,47 @@
//! Zeta2 prompt planning and generation code shared with cloud.
use anyhow::{Result, anyhow};
use cloud_llm_client::predict_edits_v3::{self, Event, ReferencedDeclaration};
use anyhow::{Context as _, Result, anyhow};
use cloud_llm_client::predict_edits_v3::{self, Event, PromptFormat, ReferencedDeclaration};
use indoc::indoc;
use ordered_float::OrderedFloat;
use rustc_hash::{FxHashMap, FxHashSet};
use std::fmt::Write;
use std::sync::Arc;
use std::{cmp::Reverse, collections::BinaryHeap, ops::Range, path::Path};
use strum::{EnumIter, IntoEnumIterator};
pub const DEFAULT_MAX_PROMPT_BYTES: usize = 10 * 1024;
pub const CURSOR_MARKER: &str = "<|user_cursor_is_here|>";
pub const CURSOR_MARKER: &str = "<|cursor_position|>";
/// NOTE: Differs from zed version of constant - includes a newline
pub const EDITABLE_REGION_START_MARKER_WITH_NEWLINE: &str = "<|editable_region_start|>\n";
/// NOTE: Differs from zed version of constant - includes a newline
pub const EDITABLE_REGION_END_MARKER_WITH_NEWLINE: &str = "<|editable_region_end|>\n";
// TODO: use constants for markers?
pub const SYSTEM_PROMPT: &str = indoc! {"
const MARKED_EXCERPT_SYSTEM_PROMPT: &str = indoc! {"
You are a code completion assistant and your task is to analyze user edits and then rewrite an excerpt that the user provides, suggesting the appropriate edits within the excerpt, taking into account the cursor location.
The excerpt to edit will be wrapped in markers <|editable_region_start|> and <|editable_region_end|>. The cursor position is marked with <|user_cursor_is_here|>. Please respond with edited code for that region.
The excerpt to edit will be wrapped in markers <|editable_region_start|> and <|editable_region_end|>. The cursor position is marked with <|cursor_position|>. Please respond with edited code for that region.
Other code is provided for context, and `…` indicates when code has been skipped.
"};
const LABELED_SECTIONS_SYSTEM_PROMPT: &str = indoc! {r#"
You are a code completion assistant and your task is to analyze user edits, and suggest an edit to one of the provided sections of code.
Sections of code are grouped by file and then labeled by `<|section_N|>` (e.g `<|section_8|>`).
The cursor position is marked with `<|cursor_position|>` and it will appear within a special section labeled `<|current_section|>`. Prefer editing the current section until no more changes are needed within it.
Respond ONLY with the name of the section to edit on a single line, followed by all of the code that should replace that section. For example:
<|current_section|>
for i in 0..16 {
println!("{i}");
}
"#};
pub struct PlannedPrompt<'a> {
request: &'a predict_edits_v3::PredictEditsRequest,
/// Snippets to include in the prompt. These may overlap - they are merged / deduplicated in
@@ -32,13 +50,18 @@ pub struct PlannedPrompt<'a> {
budget_used: usize,
}
pub struct PlanOptions {
pub max_bytes: usize,
pub fn system_prompt(format: PromptFormat) -> &'static str {
match format {
PromptFormat::MarkedExcerpt => MARKED_EXCERPT_SYSTEM_PROMPT,
PromptFormat::LabeledSections => LABELED_SECTIONS_SYSTEM_PROMPT,
// only intended for use via zeta_cli
PromptFormat::OnlySnippets => "",
}
}
#[derive(Clone, Debug)]
pub struct PlannedSnippet<'a> {
path: &'a Path,
path: Arc<Path>,
range: Range<usize>,
text: &'a str,
// TODO: Indicate this in the output
@@ -47,18 +70,24 @@ pub struct PlannedSnippet<'a> {
}
#[derive(EnumIter, Clone, Copy, PartialEq, Eq, Hash, Debug, PartialOrd, Ord)]
pub enum SnippetStyle {
pub enum DeclarationStyle {
Signature,
Declaration,
}
#[derive(Clone, Debug)]
pub struct SectionLabels {
pub excerpt_index: usize,
pub section_ranges: Vec<(Arc<Path>, Range<usize>)>,
}
impl<'a> PlannedPrompt<'a> {
/// Greedy one-pass knapsack algorithm to populate the prompt plan. Does the following:
///
/// Initializes a priority queue by populating it with each snippet, finding the SnippetStyle
/// that minimizes `score_density = score / snippet.range(style).len()`. When a "signature"
/// snippet is popped, insert an entry for the "declaration" variant that reflects the cost of
/// upgrade.
/// Initializes a priority queue by populating it with each snippet, finding the
/// DeclarationStyle that minimizes `score_density = score / snippet.range(style).len()`. When a
/// "signature" snippet is popped, insert an entry for the "declaration" variant that reflects
/// the cost of upgrade.
///
/// TODO: Implement an early halting condition. One option might be to have another priority
/// queue where the score is the size, and update it accordingly. Another option might be to
@@ -74,10 +103,7 @@ impl<'a> PlannedPrompt<'a> {
/// signatures may be shared by multiple snippets.
///
/// * Does not include file paths / other text when considering max_bytes.
pub fn populate(
request: &'a predict_edits_v3::PredictEditsRequest,
options: &PlanOptions,
) -> Result<Self> {
pub fn populate(request: &'a predict_edits_v3::PredictEditsRequest) -> Result<Self> {
let mut this = PlannedPrompt {
request,
snippets: Vec::new(),
@@ -91,11 +117,13 @@ impl<'a> PlannedPrompt<'a> {
)?;
this.add_parents(&mut included_parents, additional_parents);
if this.budget_used > options.max_bytes {
let max_bytes = request.prompt_max_bytes.unwrap_or(DEFAULT_MAX_PROMPT_BYTES);
if this.budget_used > max_bytes {
return Err(anyhow!(
"Excerpt + signatures size of {} already exceeds budget of {}",
this.budget_used,
options.max_bytes
max_bytes
));
}
@@ -103,13 +131,13 @@ impl<'a> PlannedPrompt<'a> {
struct QueueEntry {
score_density: OrderedFloat<f32>,
declaration_index: usize,
style: SnippetStyle,
style: DeclarationStyle,
}
// Initialize priority queue with the best score for each snippet.
let mut queue: BinaryHeap<QueueEntry> = BinaryHeap::new();
for (declaration_index, declaration) in request.referenced_declarations.iter().enumerate() {
let (style, score_density) = SnippetStyle::iter()
let (style, score_density) = DeclarationStyle::iter()
.map(|style| {
(
style,
@@ -138,7 +166,7 @@ impl<'a> PlannedPrompt<'a> {
};
let mut additional_bytes = declaration_size(declaration, queue_entry.style);
if this.budget_used + additional_bytes > options.max_bytes {
if this.budget_used + additional_bytes > max_bytes {
continue;
}
@@ -151,14 +179,14 @@ impl<'a> PlannedPrompt<'a> {
.iter()
.map(|(_, snippet)| snippet.text.len())
.sum::<usize>();
if this.budget_used + additional_bytes > options.max_bytes {
if this.budget_used + additional_bytes > max_bytes {
continue;
}
this.budget_used += additional_bytes;
this.add_parents(&mut included_parents, additional_parents);
let planned_snippet = match queue_entry.style {
SnippetStyle::Signature => {
DeclarationStyle::Signature => {
let Some(text) = declaration.text.get(declaration.signature_range.clone())
else {
return Err(anyhow!(
@@ -168,15 +196,15 @@ impl<'a> PlannedPrompt<'a> {
));
};
PlannedSnippet {
path: &declaration.path,
path: declaration.path.clone(),
range: (declaration.signature_range.start + declaration.range.start)
..(declaration.signature_range.end + declaration.range.start),
text,
text_is_truncated: declaration.text_is_truncated,
}
}
SnippetStyle::Declaration => PlannedSnippet {
path: &declaration.path,
DeclarationStyle::Declaration => PlannedSnippet {
path: declaration.path.clone(),
range: declaration.range.clone(),
text: &declaration.text,
text_is_truncated: declaration.text_is_truncated,
@@ -185,11 +213,13 @@ impl<'a> PlannedPrompt<'a> {
this.snippets.push(planned_snippet);
// When a Signature is consumed, insert an entry for Definition style.
if queue_entry.style == SnippetStyle::Signature {
let signature_size = declaration_size(&declaration, SnippetStyle::Signature);
let declaration_size = declaration_size(&declaration, SnippetStyle::Declaration);
let signature_score = declaration_score(&declaration, SnippetStyle::Signature);
let declaration_score = declaration_score(&declaration, SnippetStyle::Declaration);
if queue_entry.style == DeclarationStyle::Signature {
let signature_size = declaration_size(&declaration, DeclarationStyle::Signature);
let declaration_size =
declaration_size(&declaration, DeclarationStyle::Declaration);
let signature_score = declaration_score(&declaration, DeclarationStyle::Signature);
let declaration_score =
declaration_score(&declaration, DeclarationStyle::Declaration);
let score_diff = declaration_score - signature_score;
let size_diff = declaration_size.saturating_sub(signature_size);
@@ -197,7 +227,7 @@ impl<'a> PlannedPrompt<'a> {
queue.push(QueueEntry {
declaration_index: queue_entry.declaration_index,
score_density: OrderedFloat(score_diff / (size_diff as f32)),
style: SnippetStyle::Declaration,
style: DeclarationStyle::Declaration,
});
}
}
@@ -220,7 +250,7 @@ impl<'a> PlannedPrompt<'a> {
fn additional_parent_signatures(
&self,
path: &'a Path,
path: &Arc<Path>,
parent_index: Option<usize>,
included_parents: &FxHashSet<usize>,
) -> Result<Vec<(usize, PlannedSnippet<'a>)>> {
@@ -231,7 +261,7 @@ impl<'a> PlannedPrompt<'a> {
fn additional_parent_signatures_impl(
&self,
path: &'a Path,
path: &Arc<Path>,
parent_index: Option<usize>,
included_parents: &FxHashSet<usize>,
results: &mut Vec<(usize, PlannedSnippet<'a>)>,
@@ -248,7 +278,7 @@ impl<'a> PlannedPrompt<'a> {
results.push((
parent_index,
PlannedSnippet {
path,
path: path.clone(),
range: parent_signature.range.clone(),
text: &parent_signature.text,
text_is_truncated: parent_signature.text_is_truncated,
@@ -265,7 +295,7 @@ impl<'a> PlannedPrompt<'a> {
/// Renders the planned context. Each file starts with "```FILE_PATH\n` and ends with triple
/// backticks, with a newline after each file. Outputs a line with "..." between nonconsecutive
/// chunks.
pub fn to_prompt_string(&self) -> String {
pub fn to_prompt_string(&'a self) -> Result<(String, SectionLabels)> {
let mut file_to_snippets: FxHashMap<&'a std::path::Path, Vec<&PlannedSnippet<'a>>> =
FxHashMap::default();
for snippet in &self.snippets {
@@ -279,14 +309,14 @@ impl<'a> PlannedPrompt<'a> {
let mut file_snippets = Vec::new();
let mut excerpt_file_snippets = Vec::new();
for (file_path, snippets) in file_to_snippets {
if file_path == &self.request.excerpt_path {
if file_path == self.request.excerpt_path.as_ref() {
excerpt_file_snippets = snippets;
} else {
file_snippets.push((file_path, snippets, false));
}
}
let excerpt_snippet = PlannedSnippet {
path: &self.request.excerpt_path,
path: self.request.excerpt_path.clone(),
range: self.request.excerpt_range.clone(),
text: &self.request.excerpt,
text_is_truncated: false,
@@ -294,32 +324,40 @@ impl<'a> PlannedPrompt<'a> {
excerpt_file_snippets.push(&excerpt_snippet);
file_snippets.push((&self.request.excerpt_path, excerpt_file_snippets, true));
let mut excerpt_file_insertions = vec![
(
self.request.excerpt_range.start,
EDITABLE_REGION_START_MARKER_WITH_NEWLINE,
),
(
let mut excerpt_file_insertions = match self.request.prompt_format {
PromptFormat::MarkedExcerpt => vec![
(
self.request.excerpt_range.start,
EDITABLE_REGION_START_MARKER_WITH_NEWLINE,
),
(
self.request.excerpt_range.start + self.request.cursor_offset,
CURSOR_MARKER,
),
(
self.request
.excerpt_range
.end
.saturating_sub(0)
.max(self.request.excerpt_range.start),
EDITABLE_REGION_END_MARKER_WITH_NEWLINE,
),
],
PromptFormat::LabeledSections => vec![(
self.request.excerpt_range.start + self.request.cursor_offset,
CURSOR_MARKER,
),
(
self.request
.excerpt_range
.end
.saturating_sub(0)
.max(self.request.excerpt_range.start),
EDITABLE_REGION_END_MARKER_WITH_NEWLINE,
),
];
)],
PromptFormat::OnlySnippets => vec![],
};
let mut output = String::new();
output.push_str("## User Edits\n\n");
Self::push_events(&mut output, &self.request.events);
let mut prompt = String::new();
prompt.push_str("## User Edits\n\n");
Self::push_events(&mut prompt, &self.request.events);
output.push_str("\n## Code\n\n");
Self::push_file_snippets(&mut output, &mut excerpt_file_insertions, file_snippets);
output
prompt.push_str("\n## Code\n\n");
let section_labels =
self.push_file_snippets(&mut prompt, &mut excerpt_file_insertions, file_snippets)?;
Ok((prompt, section_labels))
}
fn push_events(output: &mut String, events: &[predict_edits_v3::Event]) {
@@ -366,96 +404,128 @@ impl<'a> PlannedPrompt<'a> {
}
fn push_file_snippets(
&self,
output: &mut String,
excerpt_file_insertions: &mut Vec<(usize, &'static str)>,
file_snippets: Vec<(&Path, Vec<&PlannedSnippet>, bool)>,
) {
fn push_excerpt_file_range(
range: Range<usize>,
text: &str,
excerpt_file_insertions: &mut Vec<(usize, &'static str)>,
output: &mut String,
) {
let mut last_offset = range.start;
let mut i = 0;
while i < excerpt_file_insertions.len() {
let (offset, insertion) = &excerpt_file_insertions[i];
let found = *offset >= range.start && *offset <= range.end;
if found {
output.push_str(&text[last_offset - range.start..offset - range.start]);
output.push_str(insertion);
last_offset = *offset;
excerpt_file_insertions.remove(i);
continue;
}
i += 1;
}
output.push_str(&text[last_offset - range.start..]);
}
file_snippets: Vec<(&'a Path, Vec<&'a PlannedSnippet>, bool)>,
) -> Result<SectionLabels> {
let mut section_ranges = Vec::new();
let mut excerpt_index = None;
for (file_path, mut snippets, is_excerpt_file) in file_snippets {
output.push_str(&format!("```{}\n", file_path.display()));
let mut last_included_range: Option<Range<usize>> = None;
snippets.sort_by_key(|s| (s.range.start, Reverse(s.range.end)));
// TODO: What if the snippets get expanded too large to be editable?
let mut current_snippet: Option<(&PlannedSnippet, Range<usize>)> = None;
let mut disjoint_snippets: Vec<(&PlannedSnippet, Range<usize>)> = Vec::new();
for snippet in snippets {
if let Some(last_range) = &last_included_range
&& snippet.range.start < last_range.end
if let Some((_, current_snippet_range)) = current_snippet.as_mut()
&& snippet.range.start < current_snippet_range.end
{
if snippet.range.end <= last_range.end {
continue;
if snippet.range.end > current_snippet_range.end {
current_snippet_range.end = snippet.range.end;
}
// TODO: Should probably also handle case where there is just one char (newline)
// between snippets - assume it's a newline.
let text = &snippet.text[last_range.end - snippet.range.start..];
if is_excerpt_file {
push_excerpt_file_range(
last_range.end..snippet.range.end,
text,
excerpt_file_insertions,
output,
);
} else {
output.push_str(text);
}
last_included_range = Some(last_range.start..snippet.range.end);
continue;
}
if last_included_range.is_some() {
output.push_str("\n");
if let Some(current_snippet) = current_snippet.take() {
disjoint_snippets.push(current_snippet);
}
current_snippet = Some((snippet, snippet.range.clone()));
}
if let Some(current_snippet) = current_snippet.take() {
disjoint_snippets.push(current_snippet);
}
writeln!(output, "```{}", file_path.display()).ok();
let mut skipped_last_snippet = false;
for (snippet, range) in disjoint_snippets {
let section_index = section_ranges.len();
match self.request.prompt_format {
PromptFormat::MarkedExcerpt | PromptFormat::OnlySnippets => {
if range.start > 0 && !skipped_last_snippet {
output.push_str("\n");
}
}
PromptFormat::LabeledSections => {
if is_excerpt_file
&& range.start <= self.request.excerpt_range.start
&& range.end >= self.request.excerpt_range.end
{
writeln!(output, "<|current_section|>").ok();
} else {
writeln!(output, "<|section_{}|>", section_index).ok();
}
}
}
if is_excerpt_file {
push_excerpt_file_range(
snippet.range.clone(),
snippet.text,
excerpt_file_insertions,
output,
);
if self.request.prompt_format == PromptFormat::OnlySnippets {
if range.start >= self.request.excerpt_range.start
&& range.end <= self.request.excerpt_range.end
{
skipped_last_snippet = true;
} else {
skipped_last_snippet = false;
output.push_str(snippet.text);
}
} else {
let mut last_offset = range.start;
let mut i = 0;
while i < excerpt_file_insertions.len() {
let (offset, insertion) = &excerpt_file_insertions[i];
let found = *offset >= range.start && *offset <= range.end;
if found {
excerpt_index = Some(section_index);
output.push_str(
&snippet.text[last_offset - range.start..offset - range.start],
);
output.push_str(insertion);
last_offset = *offset;
excerpt_file_insertions.remove(i);
continue;
}
i += 1;
}
skipped_last_snippet = false;
output.push_str(&snippet.text[last_offset - range.start..]);
}
} else {
skipped_last_snippet = false;
output.push_str(snippet.text);
}
last_included_range = Some(snippet.range.clone());
section_ranges.push((snippet.path.clone(), range));
}
output.push_str("```\n\n");
}
Ok(SectionLabels {
// TODO: Clean this up
excerpt_index: match self.request.prompt_format {
PromptFormat::OnlySnippets => 0,
_ => excerpt_index.context("bug: no snippet found for excerpt")?,
},
section_ranges,
})
}
}
fn declaration_score_density(declaration: &ReferencedDeclaration, style: SnippetStyle) -> f32 {
fn declaration_score_density(declaration: &ReferencedDeclaration, style: DeclarationStyle) -> f32 {
declaration_score(declaration, style) / declaration_size(declaration, style) as f32
}
fn declaration_score(declaration: &ReferencedDeclaration, style: SnippetStyle) -> f32 {
fn declaration_score(declaration: &ReferencedDeclaration, style: DeclarationStyle) -> f32 {
match style {
SnippetStyle::Signature => declaration.signature_score,
SnippetStyle::Declaration => declaration.declaration_score,
DeclarationStyle::Signature => declaration.signature_score,
DeclarationStyle::Declaration => declaration.declaration_score,
}
}
fn declaration_size(declaration: &ReferencedDeclaration, style: SnippetStyle) -> usize {
fn declaration_size(declaration: &ReferencedDeclaration, style: DeclarationStyle) -> usize {
match style {
SnippetStyle::Signature => declaration.signature_range.len(),
SnippetStyle::Declaration => declaration.text.len(),
DeclarationStyle::Signature => declaration.signature_range.len(),
DeclarationStyle::Declaration => declaration.text.len(),
}
}

View File

@@ -61,7 +61,8 @@ CREATE TABLE "projects" (
"host_user_id" INTEGER REFERENCES users (id),
"host_connection_id" INTEGER,
"host_connection_server_id" INTEGER REFERENCES servers (id) ON DELETE CASCADE,
"unregistered" BOOLEAN NOT NULL DEFAULT FALSE
"unregistered" BOOLEAN NOT NULL DEFAULT FALSE,
"windows_paths" BOOLEAN NOT NULL DEFAULT FALSE
);
CREATE INDEX "index_projects_on_host_connection_server_id" ON "projects" ("host_connection_server_id");

View File

@@ -0,0 +1 @@
ALTER TABLE projects ADD COLUMN windows_paths BOOLEAN DEFAULT FALSE;

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