Compare commits

...

96 Commits

Author SHA1 Message Date
David Kleingeld
48914ae8c1 adds conditional cmp removing use of libwebrtc on windows/freebsd
They cant compile livekit yet. This removes microphone and echo cancellation on those platforms however they can not join calls due to the same cause so it does not matter.
2025-09-11 16:21:48 +02:00
David Kleingeld
8542c5f2f8 Documentation and error handing improvements
Co-authored-by: Richard <richard@zed.dev>
2025-09-11 15:56:26 +02:00
David Kleingeld
0b7932d38e More audio settings, limit replay to staff 2025-09-10 12:14:49 +02:00
David Kleingeld
e874f979ce Adds audio replay system, replay action and moves microphone code to audio crate
The replay system stores the last 30 seconds of audio for every call source and the users microphone. Using the `CaptureRecentAudio` action all those tracks gets stored in a tar as separate .wav files. We show notification when starting the save and when its done. With the replay system the microphone code got more tightly bound to the audio crate than the livekit_client crate, so it was moved to audio.
2025-09-10 12:14:49 +02:00
David Kleingeld
28ca0edb06 Adds AsyncApp::try_read_default_global
Similar to try_read_global except it will initialize the global if it was not initialized.
2025-09-10 12:14:49 +02:00
David Kleingeld
d35f1c0b63 Make every sound go through the webrtc APM for echo cancellation
Also adds a inspect_buffer method to rodio sources trough an
extension trait. We use it to pipe everything trough the apm
echo canceller.
2025-09-10 12:14:49 +02:00
David Kleingeld
3720891480 Move apm (webrtc audio processor) to audio crate 2025-09-10 12:14:49 +02:00
David Kleingeld
2d13d4fe3d adds error handling to audio input pipeline 2025-09-10 12:14:49 +02:00
David Kleingeld
219c07c8e0 Adds microphone input through Rodio
This adds an extension trait to enable us apply the apm echo canceller to the microphone input.
2025-09-10 12:13:12 +02:00
Bennet Bo Fenner
4f1634f95c Remove unused semantic_index crate (#37780)
Release Notes:

- N/A
2025-09-08 13:38:31 +00:00
Eduardo Alba
40eec32cb8 markdown_preview: Fix trimming of leading whitespace in Markdown lists (#35750)
Closes #35712

Release Notes:

- Fixed white-space trimming leading to disconnect between list items
and content in markdown previews.

---------

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
2025-09-08 12:37:11 +00:00
张小白
17499453f6 windows: Check required GPU/driver feature StructuredBuffer (#37776)
Check whether the GPU/driver supports the StructuredBuffer feature
required by our shaders. If it doesn’t, log an error and skip that
GPU/driver, so Windows can fall back to the software renderer.

Release Notes:

- N/A
2025-09-08 12:22:57 +00:00
Lukas Wirth
80a4746a46 project: Be explicit about project-searchability for buffers (#37773)
Closes https://github.com/zed-industries/zed/issues/28830

Release Notes:

- Fixed builtin buffers and log views showing up in project search
2025-09-08 11:22:36 +00:00
Jakub Konka
01f5b73e3b cargo: Remove unused -fuse-ld=lld flag from Win config (#37769)
It is unused and generates a warning

```
 LINK : warning LNK4044: unrecognized option '/fuse-ld=lld'; ignored
```

If in the future we want to give `lld-link.exe` a try, we can set

```toml
linker = "lld-link.exe"
```

instead. At the time of writing, my tests have shown that there is no
real difference between `lld-link` and `link` in terms of linking speed.

Release Notes:

- N/A
2025-09-08 10:43:56 +00:00
Lukas Wirth
a0081dd693 project: Consider all worktrees for activation script search (#37764)
Should fix https://github.com/zed-industries/zed/issues/37734

Release Notes:

- Fixed venv not always activating correctly
2025-09-08 10:06:43 +00:00
chbk
f522823988 Highlight shorthand fields in Rust (#37674)
Release Notes:

- Highlight shorthand fields in Rust

| Zed 0.202.7 | With this PR |
| --- | --- |
| <img width="370" height="50" alt="rust-0 202 7"
src="https://github.com/user-attachments/assets/856a4d82-3ad0-4248-ad51-0472a0b6531a"
/> | <img width="370" height="50" alt="rust-pr"
src="https://github.com/user-attachments/assets/25b8e357-8519-4533-9026-3f2874b42ddb"
/> |
2025-09-08 12:32:08 +03:00
Martin Pool
5a8603bebb Install mold from the OS on Debian 13 (Trixie) (#37712)
I ran `scripts/linux` on Debian Trixie 13. It suggested manually
installing Mold, but [mold](http://packages.debian.org/mold) is packaged
on Debian and so we could install it automatically.

The version packaged there seems to work well enough for `cargo t` to
pass, at least.

## Tested

```
; sudo apt remove mold libstdc++-14-dev
... uninstalls them
; ./script/linux
The following NEW packages will be installed:
  build-essential clang clang-19 clang-tools-19 g++ g++-14 g++-14-x86-64-linux-gnu g++-x86-64-linux-gnu libstdc++-14-dev mold
; cargo t
(passes)
```

Release Notes:

- N/A
2025-09-08 12:29:17 +03:00
Martin Pool
abac87c2f8 tests: Fix doctests in crates/component (#37716)
Previously, `cargo test --package component` failed due to missing
imports for a doctest:


```

   Doc-tests component

running 1 test
test crates/component/src/component.rs - Component::description (line 229) ... FAILED

failures:

---- crates/component/src/component.rs - Component::description (line 229) stdout ----
error: cannot find derive macro `Documented` in this scope
 --> crates/component/src/component.rs:231:10
  |
4 | #[derive(Documented)]
  |          ^^^^^^^^^^

error[E0599]: no associated item named `DOCS` found for struct `MyComponent` in the current scope
 --> crates/component/src/component.rs:236:20
  |
5 | struct MyComponent;
  | ------------------ associated item `DOCS` not found for this struct
...
9 |         Some(Self::DOCS)
  |                    ^^^^ associated item not found in `MyComponent`

error: aborting due to 2 previous errors

For more information about this error, try `rustc --explain E0599`.
Couldn't compile the test.

failures:
    crates/component/src/component.rs - Component::description (line 229)

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.29s

error: doctest failed, to rerun pass `-p component --doc`
bobcat ~/src/zed (doctests) 18:33

``` 

This might be unnoticed if you mostly run nextest, as it does not run
doctests.

Release Notes:

- N/A
2025-09-08 12:23:33 +03:00
Jakub Konka
c3d065cecc git: Use self.git_binary_path instead raw git string (#37757)
Release Notes:

- N/A
2025-09-08 11:05:04 +02:00
Remco Smits
e1a5d29972 markdown: Add support for inline HTML img tags inside text (#37264)
Follow-up: #36700

This PR adds basic support for showing images inline inside a text.

As you can see inside the before screenshot, the image was displayed
right below the `Some inline text` text. This was because we didn't
consider the image to be inline with the text (paragraph). Now we do :)

All the test changes are making sure it is not more than 1 element
parsed, instead of only checking for the first parsed element. This
could work out bad when we return more than 1 result.

**Before**
<img width="1717" height="1344" alt="Screenshot 2025-08-31 at 13 49 45"
src="https://github.com/user-attachments/assets/13c5f9dd-0e0a-4e08-b2a6-28e9a4e0cab8"
/>

**After**
<img width="1719" height="1343" alt="Screenshot 2025-08-31 at 13 42 14"
src="https://github.com/user-attachments/assets/bf7aa82f-3743-4fb3-87aa-4a97a550c4d1"
/>


**Code example**:
```markdown
<p>some inline text <img src="https://picsum.photos/200/300" alt="Description of image" style="height: 100px" /> asdjkflsadjfl</p>

# Html Tag
<img src="https://picsum.photos/200/300" alt="Description of image" />

# Html Tag with width and height
<img src="https://picsum.photos/200/300" alt="Description of image" width="100" height="200" />

# Html Tag with style attribute with width and height
<img src="https://picsum.photos/200/300" alt="Description of image" style="width: 100px; height: 200px" />

# Normal Tag
![alt text](https://picsum.photos/200/300)
```

Release Notes:

- Markdown: Added support for inline HTML `img` tags inside paragraphs
2025-09-08 11:49:32 +03:00
HE7086
d342da4e9a docs: Fix typos in language configurations (#37740)
Fixes several typos in the docs. I think there are more but I have only
read what I actually needed :)

Release Notes:

- N/A
2025-09-08 08:44:46 +00:00
Umesh Yadav
7ae8f81d74 language_models: Clear cached credentials when OpenAI and OpenAI Compatible provider api_url change (#37610)
Closes #37093

Also check this: #37099.

So currently in zed for both OpenAI and OpenAI Compatible provider when
the url is changed from settings the api_key stored in the provider
state is not cleared and it is still used. But if you restart zed the
api_key is cleared. Currently zed uses the api_url to store and fetch
the api key from credential provider. The behaviour is not changed
overall, it's just that we have made it consistent it with the zed
restart logic where it re-authenticates and fetches the api_key again. I
have attached the video below to show case before and after of this.

So all in all the problem was we were not re-authenticating the in case
api_url change while zed is still running. Now we trigger a
re-authentication and clear the state in case authentication fails.
 
OpenAI Compatible Provider:

| Before | After |
|--------|--------|
| <video
src="https://github.com/user-attachments/assets/324d2707-ea72-4119-8981-6b596a9f40a3"
/> | <video
src="https://github.com/user-attachments/assets/cc7fdb73-8975-4aaf-a642-809bb03ce319"
/> |

OpenAI Provider:

| Before | After |
|--------|--------|
| <video
src="https://github.com/user-attachments/assets/a1c07d1b-1909-4b49-b33c-fc05123e92e7"
/> | <video
src="https://github.com/user-attachments/assets/d78aeccd-5cd3-4d0c-8b9f-6f98e499d7c8"
/> |

Release Notes:

- Fixed OpenAI and OpenAI Compatible provide API keys being persisted
when changing the API URL setting. Authentication is now properly
revalidated when settings change.

---------

Signed-off-by: Umesh Yadav <git@umesh.dev>
2025-09-08 06:57:16 +02:00
Umesh Yadav
36364b16a0 agent_ui: Clear model selector query on dismiss (#37569)
Closes #36756

| Before | After |
|--------|--------|
| <video src
="https://github.com/user-attachments/assets/1d022ac6-0aea-4e98-a717-9988420c9683"/>
| <video
src="https://github.com/user-attachments/assets/78d19012-1224-4c92-a6c8-47ae4c13ca31"/>
|

Release Notes:

- agent: Clear model selector query on dismiss in agent panel

---------

Signed-off-by: Umesh Yadav <git@umesh.dev>
2025-09-08 06:52:57 +02:00
Umesh Yadav
b35959f4c2 agent_ui: Fix context_server duplication when name is updated (#35403)
Closes #35400

| Before | After |
|--------|--------|
| <video
src="https://github.com/user-attachments/assets/6aae34ca-e022-457a-9b66-47b85c976b23"/>
| <video
src="https://github.com/user-attachments/assets/ae058988-8f70-4605-b537-e045175d2e75"
/> |

Release Notes:

- agent: Fix `context_server` duplication when name is updated

---------

Signed-off-by: Umesh Yadav <git@umesh.dev>
Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
2025-09-08 04:50:26 +00:00
marius851000
9450bcad25 ollama: Properly format tool calls fed back to the model (#34750)
Fix an issue that resulted in Ollama models not being able to not being
able to access the input of the commands they executed (only being able
to access the result).

This properly return the function history as shown in
https://github.com/ollama/ollama/blob/main/docs/api.md#chat-request-with-history-with-tools

Previously, function input where not returned and result where returned
as a "user" role.

Release Notes:

- ollama: Improved format when returning tool results to the models
2025-09-08 04:26:01 +00:00
Liu Jinyi
69bdef38ec editor: Fix inconsistent search behavior for untitled/temporary tabs (#37086)
Closes #37597 

Release Notes:

- N/A

---


## Problem

When using "Tab Switcher: Toggle All", temporary files (untitled buffers
without associated file paths) cannot be searched by their displayed
content. This creates an inconsistent user experience where:

- **UI Display**: Shows dynamic titles based on the first line of
content (up to 40 characters)
- **Search Text**: Only searches for the static text "untitled"

### Example
- A temporary file containing `Hello World` is displayed as "Hello
World" in the tab
- However, searching for "Hello" in Tab Switcher returns no results
- Only searching for "untitled" will find this temporary file

## Root Cause

The issue stems from inconsistent title generation logic between display
and search:

1. **Display Title** (`items.rs:724`): Uses `self.title(cx)` →
`MultiBuffer::title()` → `buffer_content_title()`
- Returns the first line of content (max 40 chars) for temporary files
   
2. **Search Text** (`items.rs:650-656`): Uses `tab_content_text()`
method
   - Returns hardcoded "untitled" for files without paths

## Solution

Modified the `tab_content_text()` method in `crates/editor/src/items.rs`
to use the same logic as the displayed title for consistency:

```rust
fn tab_content_text(&self, detail: usize, cx: &App) -> SharedString {
    if let Some(path) = path_for_buffer(&self.buffer, detail, true, cx) {
        path.to_string_lossy().to_string().into()
    } else {
        // Use the same logic as the displayed title for consistency
        self.buffer.read(cx).title(cx).to_string().into()
    }
}
```
2025-09-07 11:33:17 -07:00
Michael Sloan
0e33a3afe0 zeta: Check whether data collection is allowed for recent edit history (#37680)
Also:

* Adds tests for can_collect_data.
* Temporarily removes collection of diagnostics.

Release Notes:

- Edit Prediction: Fixed a bug where requests were marked eligible for
data collection despite the recent edit history in the request involving
files that may not be open source. The requests affected by this bug
will not be used in training data.
2025-09-07 11:16:49 -06:00
Bruno Taschenbier
76aaf6a8fe Fix docs for tabs.close_position in default.json (#37729)
Minor docs fix.
Seems like 0a4ff2f475 accidentally added
"hidden" to the docs of both – `close_position` and `show_close_button`.

Release Notes:

- N/A

Co-authored-by: tastenbier <>
2025-09-07 17:00:58 +00:00
张小白
0ef7ee172f windows: Remove some unused keys from the keymap (#37722)
AFAIK, we dont handle these keys on Windows.

Release Notes:

- N/A
2025-09-07 06:45:41 +00:00
张小白
29def012a1 windows: Update Windows keymap (#37721)
Pickup the changes from #37009

Release Notes:

- N/A
2025-09-07 06:09:35 +00:00
Smit Barmase
5c30578c49 linux: Fix IME preedit text not showing in Terminal on Wayland (#37701)
Closes https://github.com/zed-industries/zed/issues/37268
 
Release Notes:

- Fixed an issue where IME preedit text was not showing in the Terminal
on Wayland.
2025-09-07 02:01:55 +05:30
Cole Miller
1552afd8bf docs: Use #action throughout configuring-zed.md (#37709)
Release Notes:

- N/A
2025-09-06 19:38:48 +00:00
Kirill Bulatov
e04473dd26 Revert "gpui: Skip test attribute expansion for rust-analyzer (#37611)" (#37705)
This reverts commit 4124bedab7.

With the new annotation, r-a starts to skip the tasks that are marked
with `gpui::test` and when it fully loads, it starts to return
module-only tasks:


https://github.com/user-attachments/assets/5af3e3e4-91b7-4f19-aab0-ed7f186e5f74


Release Notes:

- N/A
2025-09-06 16:51:51 +00:00
Kirill Bulatov
84f166fc85 Tweak word completions more (#37697)
Follow-up of https://github.com/zed-industries/zed/pull/37352
Closes https://github.com/zed-industries/zed/issues/37132

* disabled word completions in the agent panel's editor
* if not disabled, allow to trigger word completions with an action even
if the completions threshold is not reached

Release Notes:

- Fixed word completions appearing in the agent panel's editor and not
appearing when triggered with the action before the completion threshold
is reached
2025-09-06 13:39:21 +00:00
Kirill Bulatov
065518577e Fix the tasks docs (#37699)
Closes https://github.com/zed-industries/zed/issues/37698

Release Notes:

- N/A
2025-09-06 13:37:21 +00:00
Marco Groot
1d828b6ac6 Fix broken link in CONTRIBUTING.md (#37688)
Can see currently the link is dead currently, but this changes fixes
locally


https://github.com/user-attachments/assets/e01d9c47-e91e-4c24-8285-01e3b45583b9


Release Notes:

- N/A
2025-09-06 12:29:34 +02:00
Kirill Bulatov
777ce7cc97 Fixed LSP binary info not being shown in full (#37682)
Follow-up of https://github.com/zed-industries/zed/pull/37083
Closes https://github.com/zed-industries/zed/issues/37677

Release Notes:

- Fixed LSP binary info not being shown in full
2025-09-06 07:37:59 +00:00
Umesh Yadav
1f37fbd051 language_models: Use /models/user for fetching OpenRouter models (#37534)
This PR switches the OpenRouter integration from fetching all models to
fetching only the models specified in the user's account preferences.
This will help improve the experience

**The Problem**

The previous implementation used the `/models` endpoint, which returned
an exhaustive list of all models supported by OpenRouter. This resulted
in a long and cluttered model selection dropdown in Zed, making it
difficult for users to find the models they actually use.

**The Solution**

We now use the `/models/user` endpoint. This API call returns a curated
list based on the models and providers the user has selected in their
[OpenRouter dashboard](https://openrouter.ai/models).

Ref: [OpenRouter API Docs for User-Filtered
Models](https://openrouter.ai/docs/api-reference/list-models-filtered-by-user-provider-preferences)

Release Notes:

- language_models: Support OpenRouter user preferences for available
models
2025-09-06 07:42:15 +02:00
Umesh Yadav
8c9442ad11 language_models: Skip empty delta text content in OpenAI and OpenAI compatible provider (#37626)
Closes #37302 

Related: #37614 

In case of open_ai_compatible providers like Zhipu AI and z.ai they
return empty content along with usage data. below is the example json
captured from z.ai. We now ignore empty content returned by providers
now to avoid this issue where we would return the same empty content
back to provider which would error out.

```
OpenAI Stream Response JSON:
{
  "id": "2025090518465610d80dc21e66426d",
  "created": 1757069216,
  "model": "glm-4.5",
  "choices": [
    {
      "index": 0,
      "finish_reason": "tool_calls",
      "delta": {
        "role": "assistant",
        "content": ""
      }
    }
  ],
  "usage": {
    "prompt_tokens": 7882,
    "completion_tokens": 150,
    "total_tokens": 8032,
    "prompt_tokens_details": {
      "cached_tokens": 7881
    }
  }
}
```

Release Notes:

- Skip empty delta text content in OpenAI and OpenAI compatible provider

Signed-off-by: Umesh Yadav <git@umesh.dev>
2025-09-06 07:16:08 +02:00
Michael Sloan
47a475681f Optimize Chunks::seek when offset is in current chunk (#37659)
Release Notes:

- N/A
2025-09-06 04:22:55 +00:00
Max Brunsfeld
23dc1f5ea4 Disable foreign keys in sqlite when running migrations (#37572)
Closes #37473

### Background

Previously, we enabled foreign keys at all times for our sqlite database
that we use for client-side state.
The problem with this is that In sqlite, `alter table` is somewhat
limited, so for many migrations, you must *recreate* the table: create a
new table called e.g. `workspace__2`, then copy all of the data from
`workspaces` into `workspace__2`, then delete the old `workspaces` table
and rename `workspaces__2` to `workspaces`. The way foreign keys work in
sqlite, when we delete the old table, all of its associated records in
other tables will be deleted due to `on delete cascade` clauses.

Unfortunately, one of the types of associated records that can be
deleted are `editors`, which sometimes store unsaved text. It is very
bad to delete these records, as they are the *only* place that this
unsaved text is stored.

This has already happened multiple times as we have migrated tables as
we develop Zed, but I caused it to happened again in
https://github.com/zed-industries/zed/pull/36714.

### The Fix

The Sqlite docs recommend a multi-step approach to migrations where you:

* disable foreign keys
* start a transaction
* create a new table
* populate the new table with data from the old table
* delete the old table
* rename the new table to the old name
* run a foreign key check
* if it passes, commit the transaction
* enable foreign keys

In this PR, I've adjusted our sqlite migration code path to follow this
pattern more closely. Specifically, we disable foreign key checks before
running migrations, run a foreign key check before committing, and then
enable foreign key checks after the migrations are done.

In addition, I've added a generic query that we run *before* running the
foreign key check that explicitly deletes any rows that have dangling
foreign keys. This way, we avoid failing the migration (and breaking the
app) if a migration deletes data that *does* cause associated records to
need to be deleted.

But now, in the common case where we migrate old data in the new table
and keep the ids, all of the associated data will be preserved.

Release Notes:

- Fixed a bug where workspace state would be lost when upgrading from
Zed 0.201.x. or below.
2025-09-06 01:09:50 +00:00
chbk
a6a111cadd Highlight labels in Go (#37673)
Release Notes:

- Highlight labels in Go

| Zed 0.202.7 | With this PR |
| --- | --- |
| <img width="160" height="50" alt="go-0 202 7"
src="https://github.com/user-attachments/assets/1a1b3b3c-52ae-41e3-a52b-c2a9bb7589d2"
/> | <img width="160" height="50" alt="go-pr"
src="https://github.com/user-attachments/assets/29020b9f-ed03-4298-aa5b-0201a81fd5e6"
/> |
2025-09-06 01:36:36 +02:00
Piotr Osiewicz
6a7b84eb87 toolchains: Allow users to provide custom paths to toolchains (#37009)
- **toolchains: Add new state to toolchain selector**
- **Use toolchain term for Add Toolchain button**
- **Hoist out a meta function for toolchain listers**

Closes #27332

Release Notes:

- python: Users can now specify a custom path to their virtual
environment from within the picker.

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-09-06 00:47:39 +02:00
Nia
59bdbf5a5d Various fixups to unsafe code (#37651)
A collection of fixups of possibly-unsound code and removing some small
useless writes.

Release Notes:

- N/A
2025-09-06 00:27:14 +02:00
Remco Smits
64b6e8ba0f debugger: Fix allow showing more than 1 compact session item (#37036)
Closes #36978

This PR fixes an issue that we would only show the first `root -> child`
session in compact mode, but the session that came after it, we would
only show the child session label instead of also adding the parent
label due to compact mode.

## Before
<img width="348" height="173" alt="Screenshot 2025-08-27 at 22 18 39"
src="https://github.com/user-attachments/assets/ad6afd3a-196d-497f-812a-00698676ee90"
/>

## After
<img width="563" height="211" alt="Screenshot 2025-08-27 at 21 57 16"
src="https://github.com/user-attachments/assets/a953ef2a-a796-4160-b868-96e96f81c858"
/>

With 3 parent + child sessions and one parent session only.
<img width="484" height="223" alt="Screenshot 2025-08-27 at 22 22 13"
src="https://github.com/user-attachments/assets/a26f79a4-63a5-43d0-a714-d62cb1995e6e"
/>

cc @cole-miller I know we hacked on this some while ago, so figured you
might be the best guy to ask for a review.

Release Notes:

- Debugger: Fix to allow showing more than 1 compact session item

---------

Co-authored-by: Anthony <anthony@zed.dev>
2025-09-05 21:35:28 +00:00
morgankrey
236b3e546e Update Link (#37671)
Documentation fix

Release Notes:

- N/A
2025-09-05 14:34:13 -07:00
Anthony Eid
ea363466aa Fix attach modal showing local processes in SSH sessions (#37608)
Closes #37520

This change makes the attach modal load processes from the remote server
when connecting via SSH, rather than showing local processes from the
client machine.

This works by using the new GetProcessesRequest RPC message to allow
downstream clients to get the correct processes to display. It also only
works with downstream ssh clients because the message handler is only
registered on headless projects.

Release Notes:

- debugger: Fix bug where SSH attach modal showed local processes
instead of processes from the server
2025-09-05 17:03:42 -04:00
Smit Barmase
c45177e296 editor: Fix fold placeholder hover width smaller than marker (#37663)
Bug:
<img width="196" height="95" alt="Screenshot 2025-09-06 at 1 21 39 AM"
src="https://github.com/user-attachments/assets/66ec0fc9-961e-4289-bd75-68b24dad485e"
/>

The fold marker we use, `⋯`, isn’t rendered at the same size as the
editor’s font. Notice how the fold marker appears larger than the same
character typed directly in the editor buffer.

<img width="146" height="82" alt="image"
src="https://github.com/user-attachments/assets/a059d221-6b55-4cf9-bc1e-898ff5444006"
/>

When we shape the line, we use the editor’s font size, and it ends up
determining the element’s width. To fix this, we should treat the
ellipsis as a UI element rather than a buffer character, since current
visual size looks good to me.

<img width="196" height="95" alt="Screenshot 2025-09-06 at 1 29 28 AM"
src="https://github.com/user-attachments/assets/1b766d46-00ab-40c7-b98a-95ea2d4b29bf"
/>

Release Notes:

- Fixed an issue where the fold placeholder’s hover area was smaller
than the marker.
2025-09-06 02:05:42 +05:30
Finn Evers
45fa034107 Restore notification panel settings (#37661)
Follow-up to https://github.com/zed-industries/zed/pull/37489

Notification panel settings were always missing the content, hence this
PR adds it. After #37489, the use of the same content twice broke
things, which currently makes the notification panel non-configurable on
Nightly. This PR fixes this.

There once was an issue about the documentation for the panel being
wrong as well. However, I was just unable to find that sadly.

Release Notes:

- N/A
2025-09-05 21:50:51 +02:00
Conrad Irwin
1c5c8552f2 Show actual error in InvalidBufferView (#37657)
Release Notes:

- Update error view to show the error
2025-09-05 12:03:26 -07:00
Conrad Irwin
5d374193bb Add terminal::Toggle (#37585)
Co-Authored-By: Brandan <b5@n0.computer>

Release Notes:

- Added a new action `terminal::Toggle` that is by default bound to
'ctrl-\`'. This copies the default behaviour from VSCode and Jetbrains
where the terminal opens and closes correctly. If you'd like the old
behaviour you can rebind 'ctrl-\`' to `terminal::ToggleFocus`

Co-authored-by: Brandan <b5@n0.computer>
2025-09-05 17:34:39 +00:00
Dino
b65fb06264 editor: Fix text manipulation on line mode selections (#37646)
This commit updates the implementation for
`editor::Editor.manipulate_text` to use
`editor::selections_collection::SelectionsCollection.all_adjusted`
instead of `editor::selections_collection::SelectionsCollection.all`, as
the former takes into account the selection's `line_mode`, fixing the
issue where, if an user was in vim's visual line mode, running the
`editor: convert to upper case` command would not work as expected.

Closes #36953 

Release Notes:

- Fixed bug where using the editor's convert case commands while in
vim's Visual Line mode would not work as expected
2025-09-05 10:12:51 -07:00
Matin Aniss
b3405c3bd1 Add line ending selector (#35392)
Partially addresses this issue #5294

Adds a selector between `LF` and `CRLF` for the buffer's line endings,
the checkmark denotes the currently selected line ending.

Selector
<img width="487" height="66" alt="image"
src="https://github.com/user-attachments/assets/13f2480f-4d2d-4afe-adf5-385aeb421393"
/>

Release Notes:

- Added line ending selector.

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-09-05 09:52:57 -07:00
Peter Tripp
638320b21e Improve macOS version information in telemetry (#37185)
macOS versions are currently reported as `macOS 26.0.0`.
But this makes it impossible to differentiate amongst macOS Beta
releases which have the same version number (`X.0.0`) but are different
builds.

This PR adds build number info to `os_version` for macOS Betas and
[Rapid Security Response](https://support.apple.com/en-us/102657)
release that have identical version numbers to stable release, but have
different builds numbers. We can differentiate them because the build
numbers end with a letter.

| Version | Before | After |
| - | - | - | 
| macOS Sonoma 14.7.8 | 14.7.8 | 14.7.8 |
| macOS Sequoia 15.6.1 | 15.6.1 | 15.6.1 |
| mcOS Ventura 13.3.1 | 13.3.1 | 13.3.1 |
| macOS Ventura 13.3.1 (a) |  13.3.1 | 13.3.1 (Build 22E772610a) |
| macOS Tahoe 26.0.0 (Beta1) | 26.0.0 | 26.0.0 (Build 25A5316a) |
| macOS Tahoe 26.0.0 (Beta5) | 26.0.0 | 26.0.0 (Build 25A5349a) | 

This should cause minimal telemetry changes and only impacting a macOS
betas and a couple specific older macOS versions, but will allow
differentiation between macOS beta releases in GitHub issues.

Alternatives:
1. Leave as-is (can't differentiate between macOS beta builds)
2. Always include build number info (impacts telemetry; more consistent
going forward; differentiates non-final Release Candidates which don't
include a trailing letter)

I couldn't find a cocoa method to retrieve macOS build number, so I
switched dependencies from `cocoa` to `objc2-foundation` in the client
crate. We already depend upon this crate as a dependency of
`blade-graphics` so I matched the features of that and so workspace-hack
doesn't change.

1ebc69a447/tooling/workspace-hack/Cargo.toml (L355)

Release Notes:

- N/A
2025-09-05 12:40:47 -04:00
张小白
91ab0636ec windows: Make sure zed.sh using the correct line ending (#37650)
This got missed in the changes from #37631

Release Notes:

- N/A
2025-09-05 16:25:55 +00:00
Yacine Hmito
fb6cc8794f Fix typo in development docs for macOS (#37607)
Release Notes:

- N/A
2025-09-05 15:56:40 +00:00
Jakub Konka
3d37611b6f cli: Rename script zed-wsl to zed, and enable on non-WSL (#37631)
Closes #23026

With this hotfix, git committing from the built-in Zed terminal (well,
PowerShell), now works.

Release Notes:

- N/A
2025-09-05 17:43:39 +02:00
Peter Tripp
360e372b57 linux: Restore ctrl-escape to keymap (#37636)
Closes: https://github.com/zed-industries/zed/issues/37628
Follow-up to: https://github.com/zed-industries/zed/pull/36712

Release Notes:

- linux: Fix for ctrl-escape not escaping the tab switcher.
2025-09-05 11:09:32 -04:00
Isaac Hales
74e8afe9a8 Fix logic for default values for task variables (#37588)
This is a small fix for default values in task variables. The
[documentation](https://zed.dev/docs/tasks) states

> You can also use verbose syntax that allows specifying a default if a
given variable is not available: ${ZED_FILE:default_value}

I found, however, that this doesn't actually work. Instead, the Zed
variable and the default value are just appended in the output. For
example, if I run a task `echo ${ZED_ROW:100}` the result I get is
`447:100` (in this case it should just be `447`).

This PR fixes that. I also added a new test case for handling default
values.
I also tested the fix in a dev build and it seems to work.

There are no UI adjustments.

AI disclosure: I used Claude Code to write the code, including the fix
and the tests.

This is actually my first open-source PR ever, so if I did something
wrong, I'd appreciate any tips and I'll make it right!


Release Notes:

- Fixed task variable substitution always appending the default
2025-09-05 14:57:58 +00:00
Finn Evers
e30f45cf64 Syntax tree view improvements (#37570)
In an effort to improve the experience while developing extensions and
improving themes, this PR updates the syntax tree views behavior
slightly.

Before, the view would always update to the current active editor whilst
being used. This was quite painful for improving extension scheme files,
as you would always have to change back and forth between editors to
have a view at the relevant syntax tree.

With this PR, the syntax tree view will now stay attached to the editor
it was opened in, similar to preview views. Once the view is shown, the
`UseActiveEditor` will become available in the command palette and
enable the user to update the view to the last focused editor. On file
close, the view will also be updated accordingly.



https://github.com/user-attachments/assets/922075e5-9da0-4c1d-9e1a-51e024bf41ea

A button is also shown whenever switching is possible.

Futhermore, improved the empty state of the view.

Lastly, a drive-by cleanup of the `show_action_types` method so there is
no need to call `iter()` when calling the method.


Release Notes:

- The syntax tree view will now stay attached to the buffer it was
opened in, similar to the Markdown preview. Use the `UseActiveEditor`
action when the view is shown to change it to the last focused editor.
2025-09-05 14:22:32 +02:00
Jakub Konka
16c4fd4fc5 gpui: move Option -> Result conversion out of closure in App::update_window_id (#37624)
Doesn't fix anything, but it seems that we do not need to assert and
convert into an error until after the closure run to completion,
especially since this is the only error we throw.

Release Notes:

- N/A
2025-09-05 13:19:57 +02:00
Lukas Wirth
ec58adca13 languages: Invoke conda activate in conda environments (#37627)
This isn't quite right, but using the env manager path causes conda to
scream and I am not yet sure why, either way this is an improvement over
the status quo

Release Notes:

- N/A\
2025-09-05 11:16:15 +00:00
Lukas Wirth
bed358718b agent_ui: Fix index panic in SlashCommandCompletion::try_parse (#37612)
Release Notes:

- N/A
2025-09-05 07:56:53 +00:00
Lukas Wirth
4124bedab7 gpui: Skip test attribute expansion for rust-analyzer (#37611)
The `test` attribute doesn't really matter to rust-analyzer, so we can
make use of its cfg to have it think its just the standard test
attribute which should make rust-analyzer slightly less resource
intensive in zed. It also should prevent some IDE features from possibly
failing within tests.

Notably this has no effect outside of this repo, as the `rust-analyzer`
cfg only takes effect on workspace member crates.

Ideally we'd use the ignored proc macro config here but rust-analyzer
still doesn't have toml configs working unfortunately.

Release Notes:

- N/A
2025-09-05 06:54:08 +00:00
Smit Barmase
57c6dbd71e linux: Fix IME positioning on scaled display on Wayland (#37600)
Removes IME bounds scaling on Wayland since it uses logical pixels,
unlike X11. We now scale only on X11. Windows and macOS don’t use these
bounds for IME anyway.

Release Notes:

- Fixed an issue where the IME popover could appear outside the window
or fail to show on Wayland.
2025-09-05 09:10:50 +05:30
Michael Sloan
fded3fbcdb zeta: Scope edit prediction event history to current project (#37595)
This change also causes Zeta to not do anything for editors that are not
associated with a project. In practice, this shouldn't affect any
behavior - those editors shouldn't have edit predictions anyway.

Release Notes:

- Edit Prediction: Requests no longer include recent edits from other
projects (other Zed windows).
2025-09-05 01:15:59 +00:00
Michael Sloan
a660527036 Make entry_for_path return a reference instead of cloning (#37591)
Release Notes:

- N/A
2025-09-05 00:26:37 +00:00
Anthony Eid
0cb8a8983c settings ui: Improve setting proc macro and add scroll to UI (#37581)
This PR improves the settings_ui proc macro by taking into account more
serde attributes
1. rename_all
2. rename
3. flatten

We also pass field documentation to the UI layer now too. This allows ui
elements to have more information like the switch field description.

We got the scrollbar working and started getting language settings to
show up.

Release Notes:

- N/A

---------

Co-authored-by: Ben Kunkle <ben@zed.dev>
2025-09-04 22:30:48 +00:00
Cole Miller
c7902478c1 acp: Pass project environment to external agent servers (#37568)
Closes #37469 

Release Notes:

- agent: The project shell environment is now passed to external agent
processes.

Co-authored-by: Richard Feldman <oss@rtfeldman.com>
Co-authored-by: Nia Espera <nia-e@haecceity.cc>
2025-09-04 18:16:25 -04:00
morgankrey
3c0183fa5e Extraneous backtick (#37576)
Release Notes:

- N/A
2025-09-04 16:14:57 -05:00
morgankrey
e982cb824a docs: Claude Authentication (#37573)
Release Notes:

- N/A
2025-09-04 15:57:00 -05:00
Marshall Bowers
1b865a60f8 snippets: Bump to v0.0.6 (#37567)
This PR bumps the snippets extension to v0.0.6.

Changes:

- https://github.com/zed-industries/zed/pull/37565

Release Notes:

- N/A
2025-09-04 20:08:49 +00:00
Marshall Bowers
4c32d5bf13 snippets: Disable feature_paths by default (#37565)
This PR updates the default configuration of the `snippets` extension to
disable suggesting paths (`feature_paths`).

If users want to enable it, it can be done via the settings:

```json
{
  "lsp": {
    "snippet-completion-server": {
      "settings": {
        "feature_paths": true
      }
    }
  }
}
```

Release Notes:

- N/A
2025-09-04 19:35:48 +00:00
Kirill Bulatov
ccae033d85 Make fallback open picker more intuitive (#37564)
Closes https://github.com/zed-industries/zed/issues/34991

Before, the picker did not allow to open the current directory that was
just completed:

<img width="553" height="354" alt="image"
src="https://github.com/user-attachments/assets/e77793c8-763e-416f-9728-18d5a39b467f"
/>

pressing `enter` here would open `assets`; pressing `tab` would append
the `assets/` segment to the query.
Only backspace, removing `/` would allow to open the current directory.

After:
<img width="574" height="349" alt="image"
src="https://github.com/user-attachments/assets/bdbb3e23-7c7a-4e12-8092-51a6a0ea9f87"
/>

The first item is now a placeholder for opening the current directory
with `enter`.
Any time a fuzzy query is appended, the placeholder goes away; `tab`
selects the entry below the placeholder.

Release Notes:

- Made fallback open picker more intuitive

---------

Co-authored-by: Peter Tripp <petertripp@gmail.com>
Co-authored-by: David Kleingeld <davidsk@zed.dev>
2025-09-04 19:34:23 +00:00
Marshall Bowers
c2fa9d7981 docs: Add configuration example for simple-completion-language-server (#37566)
This PR adds a configuration example for the
`simple-completion-language-server`.

We show the user how to re-enable the `feature_paths` option, as we're
now disabling it by default
(https://github.com/zed-industries/zed/pull/37565).

Release Notes:

- N/A
2025-09-04 19:25:52 +00:00
Anthony Eid
5f03202b5c settings ui: Create settings key trait (#37489)
This PR separates out the associated constant `KEY` from the `Settings`
trait into a new trait `SettingsKey`. This allows for the key trait to
be derived using attributes to specify the path so that the new
`SettingsUi` derive macro can use the same attributes to determine top
level settings paths thereby removing the need to duplicate the path in
both `Settings::KEY` and `#[settings_ui(path = "...")]`

Co-authored-by: Ben Kunkle <ben@zed.dev>

Release Notes:

- N/A

---------

Co-authored-by: Ben Kunkle <ben@zed.dev>
2025-09-04 15:19:02 -04:00
Kirill Bulatov
223fda2fe2 Make remote projects to sync in local user settings (#37560)
Closes https://github.com/zed-industries/zed/issues/20024
Closes https://github.com/zed-industries/zed/issues/23489



https://github.com/user-attachments/assets/6466e0c1-4188-4980-8bb6-52ef6e7591c9


Release Notes:

- Made remote projects to sync in local user settings
2025-09-04 19:05:21 +00:00
Marshall Bowers
a85946eba8 docs: Update TOML docs (#37561)
This PR updates the TOML docs to remove references to Taplo and suggest
the Tombi extension for users wanting language server support.

Relates to https://github.com/zed-industries/zed/issues/36766.

Release Notes:

- N/A
2025-09-04 18:54:32 +00:00
Cole Miller
9d94358971 acp: Keep diff editors in sync with AgentFontSize global (#37559)
Release Notes:

- agent: Fixed `cmd-+` and `cmd--` not affecting the font size of diffs.
2025-09-04 18:33:56 +00:00
Marshall Bowers
9e11105483 toml: Extract to zed-extensions/toml repository (#37558)
This PR extracts the TOML extension to the
[zed-extensions/toml](https://github.com/zed-extensions/toml)
repository.

Release Notes:

- N/A
2025-09-04 18:07:50 +00:00
Anthony Eid
caebd0cc4d debugger: Fix stack frame filter crash (#37555)
The crash was caused by not accounting for the fact that a range of
collapse frames only counts as one entry. Causing the filter indices to
overshoot for indices after collapse frames (it was counting all
collapse frames instead of just one).

The test missed this because it all happened in one `cx.update` closure
and didn't render the stack frame list when the filter was applied. The
test has been updated to account for this.


Release Notes:

- N/A

Co-authored-by: Cole Miller <cole@zed.dev>
2025-09-04 17:55:20 +00:00
Marshall Bowers
6e2922367c Use full SHA for blade dependency (#37554)
In https://github.com/zed-industries/zed/pull/37516 we updated the
`blade` dependency, but used a short SHA.

No reason to not use the full SHA.

Release Notes:

- N/A
2025-09-04 17:41:47 +00:00
Jiqing Yang
25ee9b1013 Fix Wayland crash on AMD GPUs by updating Blade (#37516)
Updates blade-graphics from e0ec4e7 to bfa594e to fix GPU crashes on
Wayland with AMD graphics cards.

The crash was caused by incorrect BLAS scratch buffer alignment - the
old version hardcoded 256-byte alignment, but AMD GPUs require different
alignment values. The newer Blade version uses the GPU's actual
alignment requirements instead of hardcoding.

Closes #37448

Release Notes:

- Migrate to newer version of Blade upstream
2025-09-04 17:21:44 +00:00
Ben Brandt
0870a1fe80 acp: Don't share API key with Anthropic provider (#37543)
Since Claude Code has it's own preferred method of grabbing API keys, we
don't want to reuse this one.

Release Notes:

- acp: Don't share Anthropic API key from the Anthropic provider to
allow default Claude Code login options

---------

Co-authored-by: Agus Zubiaga <agus@zed.dev>
2025-09-04 16:01:50 +00:00
Lukas Wirth
e37efc1e9b diagnostics: Fix diagnostics pane clearing up too eagerly on typing (#37546)
Closes https://github.com/zed-industries/zed/issues/30494

Release Notes:

- Fixed diagnostics pane closing buffers too eagerly when typing inside
it
2025-09-04 15:30:23 +00:00
Nathan Sobo
1ae326432e Extract a scheduler crate from GPUI to enable unified integration testing of client and server code (#37326)
Extracts and cleans up GPUI's scheduler code into a new `scheduler`
crate, making it pluggable by external runtimes. This will enable
deterministic integration testing with cloud components by providing a
unified test scheduler across Zed and backend code. In Zed, it will
replace the existing GPUI scheduler for consistent async task management
across platforms.

## Changes

- **Core Implementation**: `TestScheduler` with seed-based
randomization, session tracking (`SessionId`), and foreground/background
task separation for reproducible testing.
- **Executors**: `ForegroundExecutor` (!Send, thread-local) and
`BackgroundExecutor` (Send, with blocking/timeout support) as
GPUI-compatible wrappers.
- **Clock and Timer**: Controllable `TestClock` and future-based `Timer`
for time-sensitive tests.
- **Testing APIs**: `once()`, `with_seed()`, and `many()` methods for
configurable test runs.
- **Dependencies**: Added `async-task`, `chrono`, `futures`, etc., with
updates to `Cargo.toml` and lock file.

## Benefits

- **Integration Testing**: Facilitates reliable async tests involving
cloud sessions, reducing flakiness via deterministic execution.
- **Pluggability**: Trait-based design (`Scheduler`) allows easy
integration into non-GPUI runtimes while maintaining GPUI compatibility.
- **Cleanup**: Refactors GPUI scheduler logic for clarity, correctness
(no `unwrap()`, proper error handling), and extensibility.

Follows Rust guidelines; run `./script/clippy` for verification.

- [x] Define and test a core scheduler that we think can power our cloud
code and GPUI
- [ ] Replace GPUI's scheduler


Release Notes:

- N/A

---------

Co-authored-by: Antonio Scandurra <me@as-cii.com>
2025-09-04 17:14:53 +02:00
张小白
a05f86f97b windows: Don't log error when RedrawWindow (#37542)
Release Notes:

- N/A
2025-09-04 14:47:17 +00:00
Marshall Bowers
473bbd78cc onboarding: Fix typos in comments (#37541)
This PR fixes some grammatical typos in some comments in the
`onboarding` crate.

Release Notes:

- N/A
2025-09-04 13:46:40 +00:00
张小白
28c78d2d85 windows: Keep just one copy of GPU instance (#37445)
Now we only keep a single copy of the GPU device. The GPU lost handling
got broken after #35376, but it’s properly handled again now.

Release Notes:

- N/A
2025-09-04 21:31:12 +08:00
Lukas Wirth
fca44f89c1 languages: Allow installing pre-release of rust-analyzer and clangd (#37530)
Release Notes:

- Added lsp binary config to allow fetching nightly rust-analyzer and
clangd releases
2025-09-04 09:22:19 +00:00
Mitch (a.k.a Voz)
b7ad20773c worktree: Create parent directories on rename (#37437)
Closes https://github.com/zed-industries/zed/issues/37357

Release Notes:

- Allow creating sub-directories when renaming a file in file finder

---------

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2025-09-04 08:25:47 +00:00
Finn Evers
aa1629b544 Remove some unused events (#37498)
This PR cleans up some emitted events around the codebase. These events
are either never emitted or never listened for.

It seems better to re-implement these at some point should they again be
needed - this ensures that they will actually be fired in the cases
where they are needed as opposed to being there and getting unreliable
and stale (which is already the case for the majority of the events
removed here).

Lastly, this ensures the `CapabilitiesChanged` event is not fired too
often.

Release Notes:

- N/A
2025-09-04 09:09:28 +02:00
James Tucker
69a5c45672 gpui: Fix out-of-bounds node indices in dispatch_path (#37252)
Observed in a somewhat regular startup crash on Windows at head (~50% of
launches in release mode).

Closes #37212

Release Notes:

- N/A
2025-09-03 23:18:23 -07:00
沈瑗杰
d0aaf04673 Change DeepSeek max token count to 128k (#36864)
https://api-docs.deepseek.com/zh-cn/news/news250821

Now the official API supports 128k token content

and have modify the name to v3.1/v3.1 thinking

Release Notes:

- N/A

---------

Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
2025-09-04 05:51:48 +00:00
Francis
d677c98f43 agent2: Use inline enums in now and edit_file tools JSON schema (#37397)
Added schemars annotations to generate inline enums instead of
references ($ref) in the JSON schema passed to LLMs.

Concerns :
- "timezeone" parameter for "now" tool function
- "mode" parameter for "edit_file" tool function

Should be the same for futures tools/functions enums. This is easier for
LLMs to understand the schema since many of them don't use JSON
references correctly.

Tested with :
- local GPT-OSS-120b with llama.cpp server (openai compatible)
- remote Claude Sonnet 4.0 with Zed pro subscription

Thanks in advance for the merge.
(notice this is my first PR ever on Github, I hope I'm doing things
well, please let me know if you have any comment - edit: just noticed my
username/email were not correctly setup on my local git, sorry, it's
been 5 years I've not used git)

Closes #37389

Release Notes:

- agent: Improve "now" and "edit_file" tool schemas to work with more
models.
2025-09-04 05:39:55 +00:00
Ben Brandt
ce362864db docs: Update OpenAI-compatible provider config format (#37517)
The example was still showing how we used to setup openai compatible
providers, but that format should only be used for changing the url for
your actual OpenAI provider.

If you are doing a compatible provider, it should be using the new
format.

Closes #37093

Release Notes:

- N/A
2025-09-04 04:39:06 +00:00
322 changed files with 9231 additions and 7694 deletions

View File

@@ -19,8 +19,6 @@ rustflags = [
"windows_slim_errors", # This cfg will reduce the size of `windows::core::Error` from 16 bytes to 4 bytes
"-C",
"target-feature=+crt-static", # This fixes the linking issue when compiling livekit on Windows
"-C",
"link-arg=-fuse-ld=lld",
]
[env]

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" },
{ name = "rodio", git = "https://github.com/RustAudio/rodio", branch = "better_wav_output"},
]
[final-excludes]
@@ -41,5 +41,4 @@ workspace-members = [
"slash_commands_example",
"zed_snippets",
"zed_test_extension",
"zed_toml",
]

2
.gitattributes vendored
View File

@@ -2,4 +2,4 @@
*.json linguist-language=JSON-with-Comments
# Ensure the WSL script always has LF line endings, even on Windows
crates/zed/resources/windows/zed-wsl text eol=lf
crates/zed/resources/windows/zed.sh text eol=lf

13
.rules
View File

@@ -12,6 +12,19 @@
- Example: avoid `let _ = client.request(...).await?;` - use `client.request(...).await?;` instead
* When implementing async operations that may fail, ensure errors propagate to the UI layer so users get meaningful feedback.
* Never create files with `mod.rs` paths - prefer `src/some_module.rs` instead of `src/some_module/mod.rs`.
* When creating new crates, prefer specifying the library root path in `Cargo.toml` using `[lib] path = "...rs"` instead of the default `lib.rs`, to maintain consistent and descriptive naming (e.g., `gpui.rs` or `main.rs`).
* Avoid creative additions unless explicitly requested
* Use full words for variable names (no abbreviations like "q" for "queue")
* Use variable shadowing to scope clones in async contexts for clarity, minimizing the lifetime of borrowed references.
Example:
```rust
executor.spawn({
let task_ran = task_ran.clone();
async move {
*task_ran.borrow_mut() = true;
}
});
```
# GPUI

View File

@@ -65,7 +65,7 @@ If you would like to add a new icon to the Zed icon theme, [open a Discussion](h
## Bird's-eye view of Zed
We suggest you keep the [zed glossary](docs/src/development/GLOSSARY.md) at your side when starting out. It lists and explains some of the structures and terms you will see throughout the codebase.
We suggest you keep the [zed glossary](docs/src/development/glossary.md) at your side when starting out. It lists and explains some of the structures and terms you will see throughout the codebase.
Zed is made up of several smaller crates - let's go over those you're most likely to interact with:

322
Cargo.lock generated
View File

@@ -26,7 +26,7 @@ dependencies = [
"portable-pty",
"project",
"prompt_store",
"rand 0.8.5",
"rand 0.9.1",
"serde",
"serde_json",
"settings",
@@ -79,7 +79,7 @@ dependencies = [
"log",
"pretty_assertions",
"project",
"rand 0.8.5",
"rand 0.9.1",
"serde_json",
"settings",
"text",
@@ -172,7 +172,7 @@ dependencies = [
"pretty_assertions",
"project",
"prompt_store",
"rand 0.8.5",
"rand 0.9.1",
"ref-cast",
"rope",
"schemars",
@@ -408,7 +408,7 @@ dependencies = [
"project",
"prompt_store",
"proto",
"rand 0.8.5",
"rand 0.9.1",
"release_channel",
"rope",
"rules_library",
@@ -834,7 +834,7 @@ dependencies = [
"project",
"prompt_store",
"proto",
"rand 0.8.5",
"rand 0.9.1",
"regex",
"rpc",
"serde",
@@ -933,7 +933,7 @@ dependencies = [
"parking_lot",
"pretty_assertions",
"project",
"rand 0.8.5",
"rand 0.9.1",
"regex",
"serde",
"serde_json",
@@ -985,7 +985,7 @@ dependencies = [
"pretty_assertions",
"project",
"prompt_store",
"rand 0.8.5",
"rand 0.9.1",
"regex",
"reqwest_client",
"rust-embed",
@@ -1385,12 +1385,19 @@ name = "audio"
version = "0.1.0"
dependencies = [
"anyhow",
"async-tar",
"collections",
"crossbeam",
"gpui",
"libwebrtc",
"log",
"parking_lot",
"rodio",
"schemars",
"serde",
"settings",
"smol",
"thiserror 2.0.12",
"util",
"workspace-hack",
]
@@ -2291,7 +2298,7 @@ dependencies = [
[[package]]
name = "blade-graphics"
version = "0.6.0"
source = "git+https://github.com/kvark/blade?rev=e0ec4e720957edd51b945b64dd85605ea54bcfe5#e0ec4e720957edd51b945b64dd85605ea54bcfe5"
source = "git+https://github.com/kvark/blade?rev=bfa594ea697d4b6326ea29f747525c85ecf933b9#bfa594ea697d4b6326ea29f747525c85ecf933b9"
dependencies = [
"ash",
"ash-window",
@@ -2324,7 +2331,7 @@ dependencies = [
[[package]]
name = "blade-macros"
version = "0.3.0"
source = "git+https://github.com/kvark/blade?rev=e0ec4e720957edd51b945b64dd85605ea54bcfe5#e0ec4e720957edd51b945b64dd85605ea54bcfe5"
source = "git+https://github.com/kvark/blade?rev=bfa594ea697d4b6326ea29f747525c85ecf933b9#bfa594ea697d4b6326ea29f747525c85ecf933b9"
dependencies = [
"proc-macro2",
"quote",
@@ -2334,7 +2341,7 @@ dependencies = [
[[package]]
name = "blade-util"
version = "0.2.0"
source = "git+https://github.com/kvark/blade?rev=e0ec4e720957edd51b945b64dd85605ea54bcfe5#e0ec4e720957edd51b945b64dd85605ea54bcfe5"
source = "git+https://github.com/kvark/blade?rev=bfa594ea697d4b6326ea29f747525c85ecf933b9#bfa594ea697d4b6326ea29f747525c85ecf933b9"
dependencies = [
"blade-graphics",
"bytemuck",
@@ -2351,19 +2358,6 @@ dependencies = [
"digest",
]
[[package]]
name = "blake3"
version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3888aaa89e4b2a40fca9848e400f6a658a5a3978de7be858e209cafa8be9a4a0"
dependencies = [
"arrayref",
"arrayvec",
"cc",
"cfg-if",
"constant_time_eq 0.3.1",
]
[[package]]
name = "block"
version = "0.1.6"
@@ -2478,7 +2472,7 @@ dependencies = [
"language",
"log",
"pretty_assertions",
"rand 0.8.5",
"rand 0.9.1",
"rope",
"serde_json",
"sum_tree",
@@ -2624,6 +2618,7 @@ dependencies = [
"audio",
"client",
"collections",
"feature_flags",
"fs",
"futures 0.3.31",
"gpui",
@@ -2899,7 +2894,7 @@ dependencies = [
"language",
"log",
"postage",
"rand 0.8.5",
"rand 0.9.1",
"release_channel",
"rpc",
"settings",
@@ -3070,7 +3065,6 @@ dependencies = [
"clock",
"cloud_api_client",
"cloud_llm_client",
"cocoa 0.26.0",
"collections",
"credentials_provider",
"derive_more",
@@ -3083,10 +3077,11 @@ dependencies = [
"http_client_tls",
"httparse",
"log",
"objc2-foundation",
"parking_lot",
"paths",
"postage",
"rand 0.8.5",
"rand 0.9.1",
"regex",
"release_channel",
"rpc",
@@ -3335,7 +3330,7 @@ dependencies = [
"prometheus",
"prompt_store",
"prost 0.9.0",
"rand 0.8.5",
"rand 0.9.1",
"recent_projects",
"release_channel",
"remote",
@@ -3515,6 +3510,7 @@ name = "component"
version = "0.1.0"
dependencies = [
"collections",
"documented",
"gpui",
"inventory",
"parking_lot",
@@ -3577,12 +3573,6 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
[[package]]
name = "constant_time_eq"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
[[package]]
name = "context_server"
version = "0.1.0"
@@ -4162,6 +4152,19 @@ dependencies = [
"itertools 0.10.5",
]
[[package]]
name = "crossbeam"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8"
dependencies = [
"crossbeam-channel",
"crossbeam-deque",
"crossbeam-epoch",
"crossbeam-queue",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-channel"
version = "0.5.15"
@@ -4697,7 +4700,7 @@ dependencies = [
"markdown",
"pretty_assertions",
"project",
"rand 0.8.5",
"rand 0.9.1",
"serde",
"serde_json",
"settings",
@@ -5068,7 +5071,7 @@ dependencies = [
"parking_lot",
"pretty_assertions",
"project",
"rand 0.8.5",
"rand 0.9.1",
"regex",
"release_channel",
"rpc",
@@ -5563,7 +5566,7 @@ dependencies = [
"parking_lot",
"paths",
"project",
"rand 0.8.5",
"rand 0.9.1",
"release_channel",
"remote",
"reqwest_client",
@@ -6163,17 +6166,6 @@ dependencies = [
"futures-util",
]
[[package]]
name = "futures-batch"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f444c45a1cb86f2a7e301469fd50a82084a60dadc25d94529a8312276ecb71a"
dependencies = [
"futures 0.3.31",
"futures-timer",
"pin-utils",
]
[[package]]
name = "futures-channel"
version = "0.3.31"
@@ -6269,12 +6261,6 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
[[package]]
name = "futures-timer"
version = "3.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24"
[[package]]
name = "futures-util"
version = "0.3.31"
@@ -6412,7 +6398,7 @@ dependencies = [
"log",
"parking_lot",
"pretty_assertions",
"rand 0.8.5",
"rand 0.9.1",
"regex",
"rope",
"schemars",
@@ -7465,7 +7451,7 @@ dependencies = [
"pathfinder_geometry",
"postage",
"profiling",
"rand 0.8.5",
"rand 0.9.1",
"raw-window-handle",
"refineable",
"reqwest_client",
@@ -9078,7 +9064,7 @@ dependencies = [
"parking_lot",
"postage",
"pretty_assertions",
"rand 0.8.5",
"rand 0.9.1",
"regex",
"rpc",
"schemars",
@@ -9247,6 +9233,7 @@ dependencies = [
"anyhow",
"client",
"collections",
"command_palette_hooks",
"copilot",
"editor",
"futures 0.3.31",
@@ -9517,6 +9504,21 @@ dependencies = [
"vcpkg",
]
[[package]]
name = "line_ending_selector"
version = "0.1.0"
dependencies = [
"editor",
"gpui",
"language",
"picker",
"project",
"ui",
"util",
"workspace",
"workspace-hack",
]
[[package]]
name = "link-cplusplus"
version = "1.0.10"
@@ -9671,6 +9673,7 @@ dependencies = [
"scap",
"serde",
"serde_json",
"serde_urlencoded",
"settings",
"sha2",
"simplelog",
@@ -10392,7 +10395,7 @@ dependencies = [
"parking_lot",
"pretty_assertions",
"project",
"rand 0.8.5",
"rand 0.9.1",
"rope",
"serde",
"settings",
@@ -12618,7 +12621,7 @@ dependencies = [
"postage",
"prettier",
"pretty_assertions",
"rand 0.8.5",
"rand 0.9.1",
"regex",
"release_channel",
"remote",
@@ -13872,15 +13875,15 @@ dependencies = [
[[package]]
name = "rodio"
version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e40ecf59e742e03336be6a3d53755e789fd05a059fa22dfa0ed624722319e183"
source = "git+https://github.com/RustAudio/rodio?branch=better_wav_output#82514bd1f2c6cfd9a1a885019b26a8ffea75bc5c"
dependencies = [
"cpal",
"dasp_sample",
"hound",
"num-rational",
"rtrb",
"symphonia",
"tracing",
"thiserror 2.0.12",
]
[[package]]
@@ -13892,7 +13895,7 @@ dependencies = [
"ctor",
"gpui",
"log",
"rand 0.8.5",
"rand 0.9.1",
"rayon",
"smallvec",
"sum_tree",
@@ -13921,7 +13924,7 @@ dependencies = [
"gpui",
"parking_lot",
"proto",
"rand 0.8.5",
"rand 0.9.1",
"rsa",
"serde",
"serde_json",
@@ -13954,6 +13957,12 @@ dependencies = [
"zeroize",
]
[[package]]
name = "rtrb"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad8388ea1a9e0ea807e442e8263a699e7edcb320ecbcd21b4fa8ff859acce3ba"
[[package]]
name = "rules_library"
version = "0.1.0"
@@ -14356,6 +14365,19 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "scheduler"
version = "0.1.0"
dependencies = [
"async-task",
"chrono",
"futures 0.3.31",
"parking",
"parking_lot",
"rand 0.9.1",
"workspace-hack",
]
[[package]]
name = "schema_generator"
version = "0.1.0"
@@ -14658,49 +14680,6 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f7d95a54511e0c7be3f51e8867aa8cf35148d7b9445d44de2f943e2b206e749"
[[package]]
name = "semantic_index"
version = "0.1.0"
dependencies = [
"anyhow",
"arrayvec",
"blake3",
"client",
"clock",
"collections",
"feature_flags",
"fs",
"futures 0.3.31",
"futures-batch",
"gpui",
"heed",
"http_client",
"language",
"language_model",
"languages",
"log",
"open_ai",
"parking_lot",
"project",
"reqwest_client",
"serde",
"serde_json",
"settings",
"sha2",
"smol",
"streaming-iterator",
"tempfile",
"theme",
"tree-sitter",
"ui",
"unindent",
"util",
"workspace",
"workspace-hack",
"worktree",
"zlog",
]
[[package]]
name = "semantic_version"
version = "0.1.0"
@@ -15318,6 +15297,7 @@ dependencies = [
"futures 0.3.31",
"indoc",
"libsqlite3-sys",
"log",
"parking_lot",
"smol",
"sqlformat",
@@ -15655,7 +15635,7 @@ name = "streaming_diff"
version = "0.1.0"
dependencies = [
"ordered-float 2.10.1",
"rand 0.8.5",
"rand 0.9.1",
"rope",
"util",
"workspace-hack",
@@ -15769,7 +15749,7 @@ dependencies = [
"arrayvec",
"ctor",
"log",
"rand 0.8.5",
"rand 0.9.1",
"rayon",
"workspace-hack",
"zlog",
@@ -15944,12 +15924,53 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "815c942ae7ee74737bb00f965fa5b5a2ac2ce7b6c01c0cc169bbeaf7abd5f5a9"
dependencies = [
"lazy_static",
"symphonia-bundle-flac",
"symphonia-bundle-mp3",
"symphonia-codec-aac",
"symphonia-codec-pcm",
"symphonia-codec-vorbis",
"symphonia-core",
"symphonia-format-isomp4",
"symphonia-format-ogg",
"symphonia-format-riff",
"symphonia-metadata",
]
[[package]]
name = "symphonia-bundle-flac"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72e34f34298a7308d4397a6c7fbf5b84c5d491231ce3dd379707ba673ab3bd97"
dependencies = [
"log",
"symphonia-core",
"symphonia-metadata",
"symphonia-utils-xiph",
]
[[package]]
name = "symphonia-bundle-mp3"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c01c2aae70f0f1fb096b6f0ff112a930b1fb3626178fba3ae68b09dce71706d4"
dependencies = [
"lazy_static",
"log",
"symphonia-core",
"symphonia-metadata",
]
[[package]]
name = "symphonia-codec-aac"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdbf25b545ad0d3ee3e891ea643ad115aff4ca92f6aec472086b957a58522f70"
dependencies = [
"lazy_static",
"log",
"symphonia-core",
]
[[package]]
name = "symphonia-codec-pcm"
version = "0.5.4"
@@ -15960,6 +15981,17 @@ dependencies = [
"symphonia-core",
]
[[package]]
name = "symphonia-codec-vorbis"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a98765fb46a0a6732b007f7e2870c2129b6f78d87db7987e6533c8f164a9f30"
dependencies = [
"log",
"symphonia-core",
"symphonia-utils-xiph",
]
[[package]]
name = "symphonia-core"
version = "0.5.4"
@@ -15973,6 +16005,31 @@ dependencies = [
"log",
]
[[package]]
name = "symphonia-format-isomp4"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "abfdf178d697e50ce1e5d9b982ba1b94c47218e03ec35022d9f0e071a16dc844"
dependencies = [
"encoding_rs",
"log",
"symphonia-core",
"symphonia-metadata",
"symphonia-utils-xiph",
]
[[package]]
name = "symphonia-format-ogg"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ada3505789516bcf00fc1157c67729eded428b455c27ca370e41f4d785bfa931"
dependencies = [
"log",
"symphonia-core",
"symphonia-metadata",
"symphonia-utils-xiph",
]
[[package]]
name = "symphonia-format-riff"
version = "0.5.4"
@@ -15997,6 +16054,16 @@ dependencies = [
"symphonia-core",
]
[[package]]
name = "symphonia-utils-xiph"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "484472580fa49991afda5f6550ece662237b00c6f562c7d9638d1b086ed010fe"
dependencies = [
"symphonia-core",
"symphonia-metadata",
]
[[package]]
name = "syn"
version = "1.0.109"
@@ -16360,7 +16427,7 @@ dependencies = [
"futures 0.3.31",
"gpui",
"libc",
"rand 0.8.5",
"rand 0.9.1",
"regex",
"release_channel",
"schemars",
@@ -16408,7 +16475,7 @@ dependencies = [
"language",
"log",
"project",
"rand 0.8.5",
"rand 0.9.1",
"regex",
"schemars",
"search",
@@ -16440,7 +16507,7 @@ dependencies = [
"log",
"parking_lot",
"postage",
"rand 0.8.5",
"rand 0.9.1",
"regex",
"rope",
"smallvec",
@@ -16971,10 +17038,15 @@ checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076"
name = "toolchain_selector"
version = "0.1.0"
dependencies = [
"anyhow",
"convert_case 0.8.0",
"editor",
"file_finder",
"futures 0.3.31",
"fuzzy",
"gpui",
"language",
"menu",
"picker",
"project",
"ui",
@@ -17797,7 +17869,7 @@ dependencies = [
"libc",
"log",
"nix 0.29.0",
"rand 0.8.5",
"rand 0.9.1",
"regex",
"rust-embed",
"schemars",
@@ -17982,6 +18054,8 @@ version = "0.1.0"
dependencies = [
"anyhow",
"gpui",
"schemars",
"serde",
"settings",
"workspace-hack",
]
@@ -18588,7 +18662,7 @@ dependencies = [
"futures 0.3.31",
"gpui",
"parking_lot",
"rand 0.8.5",
"rand 0.9.1",
"workspace-hack",
"zlog",
]
@@ -19880,6 +19954,7 @@ dependencies = [
"core-foundation-sys",
"cranelift-codegen",
"crc32fast",
"crossbeam-channel",
"crossbeam-epoch",
"crossbeam-utils",
"crypto-common",
@@ -19923,6 +19998,7 @@ dependencies = [
"libsqlite3-sys",
"linux-raw-sys 0.4.15",
"linux-raw-sys 0.9.4",
"livekit-runtime",
"log",
"lyon",
"lyon_path",
@@ -20047,7 +20123,7 @@ dependencies = [
"paths",
"postage",
"pretty_assertions",
"rand 0.8.5",
"rand 0.9.1",
"rpc",
"schemars",
"serde",
@@ -20476,7 +20552,7 @@ dependencies = [
"language_tools",
"languages",
"libc",
"livekit_client",
"line_ending_selector",
"log",
"markdown",
"markdown_preview",
@@ -20627,7 +20703,7 @@ dependencies = [
[[package]]
name = "zed_snippets"
version = "0.0.5"
version = "0.0.6"
dependencies = [
"serde_json",
"zed_extension_api 0.1.0",
@@ -20640,13 +20716,6 @@ dependencies = [
"zed_extension_api 0.6.0",
]
[[package]]
name = "zed_toml"
version = "0.1.4"
dependencies = [
"zed_extension_api 0.1.0",
]
[[package]]
name = "zeno"
version = "0.3.2"
@@ -20810,9 +20879,10 @@ dependencies = [
"language_model",
"log",
"menu",
"parking_lot",
"postage",
"project",
"rand 0.8.5",
"rand 0.9.1",
"regex",
"release_channel",
"reqwest_client",
@@ -20882,7 +20952,7 @@ dependencies = [
"aes",
"byteorder",
"bzip2",
"constant_time_eq 0.1.5",
"constant_time_eq",
"crc32fast",
"crossbeam-utils",
"flate2",

View File

@@ -97,6 +97,7 @@ members = [
"crates/language_selector",
"crates/language_tools",
"crates/languages",
"crates/line_ending_selector",
"crates/livekit_api",
"crates/livekit_client",
"crates/lmstudio",
@@ -131,6 +132,7 @@ members = [
"crates/refineable",
"crates/refineable/derive_refineable",
"crates/release_channel",
"crates/scheduler",
"crates/remote",
"crates/remote_server",
"crates/repl",
@@ -141,7 +143,6 @@ members = [
"crates/rules_library",
"crates/schema_generator",
"crates/search",
"crates/semantic_index",
"crates/semantic_version",
"crates/session",
"crates/settings",
@@ -210,7 +211,6 @@ members = [
"extensions/slash-commands-example",
"extensions/snippets",
"extensions/test-extension",
"extensions/toml",
#
# Tooling
@@ -276,6 +276,7 @@ context_server = { path = "crates/context_server" }
copilot = { path = "crates/copilot" }
crashes = { path = "crates/crashes" }
credentials_provider = { path = "crates/credentials_provider" }
crossbeam = "0.8.4"
dap = { path = "crates/dap" }
dap_adapters = { path = "crates/dap_adapters" }
db = { path = "crates/db" }
@@ -323,6 +324,7 @@ language_models = { path = "crates/language_models" }
language_selector = { path = "crates/language_selector" }
language_tools = { path = "crates/language_tools" }
languages = { path = "crates/languages" }
line_ending_selector = { path = "crates/line_ending_selector" }
livekit_api = { path = "crates/livekit_api" }
livekit_client = { path = "crates/livekit_client" }
lmstudio = { path = "crates/lmstudio" }
@@ -360,17 +362,17 @@ proto = { path = "crates/proto" }
recent_projects = { path = "crates/recent_projects" }
refineable = { path = "crates/refineable" }
release_channel = { path = "crates/release_channel" }
scheduler = { path = "crates/scheduler" }
remote = { path = "crates/remote" }
remote_server = { path = "crates/remote_server" }
repl = { path = "crates/repl" }
reqwest_client = { path = "crates/reqwest_client" }
rich_text = { path = "crates/rich_text" }
rodio = { version = "0.21.1", default-features = false }
rodio = { git = "https://github.com/RustAudio/rodio", branch = "better_wav_output"}
rope = { path = "crates/rope" }
rpc = { path = "crates/rpc" }
rules_library = { path = "crates/rules_library" }
search = { path = "crates/search" }
semantic_index = { path = "crates/semantic_index" }
semantic_version = { path = "crates/semantic_version" }
session = { path = "crates/session" }
settings = { path = "crates/settings" }
@@ -444,6 +446,7 @@ async-fs = "2.1"
async-pipe = { git = "https://github.com/zed-industries/async-pipe-rs", rev = "82d00a04211cf4e1236029aa03e6b6ce2a74c553" }
async-recursion = "1.0.0"
async-tar = "0.5.0"
async-task = "4.7"
async-trait = "0.1"
async-tungstenite = "0.29.1"
async_zip = { version = "0.0.17", features = ["deflate", "deflate64"] }
@@ -459,9 +462,9 @@ aws-smithy-types = { version = "1.3.0", features = ["http-body-1-x"] }
base64 = "0.22"
bincode = "1.2.1"
bitflags = "2.6.0"
blade-graphics = { git = "https://github.com/kvark/blade", rev = "e0ec4e720957edd51b945b64dd85605ea54bcfe5" }
blade-macros = { git = "https://github.com/kvark/blade", rev = "e0ec4e720957edd51b945b64dd85605ea54bcfe5" }
blade-util = { git = "https://github.com/kvark/blade", rev = "e0ec4e720957edd51b945b64dd85605ea54bcfe5" }
blade-graphics = { git = "https://github.com/kvark/blade", rev = "bfa594ea697d4b6326ea29f747525c85ecf933b9" }
blade-macros = { git = "https://github.com/kvark/blade", rev = "bfa594ea697d4b6326ea29f747525c85ecf933b9" }
blade-util = { git = "https://github.com/kvark/blade", rev = "bfa594ea697d4b6326ea29f747525c85ecf933b9" }
blake3 = "1.5.3"
bytes = "1.0"
cargo_metadata = "0.19"
@@ -535,9 +538,35 @@ nbformat = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c80421
nix = "0.29"
num-format = "0.4.4"
objc = "0.2"
objc2-foundation = { version = "0.3", default-features = false, features = [
"NSArray",
"NSAttributedString",
"NSBundle",
"NSCoder",
"NSData",
"NSDate",
"NSDictionary",
"NSEnumerator",
"NSError",
"NSGeometry",
"NSNotification",
"NSNull",
"NSObjCRuntime",
"NSObject",
"NSProcessInfo",
"NSRange",
"NSRunLoop",
"NSString",
"NSURL",
"NSUndoManager",
"NSValue",
"objc2-core-foundation",
"std"
] }
open = "5.0.0"
ordered-float = "2.1.1"
palette = { version = "0.7.5", default-features = false, features = ["std"] }
parking = "2.0"
parking_lot = "0.12.1"
partial-json-fixer = "0.5.3"
parse_int = "0.9"
@@ -560,7 +589,7 @@ prost-build = "0.9"
prost-types = "0.9"
pulldown-cmark = { version = "0.12.0", default-features = false }
quote = "1.0.9"
rand = "0.8.5"
rand = "0.9"
rayon = "1.8"
ref-cast = "1.0.24"
regex = "1.5"

View File

@@ -16,6 +16,7 @@
"up": "menu::SelectPrevious",
"enter": "menu::Confirm",
"ctrl-enter": "menu::SecondaryConfirm",
"ctrl-escape": "menu::Cancel",
"ctrl-c": "menu::Cancel",
"escape": "menu::Cancel",
"alt-shift-enter": "menu::Restart",
@@ -582,7 +583,7 @@
"ctrl-n": "workspace::NewFile",
"shift-new": "workspace::NewWindow",
"ctrl-shift-n": "workspace::NewWindow",
"ctrl-`": "terminal_panel::ToggleFocus",
"ctrl-`": "terminal_panel::Toggle",
"f10": ["app_menu::OpenApplicationMenu", "Zed"],
"alt-1": ["workspace::ActivatePane", 0],
"alt-2": ["workspace::ActivatePane", 1],
@@ -627,6 +628,7 @@
"alt-save": "workspace::SaveAll",
"ctrl-alt-s": "workspace::SaveAll",
"ctrl-k m": "language_selector::Toggle",
"ctrl-k ctrl-m": "toolchain::AddToolchain",
"escape": "workspace::Unfollow",
"ctrl-k ctrl-left": "workspace::ActivatePaneLeft",
"ctrl-k ctrl-right": "workspace::ActivatePaneRight",
@@ -1027,6 +1029,13 @@
"tab": "channel_modal::ToggleMode"
}
},
{
"context": "ToolchainSelector",
"use_key_equivalents": true,
"bindings": {
"ctrl-shift-a": "toolchain::AddToolchain"
}
},
{
"context": "FileFinder || (FileFinder > Picker > Editor)",
"bindings": {

View File

@@ -649,7 +649,7 @@
"alt-shift-enter": "toast::RunAction",
"cmd-shift-s": "workspace::SaveAs",
"cmd-shift-n": "workspace::NewWindow",
"ctrl-`": "terminal_panel::ToggleFocus",
"ctrl-`": "terminal_panel::Toggle",
"cmd-1": ["workspace::ActivatePane", 0],
"cmd-2": ["workspace::ActivatePane", 1],
"cmd-3": ["workspace::ActivatePane", 2],
@@ -690,6 +690,7 @@
"cmd-?": "agent::ToggleFocus",
"cmd-alt-s": "workspace::SaveAll",
"cmd-k m": "language_selector::Toggle",
"cmd-k cmd-m": "toolchain::AddToolchain",
"escape": "workspace::Unfollow",
"cmd-k cmd-left": "workspace::ActivatePaneLeft",
"cmd-k cmd-right": "workspace::ActivatePaneRight",
@@ -1094,6 +1095,13 @@
"tab": "channel_modal::ToggleMode"
}
},
{
"context": "ToolchainSelector",
"use_key_equivalents": true,
"bindings": {
"cmd-shift-a": "toolchain::AddToolchain"
}
},
{
"context": "FileFinder || (FileFinder > Picker > Editor)",
"use_key_equivalents": true,

View File

@@ -25,7 +25,6 @@
"ctrl-alt-enter": ["picker::ConfirmInput", { "secondary": true }],
"ctrl-shift-w": "workspace::CloseWindow",
"shift-escape": "workspace::ToggleZoom",
"open": "workspace::Open",
"ctrl-o": "workspace::Open",
"ctrl-=": ["zed::IncreaseBufferFontSize", { "persist": false }],
"ctrl-shift-=": ["zed::IncreaseBufferFontSize", { "persist": false }],
@@ -68,18 +67,13 @@
"ctrl-k q": "editor::Rewrap",
"ctrl-backspace": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }],
"ctrl-delete": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }],
"cut": "editor::Cut",
"shift-delete": "editor::Cut",
"ctrl-x": "editor::Cut",
"copy": "editor::Copy",
"ctrl-insert": "editor::Copy",
"ctrl-c": "editor::Copy",
"paste": "editor::Paste",
"shift-insert": "editor::Paste",
"ctrl-v": "editor::Paste",
"undo": "editor::Undo",
"ctrl-z": "editor::Undo",
"redo": "editor::Redo",
"ctrl-y": "editor::Redo",
"ctrl-shift-z": "editor::Redo",
"up": "editor::MoveUp",
@@ -138,7 +132,6 @@
"ctrl-shift-enter": "editor::NewlineAbove",
"ctrl-k ctrl-z": "editor::ToggleSoftWrap",
"ctrl-k z": "editor::ToggleSoftWrap",
"find": "buffer_search::Deploy",
"ctrl-f": "buffer_search::Deploy",
"ctrl-h": "buffer_search::DeployReplace",
"ctrl-shift-.": "assistant::QuoteSelection",
@@ -177,7 +170,6 @@
"context": "Markdown",
"use_key_equivalents": true,
"bindings": {
"copy": "markdown::Copy",
"ctrl-c": "markdown::Copy"
}
},
@@ -225,7 +217,6 @@
"bindings": {
"ctrl-enter": "assistant::Assist",
"ctrl-s": "workspace::Save",
"save": "workspace::Save",
"ctrl-shift-,": "assistant::InsertIntoEditor",
"shift-enter": "assistant::Split",
"ctrl-r": "assistant::CycleMessageRole",
@@ -272,7 +263,6 @@
"context": "AgentPanel > Markdown",
"use_key_equivalents": true,
"bindings": {
"copy": "markdown::CopyAsMarkdown",
"ctrl-c": "markdown::CopyAsMarkdown"
}
},
@@ -367,7 +357,6 @@
"context": "PromptLibrary",
"use_key_equivalents": true,
"bindings": {
"new": "rules_library::NewRule",
"ctrl-n": "rules_library::NewRule",
"ctrl-shift-s": "rules_library::ToggleDefaultRule"
}
@@ -381,7 +370,6 @@
"enter": "search::SelectNextMatch",
"shift-enter": "search::SelectPreviousMatch",
"alt-enter": "search::SelectAllMatches",
"find": "search::FocusSearch",
"ctrl-f": "search::FocusSearch",
"ctrl-h": "search::ToggleReplace",
"ctrl-l": "search::ToggleSelection"
@@ -408,7 +396,6 @@
"use_key_equivalents": true,
"bindings": {
"escape": "project_search::ToggleFocus",
"shift-find": "search::FocusSearch",
"ctrl-shift-f": "search::FocusSearch",
"ctrl-shift-h": "search::ToggleReplace",
"alt-r": "search::ToggleRegex" // vscode
@@ -472,14 +459,12 @@
"forward": "pane::GoForward",
"f3": "search::SelectNextMatch",
"shift-f3": "search::SelectPreviousMatch",
"shift-find": "project_search::ToggleFocus",
"ctrl-shift-f": "project_search::ToggleFocus",
"shift-alt-h": "search::ToggleReplace",
"alt-l": "search::ToggleSelection",
"alt-enter": "search::SelectAllMatches",
"alt-c": "search::ToggleCaseSensitive",
"alt-w": "search::ToggleWholeWord",
"alt-find": "project_search::ToggleFilters",
"alt-f": "project_search::ToggleFilters",
"alt-r": "search::ToggleRegex",
// "ctrl-shift-alt-x": "search::ToggleRegex",
@@ -579,27 +564,21 @@
"context": "Workspace",
"use_key_equivalents": true,
"bindings": {
"alt-open": ["projects::OpenRecent", { "create_new_window": false }],
// Change the default action on `menu::Confirm` by setting the parameter
// "ctrl-alt-o": ["projects::OpenRecent", { "create_new_window": true }],
"ctrl-r": ["projects::OpenRecent", { "create_new_window": false }],
"shift-alt-open": ["projects::OpenRemote", { "from_existing_connection": false, "create_new_window": false }],
// Change to open path modal for existing remote connection by setting the parameter
// "ctrl-shift-alt-o": "["projects::OpenRemote", { "from_existing_connection": true }]",
"ctrl-shift-alt-o": ["projects::OpenRemote", { "from_existing_connection": false, "create_new_window": false }],
"shift-alt-b": "branches::OpenRecent",
"shift-alt-enter": "toast::RunAction",
"ctrl-shift-`": "workspace::NewTerminal",
"save": "workspace::Save",
"ctrl-s": "workspace::Save",
"ctrl-k ctrl-shift-s": "workspace::SaveWithoutFormat",
"shift-save": "workspace::SaveAs",
"ctrl-shift-s": "workspace::SaveAs",
"new": "workspace::NewFile",
"ctrl-n": "workspace::NewFile",
"shift-new": "workspace::NewWindow",
"ctrl-shift-n": "workspace::NewWindow",
"ctrl-`": "terminal_panel::ToggleFocus",
"ctrl-`": "terminal_panel::Toggle",
"f10": ["app_menu::OpenApplicationMenu", "Zed"],
"alt-1": ["workspace::ActivatePane", 0],
"alt-2": ["workspace::ActivatePane", 1],
@@ -621,7 +600,6 @@
"shift-alt-0": "workspace::ResetOpenDocksSize",
"ctrl-shift-alt--": ["workspace::DecreaseOpenDocksSize", { "px": 0 }],
"ctrl-shift-alt-=": ["workspace::IncreaseOpenDocksSize", { "px": 0 }],
"shift-find": "pane::DeploySearch",
"ctrl-shift-f": "pane::DeploySearch",
"ctrl-shift-h": ["pane::DeploySearch", { "replace_enabled": true }],
"ctrl-shift-t": "pane::ReopenClosedItem",
@@ -641,9 +619,9 @@
"ctrl-shift-g": "git_panel::ToggleFocus",
"ctrl-shift-d": "debug_panel::ToggleFocus",
"ctrl-shift-/": "agent::ToggleFocus",
"alt-save": "workspace::SaveAll",
"ctrl-k s": "workspace::SaveAll",
"ctrl-k m": "language_selector::Toggle",
"ctrl-m ctrl-m": "toolchain::AddToolchain",
"escape": "workspace::Unfollow",
"ctrl-k ctrl-left": "workspace::ActivatePaneLeft",
"ctrl-k ctrl-right": "workspace::ActivatePaneRight",
@@ -848,9 +826,7 @@
"bindings": {
"left": "outline_panel::CollapseSelectedEntry",
"right": "outline_panel::ExpandSelectedEntry",
"alt-copy": "outline_panel::CopyPath",
"shift-alt-c": "outline_panel::CopyPath",
"shift-alt-copy": "workspace::CopyRelativePath",
"ctrl-shift-alt-c": "workspace::CopyRelativePath",
"ctrl-alt-r": "outline_panel::RevealInFileManager",
"space": "outline_panel::OpenSelectedEntry",
@@ -866,21 +842,14 @@
"bindings": {
"left": "project_panel::CollapseSelectedEntry",
"right": "project_panel::ExpandSelectedEntry",
"new": "project_panel::NewFile",
"ctrl-n": "project_panel::NewFile",
"alt-new": "project_panel::NewDirectory",
"alt-n": "project_panel::NewDirectory",
"cut": "project_panel::Cut",
"ctrl-x": "project_panel::Cut",
"copy": "project_panel::Copy",
"ctrl-insert": "project_panel::Copy",
"ctrl-c": "project_panel::Copy",
"paste": "project_panel::Paste",
"shift-insert": "project_panel::Paste",
"ctrl-v": "project_panel::Paste",
"alt-copy": "project_panel::CopyPath",
"shift-alt-c": "project_panel::CopyPath",
"shift-alt-copy": "workspace::CopyRelativePath",
"ctrl-k ctrl-shift-c": "workspace::CopyRelativePath",
"enter": "project_panel::Rename",
"f2": "project_panel::Rename",
@@ -892,7 +861,6 @@
"ctrl-alt-r": "project_panel::RevealInFileManager",
"ctrl-shift-enter": "project_panel::OpenWithSystem",
"alt-d": "project_panel::CompareMarkedFiles",
"shift-find": "project_panel::NewSearchInDirectory",
"ctrl-k ctrl-shift-f": "project_panel::NewSearchInDirectory",
"shift-down": "menu::SelectNext",
"shift-up": "menu::SelectPrevious",
@@ -1075,6 +1043,13 @@
"tab": "channel_modal::ToggleMode"
}
},
{
"context": "ToolchainSelector",
"use_key_equivalents": true,
"bindings": {
"ctrl-shift-a": "toolchain::AddToolchain"
}
},
{
"context": "FileFinder || (FileFinder > Picker > Editor)",
"use_key_equivalents": true,
@@ -1110,10 +1085,8 @@
"use_key_equivalents": true,
"bindings": {
"ctrl-alt-space": "terminal::ShowCharacterPalette",
"copy": "terminal::Copy",
"ctrl-insert": "terminal::Copy",
"ctrl-shift-c": "terminal::Copy",
"paste": "terminal::Paste",
"shift-insert": "terminal::Paste",
"ctrl-shift-v": "terminal::Paste",
"ctrl-enter": "assistant::InlineAssist",
@@ -1129,7 +1102,6 @@
"ctrl-w": ["terminal::SendKeystroke", "ctrl-w"],
"ctrl-backspace": ["terminal::SendKeystroke", "ctrl-w"],
"ctrl-shift-a": "editor::SelectAll",
"find": "buffer_search::Deploy",
"ctrl-shift-f": "buffer_search::Deploy",
"ctrl-shift-l": "terminal::Clear",
"ctrl-shift-w": "pane::CloseActiveItem",
@@ -1210,7 +1182,6 @@
"use_key_equivalents": true,
"bindings": {
"ctrl-f": "search::FocusSearch",
"alt-find": "keymap_editor::ToggleKeystrokeSearch",
"alt-f": "keymap_editor::ToggleKeystrokeSearch",
"alt-c": "keymap_editor::ToggleConflictFilter",
"enter": "keymap_editor::EditBinding",

View File

@@ -125,7 +125,7 @@
{
"context": "Workspace || Editor",
"bindings": {
"alt-f12": "terminal_panel::ToggleFocus",
"alt-f12": "terminal_panel::Toggle",
"ctrl-shift-k": "git::Push"
}
},

View File

@@ -127,7 +127,7 @@
{
"context": "Workspace || Editor",
"bindings": {
"alt-f12": "terminal_panel::ToggleFocus",
"alt-f12": "terminal_panel::Toggle",
"cmd-shift-k": "git::Push"
}
},

View File

@@ -962,7 +962,7 @@
// Show git status colors in the editor tabs.
"git_status": false,
// Position of the close button on the editor tabs.
// One of: ["right", "left", "hidden"]
// One of: ["right", "left"]
"close_position": "right",
// Whether to show the file icon for a tab.
"file_icons": false,

View File

@@ -2114,7 +2114,7 @@ mod tests {
use gpui::{App, AsyncApp, TestAppContext, WeakEntity};
use indoc::indoc;
use project::{FakeFs, Fs};
use rand::Rng as _;
use rand::{distr, prelude::*};
use serde_json::json;
use settings::SettingsStore;
use smol::stream::StreamExt as _;
@@ -3057,8 +3057,8 @@ mod tests {
cx: &mut App,
) -> Task<gpui::Result<Entity<AcpThread>>> {
let session_id = acp::SessionId(
rand::thread_rng()
.sample_iter(&rand::distributions::Alphanumeric)
rand::rng()
.sample_iter(&distr::Alphanumeric)
.take(7)
.map(char::from)
.collect::<String>()

View File

@@ -2218,7 +2218,7 @@ mod tests {
action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
for _ in 0..operations {
match rng.gen_range(0..100) {
match rng.random_range(0..100) {
0..25 => {
action_log.update(cx, |log, cx| {
let range = buffer.read(cx).random_byte_range(0, &mut rng);
@@ -2237,7 +2237,7 @@ mod tests {
.unwrap();
}
_ => {
let is_agent_edit = rng.gen_bool(0.5);
let is_agent_edit = rng.random_bool(0.5);
if is_agent_edit {
log::info!("agent edit");
} else {
@@ -2252,7 +2252,7 @@ mod tests {
}
}
if rng.gen_bool(0.2) {
if rng.random_bool(0.2) {
quiesce(&action_log, &buffer, cx);
}
}

View File

@@ -84,7 +84,6 @@ impl ActivityIndicator {
) -> Entity<ActivityIndicator> {
let project = workspace.project().clone();
let auto_updater = AutoUpdater::get(cx);
let workspace_handle = cx.entity();
let this = cx.new(|cx| {
let mut status_events = languages.language_server_binary_statuses();
cx.spawn(async move |this, cx| {
@@ -102,20 +101,6 @@ impl ActivityIndicator {
})
.detach();
cx.subscribe_in(
&workspace_handle,
window,
|activity_indicator, _, event, window, cx| {
if let workspace::Event::ClearActivityIndicator = event
&& activity_indicator.statuses.pop().is_some()
{
activity_indicator.dismiss_error_message(&DismissErrorMessage, window, cx);
cx.notify();
}
},
)
.detach();
cx.subscribe(
&project.read(cx).lsp_store(),
|activity_indicator, _, event, cx| {
@@ -227,7 +212,8 @@ impl ActivityIndicator {
server_name,
status,
} => {
let create_buffer = project.update(cx, |project, cx| project.create_buffer(cx));
let create_buffer =
project.update(cx, |project, cx| project.create_buffer(false, cx));
let status = status.clone();
let server_name = server_name.clone();
cx.spawn_in(window, async move |workspace, cx| {

View File

@@ -83,6 +83,7 @@ struct EditFileToolPartialInput {
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
#[schemars(inline)]
pub enum EditFileMode {
Edit,
Create,

View File

@@ -11,6 +11,7 @@ use crate::{AgentTool, ToolCallEventStream};
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
#[schemars(inline)]
pub enum Timezone {
/// Use UTC for the datetime.
Utc,

View File

@@ -1,4 +1,3 @@
use language_models::provider::anthropic::AnthropicLanguageModelProvider;
use settings::SettingsStore;
use std::path::Path;
use std::rc::Rc;
@@ -81,8 +80,15 @@ impl AgentServer for ClaudeCode {
let settings = cx.read_global(|settings: &SettingsStore, _| {
settings.get::<AllAgentServersSettings>(None).claude.clone()
});
let project = delegate.project().clone();
cx.spawn(async move |cx| {
let mut project_env = project
.update(cx, |project, cx| {
project.directory_environment(root_dir.as_path().into(), cx)
})?
.await
.unwrap_or_default();
let mut command = if let Some(settings) = settings {
settings.command
} else {
@@ -98,17 +104,13 @@ impl AgentServer for ClaudeCode {
})?
.await?
};
project_env.extend(command.env.take().unwrap_or_default());
command.env = Some(project_env);
if let Some(api_key) = cx
.update(AnthropicLanguageModelProvider::api_key)?
.await
.ok()
{
command
.env
.get_or_insert_default()
.insert("ANTHROPIC_API_KEY".to_owned(), api_key.key);
}
command
.env
.get_or_insert_default()
.insert("ANTHROPIC_API_KEY".to_owned(), "".to_owned());
let root_dir_exists = fs.is_dir(&root_dir).await;
anyhow::ensure!(

View File

@@ -41,12 +41,19 @@ impl AgentServer for Gemini {
let settings = cx.read_global(|settings: &SettingsStore, _| {
settings.get::<AllAgentServersSettings>(None).gemini.clone()
});
let project = delegate.project().clone();
cx.spawn(async move |cx| {
let ignore_system_version = settings
.as_ref()
.and_then(|settings| settings.ignore_system_version)
.unwrap_or(true);
let mut project_env = project
.update(cx, |project, cx| {
project.directory_environment(root_dir.as_path().into(), cx)
})?
.await
.unwrap_or_default();
let mut command = if let Some(settings) = settings
&& let Some(command) = settings.custom_command()
{
@@ -67,13 +74,12 @@ impl AgentServer for Gemini {
if !command.args.contains(&ACP_ARG.into()) {
command.args.push(ACP_ARG.into());
}
if let Some(api_key) = cx.update(GoogleLanguageModelProvider::api_key)?.await.ok() {
command
.env
.get_or_insert_default()
project_env
.insert("GEMINI_API_KEY".to_owned(), api_key.key);
}
project_env.extend(command.env.take().unwrap_or_default());
command.env = Some(project_env);
let root_dir_exists = fs.is_dir(&root_dir).await;
anyhow::ensure!(

View File

@@ -6,13 +6,14 @@ use collections::HashMap;
use gpui::{App, SharedString};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources, SettingsUi};
use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
pub fn init(cx: &mut App) {
AllAgentServersSettings::register(cx);
}
#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug, SettingsUi)]
#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug, SettingsUi, SettingsKey)]
#[settings_key(key = "agent_servers")]
pub struct AllAgentServersSettings {
pub gemini: Option<BuiltinAgentServerSettings>,
pub claude: Option<CustomAgentServerSettings>,
@@ -75,8 +76,6 @@ pub struct CustomAgentServerSettings {
}
impl settings::Settings for AllAgentServersSettings {
const KEY: Option<&'static str> = Some("agent_servers");
type FileContent = Self;
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {

View File

@@ -8,7 +8,7 @@ use gpui::{App, Pixels, SharedString};
use language_model::LanguageModel;
use schemars::{JsonSchema, json_schema};
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources, SettingsUi};
use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
use std::borrow::Cow;
pub use crate::agent_profile::*;
@@ -223,7 +223,8 @@ impl AgentSettingsContent {
}
}
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, Default, SettingsUi)]
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, Default, SettingsUi, SettingsKey)]
#[settings_key(key = "agent", fallback_key = "assistant")]
pub struct AgentSettingsContent {
/// Whether the Agent is enabled.
///
@@ -399,10 +400,6 @@ pub struct ContextServerPresetContent {
}
impl Settings for AgentSettings {
const KEY: Option<&'static str> = Some("agent");
const FALLBACK_KEY: Option<&'static str> = Some("assistant");
const PRESERVED_KEYS: Option<&'static [&'static str]> = Some(&["version"]);
type FileContent = AgentSettingsContent;

View File

@@ -1025,43 +1025,31 @@ impl SlashCommandCompletion {
return None;
}
let last_command_start = line.rfind('/')?;
if last_command_start >= line.len() {
return Some(Self::default());
}
if last_command_start > 0
&& line
.chars()
.nth(last_command_start - 1)
.is_some_and(|c| !c.is_whitespace())
let (prefix, last_command) = line.rsplit_once('/')?;
if prefix.chars().last().is_some_and(|c| !c.is_whitespace())
|| last_command.starts_with(char::is_whitespace)
{
return None;
}
let rest_of_line = &line[last_command_start + 1..];
let mut command = None;
let mut argument = None;
let mut end = last_command_start + 1;
if let Some(command_text) = rest_of_line.split_whitespace().next() {
command = Some(command_text.to_string());
end += command_text.len();
// Find the start of arguments after the command
if let Some(args_start) =
rest_of_line[command_text.len()..].find(|c: char| !c.is_whitespace())
{
let args = &rest_of_line[command_text.len() + args_start..].trim_end();
if !args.is_empty() {
argument = Some(args.to_string());
end += args.len() + 1;
}
let mut command = None;
if let Some((command_text, args)) = last_command.split_once(char::is_whitespace) {
if !args.is_empty() {
argument = Some(args.trim_end().to_string());
}
}
command = Some(command_text.to_string());
} else if !last_command.is_empty() {
command = Some(last_command.to_string());
};
Some(Self {
source_range: last_command_start + offset_to_line..end + offset_to_line,
source_range: prefix.len() + offset_to_line
..line
.rfind(|c: char| !c.is_whitespace())
.unwrap_or_else(|| line.len())
+ 1
+ offset_to_line,
command,
argument,
})
@@ -1180,6 +1168,15 @@ mod tests {
})
);
assert_eq!(
SlashCommandCompletion::try_parse("/拿不到命令 拿不到命令 ", 0),
Some(SlashCommandCompletion {
source_range: 0..30,
command: Some("拿不到命令".to_string()),
argument: Some("拿不到命令".to_string()),
})
);
assert_eq!(SlashCommandCompletion::try_parse("Lorem Ipsum", 0), None);
assert_eq!(SlashCommandCompletion::try_parse("Lorem /", 0), None);
@@ -1187,6 +1184,8 @@ mod tests {
assert_eq!(SlashCommandCompletion::try_parse("Lorem /help", 0), None);
assert_eq!(SlashCommandCompletion::try_parse("Lorem/", 0), None);
assert_eq!(SlashCommandCompletion::try_parse("/ ", 0), None);
}
#[test]

View File

@@ -207,7 +207,7 @@ impl EntryViewState {
self.entries.drain(range);
}
pub fn settings_changed(&mut self, cx: &mut App) {
pub fn agent_font_size_changed(&mut self, cx: &mut App) {
for entry in self.entries.iter() {
match entry {
Entry::UserMessage { .. } | Entry::AssistantMessage { .. } => {}

View File

@@ -493,14 +493,13 @@ impl MessageEditor {
let Some(entry) = self.project.read(cx).entry_for_path(&project_path, cx) else {
return Task::ready(Err(anyhow!("project entry not found")));
};
let Some(worktree) = self.project.read(cx).worktree_for_entry(entry.id, cx) else {
let directory_path = entry.path.clone();
let worktree_id = project_path.worktree_id;
let Some(worktree) = self.project.read(cx).worktree_for_id(worktree_id, cx) else {
return Task::ready(Err(anyhow!("worktree not found")));
};
let project = self.project.clone();
cx.spawn(async move |_, cx| {
let directory_path = entry.path.clone();
let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id())?;
let file_paths = worktree.read_with(cx, |worktree, _cx| {
collect_files_in_path(worktree, &directory_path)
})?;

View File

@@ -192,8 +192,10 @@ impl PickerDelegate for AcpModelPickerDelegate {
}
}
fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
cx.emit(DismissEvent);
fn dismissed(&mut self, window: &mut Window, cx: &mut Context<Picker<Self>>) {
cx.defer_in(window, |picker, window, cx| {
picker.set_query("", window, cx);
});
}
fn render_match(

View File

@@ -43,7 +43,7 @@ use std::{collections::BTreeMap, rc::Rc, time::Duration};
use task::SpawnInTerminal;
use terminal_view::terminal_panel::TerminalPanel;
use text::Anchor;
use theme::ThemeSettings;
use theme::{AgentFontSize, ThemeSettings};
use ui::{
Callout, CommonAnimationExt, Disclosure, Divider, DividerColor, ElevationIndex, KeyBinding,
PopoverMenuHandle, Scrollbar, ScrollbarState, SpinnerLabel, TintColor, Tooltip, prelude::*,
@@ -290,7 +290,7 @@ pub struct AcpThreadView {
is_loading_contents: bool,
new_server_version_available: Option<SharedString>,
_cancel_task: Option<Task<()>>,
_subscriptions: [Subscription; 3],
_subscriptions: [Subscription; 4],
}
enum ThreadState {
@@ -380,7 +380,8 @@ impl AcpThreadView {
});
let subscriptions = [
cx.observe_global_in::<SettingsStore>(window, Self::settings_changed),
cx.observe_global_in::<SettingsStore>(window, Self::agent_font_size_changed),
cx.observe_global_in::<AgentFontSize>(window, Self::agent_font_size_changed),
cx.subscribe_in(&message_editor, window, Self::handle_message_editor_event),
cx.subscribe_in(&entry_view_state, window, Self::handle_entry_view_event),
];
@@ -983,7 +984,7 @@ impl AcpThreadView {
this,
AuthRequired {
description: None,
provider_id: Some(language_model::ANTHROPIC_PROVIDER_ID),
provider_id: None,
},
agent,
connection,
@@ -3010,6 +3011,8 @@ impl AcpThreadView {
let show_description =
configuration_view.is_none() && description.is_none() && pending_auth_method.is_none();
let auth_methods = connection.auth_methods();
v_flex().flex_1().size_full().justify_end().child(
v_flex()
.p_2()
@@ -3040,21 +3043,23 @@ impl AcpThreadView {
.cloned()
.map(|view| div().w_full().child(view)),
)
.when(
show_description,
|el| {
el.child(
Label::new(format!(
"You are not currently authenticated with {}. Please choose one of the following options:",
self.agent.name()
))
.size(LabelSize::Small)
.color(Color::Muted)
.mb_1()
.ml_5(),
)
},
)
.when(show_description, |el| {
el.child(
Label::new(format!(
"You are not currently authenticated with {}.{}",
self.agent.name(),
if auth_methods.len() > 1 {
" Please choose one of the following options:"
} else {
""
}
))
.size(LabelSize::Small)
.color(Color::Muted)
.mb_1()
.ml_5(),
)
})
.when_some(pending_auth_method, |el, _| {
el.child(
h_flex()
@@ -3066,12 +3071,12 @@ impl AcpThreadView {
Icon::new(IconName::ArrowCircle)
.size(IconSize::Small)
.color(Color::Muted)
.with_rotate_animation(2)
.with_rotate_animation(2),
)
.child(Label::new("Authenticating…").size(LabelSize::Small)),
)
})
.when(!connection.auth_methods().is_empty(), |this| {
.when(!auth_methods.is_empty(), |this| {
this.child(
h_flex()
.justify_end()
@@ -3083,38 +3088,32 @@ impl AcpThreadView {
.pt_2()
.border_color(cx.theme().colors().border.opacity(0.8))
})
.children(
connection
.auth_methods()
.iter()
.enumerate()
.rev()
.map(|(ix, method)| {
Button::new(
SharedString::from(method.id.0.clone()),
method.name.clone(),
)
.when(ix == 0, |el| {
el.style(ButtonStyle::Tinted(ui::TintColor::Warning))
})
.label_size(LabelSize::Small)
.on_click({
let method_id = method.id.clone();
cx.listener(move |this, _, window, cx| {
telemetry::event!(
"Authenticate Agent Started",
agent = this.agent.telemetry_id(),
method = method_id
);
.children(connection.auth_methods().iter().enumerate().rev().map(
|(ix, method)| {
Button::new(
SharedString::from(method.id.0.clone()),
method.name.clone(),
)
.when(ix == 0, |el| {
el.style(ButtonStyle::Tinted(ui::TintColor::Warning))
})
.label_size(LabelSize::Small)
.on_click({
let method_id = method.id.clone();
cx.listener(move |this, _, window, cx| {
telemetry::event!(
"Authenticate Agent Started",
agent = this.agent.telemetry_id(),
method = method_id
);
this.authenticate(method_id.clone(), window, cx)
})
this.authenticate(method_id.clone(), window, cx)
})
}),
),
})
},
)),
)
})
}),
)
}
@@ -4071,15 +4070,15 @@ impl AcpThreadView {
MentionUri::PastedImage => {}
MentionUri::Directory { abs_path } => {
let project = workspace.project();
let Some(entry) = project.update(cx, |project, cx| {
let Some(entry_id) = project.update(cx, |project, cx| {
let path = project.find_project_path(abs_path, cx)?;
project.entry_for_path(&path, cx)
project.entry_for_path(&path, cx).map(|entry| entry.id)
}) else {
return;
};
project.update(cx, |_, cx| {
cx.emit(project::Event::RevealInProjectPanel(entry.id));
cx.emit(project::Event::RevealInProjectPanel(entry_id));
});
}
MentionUri::Symbol {
@@ -4092,11 +4091,9 @@ impl AcpThreadView {
line_range,
} => {
let project = workspace.project();
let Some((path, _)) = project.update(cx, |project, cx| {
let path = project.find_project_path(path, cx)?;
let entry = project.entry_for_path(&path, cx)?;
Some((path, entry))
}) else {
let Some(path) =
project.update(cx, |project, cx| project.find_project_path(path, cx))
else {
return;
};
@@ -4258,7 +4255,7 @@ impl AcpThreadView {
}
let buffer = project.update(cx, |project, cx| {
project.create_local_buffer(&markdown, Some(markdown_language), cx)
project.create_local_buffer(&markdown, Some(markdown_language), true, cx)
});
let buffer = cx.new(|cx| {
MultiBuffer::singleton(buffer, cx).with_title(thread_summary.clone())
@@ -4737,9 +4734,9 @@ impl AcpThreadView {
)
}
fn settings_changed(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
fn agent_font_size_changed(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
self.entry_view_state.update(cx, |entry_view_state, cx| {
entry_view_state.settings_changed(cx);
entry_view_state.agent_font_size_changed(cx);
});
}

View File

@@ -3585,7 +3585,7 @@ pub(crate) fn open_active_thread_as_markdown(
}
let buffer = project.update(cx, |project, cx| {
project.create_local_buffer(&markdown, Some(markdown_language), cx)
project.create_local_buffer(&markdown, Some(markdown_language), true, cx)
});
let buffer =
cx.new(|cx| MultiBuffer::singleton(buffer, cx).with_title(thread_summary.clone()));

View File

@@ -251,6 +251,7 @@ pub struct ConfigureContextServerModal {
workspace: WeakEntity<Workspace>,
source: ConfigurationSource,
state: State,
original_server_id: Option<ContextServerId>,
}
impl ConfigureContextServerModal {
@@ -348,6 +349,11 @@ impl ConfigureContextServerModal {
context_server_store,
workspace: workspace_handle,
state: State::Idle,
original_server_id: match &target {
ConfigurationTarget::Existing { id, .. } => Some(id.clone()),
ConfigurationTarget::Extension { id, .. } => Some(id.clone()),
ConfigurationTarget::New => None,
},
source: ConfigurationSource::from_target(
target,
language_registry,
@@ -415,9 +421,19 @@ impl ConfigureContextServerModal {
// When we write the settings to the file, the context server will be restarted.
workspace.update(cx, |workspace, cx| {
let fs = workspace.app_state().fs.clone();
update_settings_file::<ProjectSettings>(fs.clone(), cx, |project_settings, _| {
project_settings.context_servers.insert(id.0, settings);
});
let original_server_id = self.original_server_id.clone();
update_settings_file::<ProjectSettings>(
fs.clone(),
cx,
move |project_settings, _| {
if let Some(original_id) = original_server_id {
if original_id != id {
project_settings.context_servers.remove(&original_id.0);
}
}
project_settings.context_servers.insert(id.0, settings);
},
);
});
} else if let Some(existing_server) = existing_server {
self.context_server_store

View File

@@ -337,8 +337,7 @@ fn update_command_palette_filter(cx: &mut App) {
];
filter.show_action_types(edit_prediction_actions.iter());
filter
.show_action_types([TypeId::of::<zed_actions::OpenZedPredictOnboarding>()].iter());
filter.show_action_types(&[TypeId::of::<zed_actions::OpenZedPredictOnboarding>()]);
}
});
}

View File

@@ -1139,7 +1139,7 @@ mod tests {
);
while !new_text.is_empty() {
let max_len = cmp::min(new_text.len(), 10);
let len = rng.gen_range(1..=max_len);
let len = rng.random_range(1..=max_len);
let (chunk, suffix) = new_text.split_at(len);
chunks_tx.unbounded_send(chunk.to_string()).unwrap();
new_text = suffix;
@@ -1208,7 +1208,7 @@ mod tests {
);
while !new_text.is_empty() {
let max_len = cmp::min(new_text.len(), 10);
let len = rng.gen_range(1..=max_len);
let len = rng.random_range(1..=max_len);
let (chunk, suffix) = new_text.split_at(len);
chunks_tx.unbounded_send(chunk.to_string()).unwrap();
new_text = suffix;
@@ -1277,7 +1277,7 @@ mod tests {
);
while !new_text.is_empty() {
let max_len = cmp::min(new_text.len(), 10);
let len = rng.gen_range(1..=max_len);
let len = rng.random_range(1..=max_len);
let (chunk, suffix) = new_text.split_at(len);
chunks_tx.unbounded_send(chunk.to_string()).unwrap();
new_text = suffix;

View File

@@ -987,7 +987,8 @@ impl MentionLink {
.read(cx)
.project()
.read(cx)
.entry_for_path(&project_path, cx)?;
.entry_for_path(&project_path, cx)?
.clone();
Some(MentionLink::File(project_path, entry))
}
Self::SYMBOL => {

View File

@@ -125,6 +125,7 @@ pub(crate) fn create_editor(
cx,
);
editor.set_placeholder_text("Message the agent @ to include context", cx);
editor.disable_word_completions();
editor.set_show_indent_guides(false, cx);
editor.set_soft_wrap();
editor.set_use_modal_editing(true);

View File

@@ -2,10 +2,11 @@ use anyhow::Result;
use gpui::App;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources, SettingsUi};
use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
/// Settings for slash commands.
#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema, SettingsUi)]
#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema, SettingsUi, SettingsKey)]
#[settings_key(key = "slash_commands")]
pub struct SlashCommandSettings {
/// Settings for the `/cargo-workspace` slash command.
#[serde(default)]
@@ -21,8 +22,6 @@ pub struct CargoWorkspaceCommandSettings {
}
impl Settings for SlashCommandSettings {
const KEY: Option<&'static str> = Some("slash_commands");
type FileContent = Self;
fn load(sources: SettingsSources<Self::FileContent>, _cx: &mut App) -> Result<Self> {

View File

@@ -764,7 +764,7 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
let network = Arc::new(Mutex::new(Network::new(rng.clone())));
let mut contexts = Vec::new();
let num_peers = rng.gen_range(min_peers..=max_peers);
let num_peers = rng.random_range(min_peers..=max_peers);
let context_id = ContextId::new();
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
for i in 0..num_peers {
@@ -806,10 +806,10 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
|| !network.lock().is_idle()
|| network.lock().contains_disconnected_peers()
{
let context_index = rng.gen_range(0..contexts.len());
let context_index = rng.random_range(0..contexts.len());
let context = &contexts[context_index];
match rng.gen_range(0..100) {
match rng.random_range(0..100) {
0..=29 if mutation_count > 0 => {
log::info!("Context {}: edit buffer", context_index);
context.update(cx, |context, cx| {
@@ -874,10 +874,10 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
merge_same_roles: true,
})];
let num_sections = rng.gen_range(0..=3);
let num_sections = rng.random_range(0..=3);
let mut section_start = 0;
for _ in 0..num_sections {
let mut section_end = rng.gen_range(section_start..=output_text.len());
let mut section_end = rng.random_range(section_start..=output_text.len());
while !output_text.is_char_boundary(section_end) {
section_end += 1;
}
@@ -924,7 +924,7 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
75..=84 if mutation_count > 0 => {
context.update(cx, |context, cx| {
if let Some(message) = context.messages(cx).choose(&mut rng) {
let new_status = match rng.gen_range(0..3) {
let new_status = match rng.random_range(0..3) {
0 => MessageStatus::Done,
1 => MessageStatus::Pending,
_ => MessageStatus::Error(SharedString::from("Random error")),
@@ -971,7 +971,7 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
network.lock().broadcast(replica_id, ops_to_send);
context.update(cx, |context, cx| context.apply_ops(ops_to_receive, cx));
} else if rng.gen_bool(0.1) && replica_id != 0 {
} else if rng.random_bool(0.1) && replica_id != 0 {
log::info!("Context {}: disconnecting", context_index);
network.lock().disconnect_peer(replica_id);
} else if network.lock().has_unreceived(replica_id) {

View File

@@ -1315,17 +1315,17 @@ mod tests {
#[gpui::test(iterations = 100)]
async fn test_random_indents(mut rng: StdRng) {
let len = rng.gen_range(1..=100);
let len = rng.random_range(1..=100);
let new_text = util::RandomCharIter::new(&mut rng)
.with_simple_text()
.take(len)
.collect::<String>();
let new_text = new_text
.split('\n')
.map(|line| format!("{}{}", " ".repeat(rng.gen_range(0..=8)), line))
.map(|line| format!("{}{}", " ".repeat(rng.random_range(0..=8)), line))
.collect::<Vec<_>>()
.join("\n");
let delta = IndentDelta::Spaces(rng.gen_range(-4..=4));
let delta = IndentDelta::Spaces(rng.random_range(-4i8..=4i8) as isize);
let chunks = to_random_chunks(&mut rng, &new_text);
let new_text_chunks = stream::iter(chunks.iter().enumerate().map(|(index, chunk)| {
@@ -1357,7 +1357,7 @@ mod tests {
}
fn to_random_chunks(rng: &mut StdRng, input: &str) -> Vec<String> {
let chunk_count = rng.gen_range(1..=cmp::min(input.len(), 50));
let chunk_count = rng.random_range(1..=cmp::min(input.len(), 50));
let mut chunk_indices = (0..input.len()).choose_multiple(rng, chunk_count);
chunk_indices.sort();
chunk_indices.push(input.len());

View File

@@ -204,7 +204,7 @@ mod tests {
}
fn parse_random_chunks(input: &str, parser: &mut CreateFileParser, rng: &mut StdRng) -> String {
let chunk_count = rng.gen_range(1..=cmp::min(input.len(), 50));
let chunk_count = rng.random_range(1..=cmp::min(input.len(), 50));
let mut chunk_indices = (0..input.len()).choose_multiple(rng, chunk_count);
chunk_indices.sort();
chunk_indices.push(input.len());

View File

@@ -996,7 +996,7 @@ mod tests {
}
fn parse_random_chunks(input: &str, parser: &mut EditParser, rng: &mut StdRng) -> Vec<Edit> {
let chunk_count = rng.gen_range(1..=cmp::min(input.len(), 50));
let chunk_count = rng.random_range(1..=cmp::min(input.len(), 50));
let mut chunk_indices = (0..input.len()).choose_multiple(rng, chunk_count);
chunk_indices.sort();
chunk_indices.push(input.len());

View File

@@ -1399,7 +1399,7 @@ fn eval(
}
fn run_eval(eval: EvalInput, tx: mpsc::Sender<Result<EvalOutput>>) {
let dispatcher = gpui::TestDispatcher::new(StdRng::from_entropy());
let dispatcher = gpui::TestDispatcher::new(StdRng::from_os_rng());
let mut cx = TestAppContext::build(dispatcher, None);
let output = cx.executor().block_test(async {
let test = EditAgentTest::new(&mut cx).await;
@@ -1707,7 +1707,7 @@ async fn retry_on_rate_limit<R>(mut request: impl AsyncFnMut() -> Result<R>) ->
};
if let Some(retry_after) = retry_delay {
let jitter = retry_after.mul_f64(rand::thread_rng().gen_range(0.0..1.0));
let jitter = retry_after.mul_f64(rand::rng().random_range(0.0..1.0));
eprintln!("Attempt #{attempt}: Retry after {retry_after:?} + jitter of {jitter:?}");
Timer::after(retry_after + jitter).await;
} else {

View File

@@ -771,7 +771,7 @@ mod tests {
}
fn to_random_chunks(rng: &mut StdRng, input: &str) -> Vec<String> {
let chunk_count = rng.gen_range(1..=cmp::min(input.len(), 50));
let chunk_count = rng.random_range(1..=cmp::min(input.len(), 50));
let mut chunk_indices = (0..input.len()).choose_multiple(rng, chunk_count);
chunk_indices.sort();
chunk_indices.push(input.len());

View File

@@ -14,11 +14,20 @@ doctest = false
[dependencies]
anyhow.workspace = true
async-tar.workspace = true
collections.workspace = true
crossbeam.workspace = true
gpui.workspace = true
settings.workspace = true
log.workspace = true
parking_lot.workspace = true
rodio = { workspace = true, features = [ "wav", "playback", "wav_output" ] }
schemars.workspace = true
serde.workspace = true
rodio = { workspace = true, features = [ "wav", "playback", "tracing" ] }
settings.workspace = true
smol.workspace = true
thiserror.workspace = true
util.workspace = true
workspace-hack.workspace = true
[target.'cfg(not(any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd")))'.dependencies]
libwebrtc = { rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d", git = "https://github.com/zed-industries/livekit-rust-sdks" }

View File

@@ -1,19 +1,56 @@
use anyhow::{Context as _, Result, anyhow};
use anyhow::{Context as _, Result};
use collections::HashMap;
use gpui::{App, BorrowAppContext, Global};
use rodio::{Decoder, OutputStream, OutputStreamBuilder, Source, source::Buffered};
use gpui::{App, BackgroundExecutor, BorrowAppContext, Global};
#[cfg(not(any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd")))]
mod non_windows_and_freebsd_deps {
pub(super) use gpui::AsyncApp;
pub(super) use libwebrtc::native::apm;
pub(super) use log::info;
pub(super) use parking_lot::Mutex;
pub(super) use rodio::cpal::Sample;
pub(super) use rodio::source::{LimitSettings, UniformSourceIterator};
pub(super) use std::sync::Arc;
}
#[cfg(not(any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd")))]
use non_windows_and_freebsd_deps::*;
use rodio::{
Decoder, OutputStream, OutputStreamBuilder, Source, mixer::Mixer, nz, source::Buffered,
};
use settings::Settings;
use std::io::Cursor;
use std::{io::Cursor, num::NonZero, path::PathBuf, sync::atomic::Ordering, time::Duration};
use util::ResultExt;
mod audio_settings;
mod replays;
mod rodio_ext;
pub use audio_settings::AudioSettings;
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.
//
// 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);
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 REPLAY_DURATION: Duration = Duration::from_secs(30);
pub fn init(cx: &mut App) {
AudioSettings::register(cx);
LIVE_SETTINGS.initialize(cx);
}
#[derive(Copy, Clone, Eq, Hash, PartialEq)]
#[derive(Debug, Copy, Clone, Eq, Hash, PartialEq)]
pub enum Sound {
Joined,
Leave,
@@ -38,32 +75,152 @@ impl Sound {
}
}
#[derive(Default)]
pub struct Audio {
output_handle: Option<OutputStream>,
output_mixer: Option<Mixer>,
#[cfg(not(any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd")))]
pub echo_canceller: Arc<Mutex<apm::AudioProcessingModule>>,
source_cache: HashMap<Sound, Buffered<Decoder<Cursor<Vec<u8>>>>>,
replays: replays::Replays,
}
impl Default for Audio {
fn default() -> Self {
Self {
output_handle: Default::default(),
output_mixer: Default::default(),
#[cfg(not(any(
all(target_os = "windows", target_env = "gnu"),
target_os = "freebsd"
)))]
echo_canceller: Arc::new(Mutex::new(apm::AudioProcessingModule::new(
true, false, false, false,
))),
source_cache: Default::default(),
replays: Default::default(),
}
}
}
impl Global for Audio {}
impl Audio {
fn ensure_output_exists(&mut self) -> Option<&OutputStream> {
fn ensure_output_exists(&mut self) -> Result<&Mixer> {
if self.output_handle.is_none() {
self.output_handle = OutputStreamBuilder::open_default_stream().log_err();
self.output_handle = Some(
OutputStreamBuilder::open_default_stream()
.context("Could not open default output stream")?,
);
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.
mixer.add(rodio::source::Zero::new(CHANNEL_COUNT, SAMPLE_RATE));
self.output_mixer = Some(mixer);
// The webrtc apm is not yet compiling for windows & freebsd
#[cfg(not(any(
any(all(target_os = "windows", target_env = "gnu")),
target_os = "freebsd"
)))]
let echo_canceller = Arc::clone(&self.echo_canceller);
#[cfg(not(any(
any(all(target_os = "windows", target_env = "gnu")),
target_os = "freebsd"
)))]
let source = source.inspect_buffer::<BUFFER_SIZE, _>(move |buffer| {
let mut buf: [i16; _] = buffer.map(|s| s.to_sample());
echo_canceller
.lock()
.process_reverse_stream(
&mut buf,
SAMPLE_RATE.get() as i32,
CHANNEL_COUNT.get().into(),
)
.expect("Audio input and output threads should not panic");
});
output_handle.mixer().add(source);
}
}
self.output_handle.as_ref()
Ok(self
.output_mixer
.as_ref()
.expect("we only get here if opening the outputstream succeeded"))
}
pub fn play_source(
pub fn save_replays(
&self,
executor: BackgroundExecutor,
) -> gpui::Task<anyhow::Result<(PathBuf, Duration)>> {
self.replays.replays_to_tar(executor)
}
#[cfg(not(any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd")))]
pub fn open_microphone(voip_parts: VoipParts) -> anyhow::Result<impl Source> {
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_buffer_sizes(512..)
.open_stream()?;
info!("Opened microphone: {:?}", stream.config());
let (replay, stream) = UniformSourceIterator::new(stream, CHANNEL_COUNT, SAMPLE_RATE)
.limit(LimitSettings::live_performance())
.process_buffer::<BUFFER_SIZE, _>(move |buffer| {
let mut int_buffer: [i16; _] = buffer.map(|s| s.to_sample());
if voip_parts
.echo_canceller
.lock()
.process_stream(
&mut int_buffer,
SAMPLE_RATE.get() as i32,
CHANNEL_COUNT.get() as i32,
)
.context("livekit audio processor error")
.log_err()
.is_some()
{
for (sample, processed) in buffer.iter_mut().zip(&int_buffer) {
*sample = (*processed).to_sample();
}
}
})
.automatic_gain_control(1.0, 4.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)?;
voip_parts
.replays
.add_voip_stream("local microphone".to_string(), replay);
Ok(stream)
}
pub fn play_voip_stream(
source: impl rodio::Source + Send + 'static,
speaker_name: String,
is_staff: bool,
cx: &mut App,
) -> anyhow::Result<()> {
let (replay_source, source) = source
.automatic_gain_control(1.0, 4.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)
.expect("REPLAY_DURATION is longer then 100ms");
cx.update_default_global(|this: &mut Self, _cx| {
let output_handle = this
let output_mixer = this
.ensure_output_exists()
.ok_or_else(|| anyhow!("Could not open audio output"))?;
output_handle.mixer().add(source);
.context("Could not get output mixer")?;
output_mixer.add(source);
if is_staff {
this.replays.add_voip_stream(speaker_name, replay_source);
}
Ok(())
})
}
@@ -71,8 +228,12 @@ impl Audio {
pub fn play_sound(sound: Sound, cx: &mut App) {
cx.update_default_global(|this: &mut Self, cx| {
let source = this.sound_source(sound, cx).log_err()?;
let output_handle = this.ensure_output_exists()?;
output_handle.mixer().add(source);
let output_mixer = this
.ensure_output_exists()
.context("Could not get output mixer")
.log_err()?;
output_mixer.add(source);
Some(())
});
}
@@ -103,3 +264,23 @@ impl Audio {
Ok(source)
}
}
#[cfg(not(any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd")))]
pub struct VoipParts {
echo_canceller: Arc<Mutex<apm::AudioProcessingModule>>,
replays: replays::Replays,
}
#[cfg(not(any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd")))]
impl VoipParts {
pub fn new(cx: &AsyncApp) -> anyhow::Result<Self> {
let (apm, replays) = cx.try_read_default_global::<Audio, _>(|audio, _| {
(Arc::clone(&audio.echo_canceller), audio.replays.clone())
})?;
Ok(Self {
echo_canceller: apm,
replays,
})
}
}

View File

@@ -1,28 +1,55 @@
use std::sync::atomic::{AtomicBool, Ordering};
use anyhow::Result;
use gpui::App;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources, SettingsUi};
use settings::{Settings, SettingsKey, SettingsSources, SettingsStore, SettingsUi};
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)]
pub struct AudioSettings {
/// Opt into the new audio system.
#[serde(rename = "experimental.rodio_audio", default)]
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.
#[serde(rename = "experimental.control_input_volume", default)]
pub control_input_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.
#[serde(rename = "experimental.control_output_volume", default)]
pub control_output_volume: bool,
}
/// Configuration of audio in Zed.
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)]
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)]
#[serde(default)]
#[settings_key(key = "audio")]
pub struct AudioSettingsContent {
/// Whether to use the experimental audio system
/// Opt into the new audio system.
#[serde(rename = "experimental.rodio_audio", default)]
pub rodio_audio: bool,
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.
#[serde(rename = "experimental.control_input_volume", default)]
pub control_input_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.
#[serde(rename = "experimental.control_output_volume", default)]
pub control_output_volume: bool,
}
impl Settings for AudioSettings {
const KEY: Option<&'static str> = Some("audio");
type FileContent = AudioSettingsContent;
fn load(sources: SettingsSources<Self::FileContent>, _cx: &mut App) -> Result<Self> {
@@ -31,3 +58,42 @@ impl Settings for AudioSettings {
fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
}
/// See docs on [LIVE_SETTINGS]
pub(crate) struct LiveSettings {
pub(crate) control_input_volume: AtomicBool,
pub(crate) control_output_volume: 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,
Ordering::Relaxed,
);
LIVE_SETTINGS.control_output_volume.store(
AudioSettings::get_global(cx).control_output_volume,
Ordering::Relaxed,
);
})
.detach();
let init_settings = AudioSettings::get_global(cx);
LIVE_SETTINGS
.control_input_volume
.store(init_settings.control_input_volume, Ordering::Relaxed);
LIVE_SETTINGS
.control_output_volume
.store(init_settings.control_output_volume, Ordering::Relaxed);
}
}
/// Allows access to settings from the audio thread. Updated by
/// observer of SettingsStore. Needed because audio playback and recording are
/// 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),
};

View File

@@ -0,0 +1,77 @@
use anyhow::{Context, anyhow};
use async_tar::{Builder, Header};
use gpui::{BackgroundExecutor, Task};
use collections::HashMap;
use parking_lot::Mutex;
use rodio::Source;
use smol::fs::File;
use std::{io, path::PathBuf, sync::Arc, time::Duration};
use crate::{REPLAY_DURATION, rodio_ext::Replay};
#[derive(Default, Clone)]
pub(crate) struct Replays(Arc<Mutex<HashMap<String, Replay>>>);
impl Replays {
pub(crate) fn add_voip_stream(&self, stream_name: String, source: Replay) {
let mut map = self.0.lock();
map.retain(|_, replay| replay.source_is_active());
map.insert(stream_name, source);
}
pub(crate) fn replays_to_tar(
&self,
executor: BackgroundExecutor,
) -> Task<anyhow::Result<(PathBuf, Duration)>> {
let map = Arc::clone(&self.0);
executor.spawn(async move {
let recordings: Vec<_> = map
.lock()
.iter_mut()
.map(|(name, replay)| {
let queued = REPLAY_DURATION.min(replay.duration_ready());
(name.clone(), replay.take_duration(queued).record())
})
.collect();
let longest = recordings
.iter()
.map(|(_, r)| {
r.total_duration()
.expect("SamplesBuffer always returns a total duration")
})
.max()
.ok_or(anyhow!("There is no audio to capture"))?;
let path = std::env::current_dir()
.context("Could not get current dir")?
.join("replays.tar");
let tar = File::create(&path)
.await
.context("Could not create file for tar")?;
let mut tar = Builder::new(tar);
for (name, recording) in recordings {
let mut writer = io::Cursor::new(Vec::new());
rodio::wav_to_writer(recording, &mut writer).context("failed to encode wav")?;
let wav_data = writer.into_inner();
let path = name.replace(' ', "_") + ".wav";
let mut header = Header::new_gnu();
// rw permissions for everyone
header.set_mode(0o666);
header.set_size(wav_data.len() as u64);
tar.append_data(&mut header, path, wav_data.as_slice())
.await
.context("failed to apped wav to tar")?;
}
tar.into_inner()
.await
.context("Could not finish writing tar")?
.sync_all()
.await
.context("Could not flush tar file to disk")?;
Ok((path, longest))
})
}
}

View File

@@ -0,0 +1,598 @@
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 trait RodioExt: Source + Sized {
fn process_buffer<const N: usize, F>(self, callback: F) -> ProcessBuffer<N, Self, F>
where
F: FnMut(&mut [Sample; N]);
fn inspect_buffer<const N: usize, F>(self, callback: F) -> InspectBuffer<N, Self, F>
where
F: FnMut(&[Sample; N]);
fn replayable(
self,
duration: Duration,
) -> Result<(Replay, Replayable<Self>), ReplayDurationTooShort>;
fn take_samples(self, n: usize) -> TakeSamples<Self>;
}
impl<S: Source> RodioExt for S {
fn process_buffer<const N: usize, F>(self, callback: F) -> ProcessBuffer<N, Self, F>
where
F: FnMut(&mut [Sample; N]),
{
ProcessBuffer {
inner: self,
callback,
buffer: [0.0; N],
next: N,
}
}
fn inspect_buffer<const N: usize, F>(self, callback: F) -> InspectBuffer<N, Self, F>
where
F: FnMut(&[Sample; N]),
{
InspectBuffer {
inner: self,
callback,
buffer: [0.0; N],
free: 0,
}
}
/// Maintains a live replay with a history of at least `duration` seconds.
///
/// Note:
/// History can be 100ms longer if the source drops before or while the
/// replay is being read
///
/// # Errors
/// If duration is smaller then 100ms
fn replayable(
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,
},
))
}
fn take_samples(self, n: usize) -> TakeSamples<S> {
TakeSamples {
inner: self,
left_to_take: n,
}
}
}
pub struct TakeSamples<S> {
inner: S,
left_to_take: usize,
}
impl<S: Source> Iterator for TakeSamples<S> {
type Item = Sample;
fn next(&mut self) -> Option<Self::Item> {
if self.left_to_take == 0 {
None
} else {
self.left_to_take -= 1;
self.inner.next()
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
(0, Some(self.left_to_take))
}
}
impl<S: Source> Source for TakeSamples<S> {
fn current_span_len(&self) -> Option<usize> {
None // does not support spans
}
fn channels(&self) -> ChannelCount {
self.inner.channels()
}
fn sample_rate(&self) -> SampleRate {
self.inner.sample_rate()
}
fn total_duration(&self) -> Option<Duration> {
Some(Duration::from_secs_f64(
self.left_to_take as f64
/ self.sample_rate().get() as f64
/ self.channels().get() as f64,
))
}
}
#[derive(Debug)]
struct ReplayQueue {
inner: ArrayQueue<Vec<Sample>>,
normal_chunk_len: usize,
/// The last chunk in the queue may be smaller then
/// 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);
}
}
pub struct ProcessBuffer<const N: usize, S, F>
where
S: Source + Sized,
F: FnMut(&mut [Sample; N]),
{
inner: S,
callback: F,
/// Buffer used for both input and output.
buffer: [Sample; N],
/// Next already processed sample is at this index
/// in buffer.
///
/// If this is equal to the length of the buffer we have no more samples and
/// we must get new ones and process them
next: usize,
}
impl<const N: usize, S, F> Iterator for ProcessBuffer<N, S, F>
where
S: Source + Sized,
F: FnMut(&mut [Sample; N]),
{
type Item = Sample;
fn next(&mut self) -> Option<Self::Item> {
self.next += 1;
if self.next < self.buffer.len() {
let sample = self.buffer[self.next];
return Some(sample);
}
for sample in &mut self.buffer {
*sample = self.inner.next()?
}
(self.callback)(&mut self.buffer);
self.next = 0;
Some(self.buffer[0])
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.inner.size_hint()
}
}
impl<const N: usize, S, F> Source for ProcessBuffer<N, S, F>
where
S: Source + Sized,
F: FnMut(&mut [Sample; N]),
{
fn current_span_len(&self) -> Option<usize> {
None
}
fn channels(&self) -> rodio::ChannelCount {
self.inner.channels()
}
fn sample_rate(&self) -> rodio::SampleRate {
self.inner.sample_rate()
}
fn total_duration(&self) -> Option<std::time::Duration> {
self.inner.total_duration()
}
}
pub struct InspectBuffer<const N: usize, S, F>
where
S: Source + Sized,
F: FnMut(&[Sample; N]),
{
inner: S,
callback: F,
/// Stores already emitted samples, once its full we call the callback.
buffer: [Sample; N],
/// Next free element in buffer. If this is equal to the buffer length
/// we have no more free lements.
free: usize,
}
impl<const N: usize, S, F> Iterator for InspectBuffer<N, S, F>
where
S: Source + Sized,
F: FnMut(&[Sample; N]),
{
type Item = Sample;
fn next(&mut self) -> Option<Self::Item> {
let Some(sample) = self.inner.next() else {
return None;
};
self.buffer[self.free] = sample;
self.free += 1;
if self.free == self.buffer.len() {
(self.callback)(&self.buffer);
self.free = 0
}
Some(sample)
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.inner.size_hint()
}
}
impl<const N: usize, S, F> Source for InspectBuffer<N, S, F>
where
S: Source + Sized,
F: FnMut(&[Sample; N]),
{
fn current_span_len(&self) -> Option<usize> {
None
}
fn channels(&self) -> rodio::ChannelCount {
self.inner.channels()
}
fn sample_rate(&self) -> rodio::SampleRate {
self.inner.sample_rate()
}
fn total_duration(&self) -> Option<std::time::Duration> {
self.inner.total_duration()
}
}
#[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];
fn test_source() -> StaticSamplesBuffer {
StaticSamplesBuffer::new(nz!(1), nz!(1), &SAMPLES)
}
mod process_buffer {
use super::*;
#[test]
fn callback_gets_all_samples() {
let input = test_source();
let _ = input
.process_buffer::<{ SAMPLES.len() }, _>(|buffer| assert_eq!(*buffer, SAMPLES))
.count();
}
#[test]
fn callback_modifies_yielded() {
let input = test_source();
let yielded: Vec<_> = input
.process_buffer::<{ SAMPLES.len() }, _>(|buffer| {
for sample in buffer {
*sample += 1.0;
}
})
.collect();
assert_eq!(
yielded,
SAMPLES.into_iter().map(|s| s + 1.0).collect::<Vec<_>>()
)
}
#[test]
fn source_truncates_to_whole_buffers() {
let input = test_source();
let yielded = input
.process_buffer::<3, _>(|buffer| assert_eq!(buffer, &SAMPLES[..3]))
.count();
assert_eq!(yielded, 3)
}
}
mod inspect_buffer {
use super::*;
#[test]
fn callback_gets_all_samples() {
let input = test_source();
let _ = input
.inspect_buffer::<{ SAMPLES.len() }, _>(|buffer| assert_eq!(*buffer, SAMPLES))
.count();
}
#[test]
fn source_does_not_truncate() {
let input = test_source();
let yielded = input
.inspect_buffer::<3, _>(|buffer| assert_eq!(buffer, &SAMPLES[..3]))
.count();
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 then 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 then 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 then 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 then 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

@@ -10,7 +10,7 @@ use paths::remote_servers_dir;
use release_channel::{AppCommitSha, ReleaseChannel};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources, SettingsStore, SettingsUi};
use settings::{Settings, SettingsKey, SettingsSources, SettingsStore, SettingsUi};
use smol::{fs, io::AsyncReadExt};
use smol::{fs::File, process::Command};
use std::{
@@ -118,13 +118,12 @@ struct AutoUpdateSetting(bool);
/// Whether or not to automatically check for updates.
///
/// Default: true
#[derive(Clone, Copy, Default, JsonSchema, Deserialize, Serialize, SettingsUi)]
#[derive(Clone, Copy, Default, JsonSchema, Deserialize, Serialize, SettingsUi, SettingsKey)]
#[serde(transparent)]
#[settings_key(key = "auto_update")]
struct AutoUpdateSettingContent(bool);
impl Settings for AutoUpdateSetting {
const KEY: Option<&'static str> = Some("auto_update");
type FileContent = AutoUpdateSettingContent;
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {

View File

@@ -88,10 +88,7 @@ fn view_release_notes_locally(
.update_in(cx, |workspace, window, cx| {
let project = workspace.project().clone();
let buffer = project.update(cx, |project, cx| {
let buffer = project.create_local_buffer("", markdown, cx);
project
.mark_buffer_as_non_searchable(buffer.read(cx).remote_id(), cx);
buffer
project.create_local_buffer("", markdown, false, cx)
});
buffer.update(cx, |buffer, cx| {
buffer.edit([(0..0, body.release_notes)], None, cx)

View File

@@ -2044,10 +2044,10 @@ mod tests {
#[gpui::test(iterations = 100)]
async fn test_staging_and_unstaging_hunks(cx: &mut TestAppContext, mut rng: StdRng) {
fn gen_line(rng: &mut StdRng) -> String {
if rng.gen_bool(0.2) {
if rng.random_bool(0.2) {
"\n".to_owned()
} else {
let c = rng.gen_range('A'..='Z');
let c = rng.random_range('A'..='Z');
format!("{c}{c}{c}\n")
}
}
@@ -2066,7 +2066,7 @@ mod tests {
old_lines.into_iter()
};
let mut result = String::new();
let unchanged_count = rng.gen_range(0..=old_lines.len());
let unchanged_count = rng.random_range(0..=old_lines.len());
result +=
&old_lines
.by_ref()
@@ -2076,14 +2076,14 @@ mod tests {
s
});
while old_lines.len() > 0 {
let deleted_count = rng.gen_range(0..=old_lines.len());
let deleted_count = rng.random_range(0..=old_lines.len());
let _advance = old_lines
.by_ref()
.take(deleted_count)
.map(|line| line.len() + 1)
.sum::<usize>();
let minimum_added = if deleted_count == 0 { 1 } else { 0 };
let added_count = rng.gen_range(minimum_added..=5);
let added_count = rng.random_range(minimum_added..=5);
let addition = (0..added_count).map(|_| gen_line(rng)).collect::<String>();
result += &addition;
@@ -2092,7 +2092,8 @@ mod tests {
if blank_lines == old_lines.len() {
break;
};
let unchanged_count = rng.gen_range((blank_lines + 1).max(1)..=old_lines.len());
let unchanged_count =
rng.random_range((blank_lines + 1).max(1)..=old_lines.len());
result += &old_lines.by_ref().take(unchanged_count).fold(
String::new(),
|mut s, line| {
@@ -2149,7 +2150,7 @@ mod tests {
)
});
let working_copy = working_copy.read_with(cx, |working_copy, _| working_copy.snapshot());
let mut index_text = if rng.r#gen() {
let mut index_text = if rng.random() {
Rope::from(head_text.as_str())
} else {
working_copy.as_rope().clone()
@@ -2165,7 +2166,7 @@ mod tests {
}
for _ in 0..operations {
let i = rng.gen_range(0..hunks.len());
let i = rng.random_range(0..hunks.len());
let hunk = &mut hunks[i];
let hunk_to_change = hunk.clone();
let stage = match hunk.secondary_status {

View File

@@ -29,6 +29,7 @@ client.workspace = true
collections.workspace = true
fs.workspace = true
futures.workspace = true
feature_flags.workspace = true
gpui = { workspace = true, features = ["screen-capture"] }
language.workspace = true
log.workspace = true

View File

@@ -9,6 +9,7 @@ use client::{
proto::{self, PeerId},
};
use collections::{BTreeMap, HashMap, HashSet};
use feature_flags::FeatureFlagAppExt;
use fs::Fs;
use futures::StreamExt;
use gpui::{
@@ -1322,8 +1323,18 @@ impl Room {
return Task::ready(Err(anyhow!("live-kit was not initialized")));
};
let is_staff = cx.is_staff();
let user_name = self
.user_store
.read(cx)
.current_user()
.and_then(|user| user.name.clone())
.unwrap_or_else(|| "unknown".to_string());
cx.spawn(async move |this, cx| {
let publication = room.publish_local_microphone_track(cx).await;
let publication = room
.publish_local_microphone_track(user_name, is_staff, cx)
.await;
this.update(cx, |this, cx| {
let live_kit = this
.live_kit

View File

@@ -2,7 +2,7 @@ use anyhow::Result;
use gpui::App;
use schemars::JsonSchema;
use serde_derive::{Deserialize, Serialize};
use settings::{Settings, SettingsSources, SettingsUi};
use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
#[derive(Deserialize, Debug)]
pub struct CallSettings {
@@ -11,7 +11,8 @@ pub struct CallSettings {
}
/// Configuration of voice calls in Zed.
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)]
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)]
#[settings_key(key = "calls")]
pub struct CallSettingsContent {
/// Whether the microphone should be muted when joining a channel or a call.
///
@@ -25,8 +26,6 @@ pub struct CallSettingsContent {
}
impl Settings for CallSettings {
const KEY: Option<&'static str> = Some("calls");
type FileContent = CallSettingsContent;
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {

View File

@@ -129,7 +129,7 @@ impl ChannelChat {
loaded_all_messages: false,
next_pending_message_id: 0,
last_acknowledged_id: None,
rng: StdRng::from_entropy(),
rng: StdRng::from_os_rng(),
first_loaded_message_id: None,
_subscription: subscription.set_entity(&cx.entity(), &cx.to_async()),
}
@@ -183,7 +183,7 @@ impl ChannelChat {
let channel_id = self.channel_id;
let pending_id = ChannelMessageId::Pending(post_inc(&mut self.next_pending_message_id));
let nonce = self.rng.r#gen();
let nonce = self.rng.random();
self.insert_messages(
SumTree::from_item(
ChannelMessage {
@@ -257,7 +257,7 @@ impl ChannelChat {
cx,
);
let nonce: u128 = self.rng.r#gen();
let nonce: u128 = self.rng.random();
let request = self.rpc.request(proto::UpdateChannelMessage {
channel_id: self.channel_id.0,

View File

@@ -75,7 +75,7 @@ util = { workspace = true, features = ["test-support"] }
windows.workspace = true
[target.'cfg(target_os = "macos")'.dependencies]
cocoa.workspace = true
objc2-foundation.workspace = true
[target.'cfg(any(target_os = "windows", target_os = "macos"))'.dependencies]
tokio-native-tls = "0.3"

View File

@@ -31,7 +31,7 @@ use release_channel::{AppVersion, ReleaseChannel};
use rpc::proto::{AnyTypedEnvelope, EnvelopedMessage, PeerId, RequestMessage};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources, SettingsUi};
use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
use std::{
any::TypeId,
convert::TryFrom,
@@ -96,7 +96,8 @@ actions!(
]
);
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi)]
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)]
#[settings_key(None)]
pub struct ClientSettingsContent {
server_url: Option<String>,
}
@@ -107,8 +108,6 @@ pub struct ClientSettings {
}
impl Settings for ClientSettings {
const KEY: Option<&'static str> = None;
type FileContent = ClientSettingsContent;
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
@@ -122,7 +121,8 @@ impl Settings for ClientSettings {
fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
}
#[derive(Default, Clone, Serialize, Deserialize, JsonSchema, SettingsUi)]
#[derive(Default, Clone, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)]
#[settings_key(None)]
pub struct ProxySettingsContent {
proxy: Option<String>,
}
@@ -133,8 +133,6 @@ pub struct ProxySettings {
}
impl Settings for ProxySettings {
const KEY: Option<&'static str> = None;
type FileContent = ProxySettingsContent;
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
@@ -527,7 +525,8 @@ pub struct TelemetrySettings {
}
/// Control what info is collected by Zed.
#[derive(Default, Clone, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)]
#[derive(Default, Clone, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)]
#[settings_key(key = "telemetry")]
pub struct TelemetrySettingsContent {
/// Send debug info like crash reports.
///
@@ -540,8 +539,6 @@ pub struct TelemetrySettingsContent {
}
impl settings::Settings for TelemetrySettings {
const KEY: Option<&'static str> = Some("telemetry");
type FileContent = TelemetrySettingsContent;
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
@@ -691,7 +688,7 @@ impl Client {
#[cfg(any(test, feature = "test-support"))]
let mut rng = StdRng::seed_from_u64(0);
#[cfg(not(any(test, feature = "test-support")))]
let mut rng = StdRng::from_entropy();
let mut rng = StdRng::from_os_rng();
let mut delay = INITIAL_RECONNECTION_DELAY;
loop {
@@ -721,8 +718,9 @@ impl Client {
},
cx,
);
let jitter =
Duration::from_millis(rng.gen_range(0..delay.as_millis() as u64));
let jitter = Duration::from_millis(
rng.random_range(0..delay.as_millis() as u64),
);
cx.background_executor().timer(delay + jitter).await;
delay = cmp::min(delay * 2, MAX_RECONNECTION_DELAY);
} else {

View File

@@ -84,6 +84,10 @@ static DOTNET_PROJECT_FILES_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"^(global\.json|Directory\.Build\.props|.*\.(csproj|fsproj|vbproj|sln))$").unwrap()
});
#[cfg(target_os = "macos")]
static MACOS_VERSION_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"(\s*\(Build [^)]*[0-9]\))").unwrap());
pub fn os_name() -> String {
#[cfg(target_os = "macos")]
{
@@ -108,19 +112,16 @@ pub fn os_name() -> String {
pub fn os_version() -> String {
#[cfg(target_os = "macos")]
{
use cocoa::base::nil;
use cocoa::foundation::NSProcessInfo;
unsafe {
let process_info = cocoa::foundation::NSProcessInfo::processInfo(nil);
let version = process_info.operatingSystemVersion();
gpui::SemanticVersion::new(
version.majorVersion as usize,
version.minorVersion as usize,
version.patchVersion as usize,
)
use objc2_foundation::NSProcessInfo;
let process_info = NSProcessInfo::processInfo();
let version_nsstring = unsafe { process_info.operatingSystemVersionString() };
// "Version 15.6.1 (Build 24G90)" -> "15.6.1 (Build 24G90)"
let version_string = version_nsstring.to_string().replace("Version ", "");
// "15.6.1 (Build 24G90)" -> "15.6.1"
// "26.0.0 (Build 25A5349a)" -> unchanged (Beta or Rapid Security Response; ends with letter)
MACOS_VERSION_REGEX
.replace_all(&version_string, "")
.to_string()
}
}
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
{

View File

@@ -227,7 +227,7 @@ pub async fn verify_access_token(
#[cfg(test)]
mod test {
use rand::thread_rng;
use rand::prelude::*;
use scrypt::password_hash::{PasswordHasher, SaltString};
use sea_orm::EntityTrait;
@@ -358,9 +358,42 @@ mod test {
None,
None,
params,
&SaltString::generate(thread_rng()),
&SaltString::generate(PasswordHashRngCompat::new()),
)
.map_err(anyhow::Error::new)?
.to_string())
}
// TODO: remove once we password_hash v0.6 is released.
struct PasswordHashRngCompat(rand::rngs::ThreadRng);
impl PasswordHashRngCompat {
fn new() -> Self {
Self(rand::rng())
}
}
impl scrypt::password_hash::rand_core::RngCore for PasswordHashRngCompat {
fn next_u32(&mut self) -> u32 {
self.0.next_u32()
}
fn next_u64(&mut self) -> u64 {
self.0.next_u64()
}
fn fill_bytes(&mut self, dest: &mut [u8]) {
self.0.fill_bytes(dest);
}
fn try_fill_bytes(
&mut self,
dest: &mut [u8],
) -> Result<(), scrypt::password_hash::rand_core::Error> {
self.fill_bytes(dest);
Ok(())
}
}
impl scrypt::password_hash::rand_core::CryptoRng for PasswordHashRngCompat {}
}

View File

@@ -256,7 +256,7 @@ impl Database {
let test_options = self.test_options.as_ref().unwrap();
test_options.executor.simulate_random_delay().await;
let fail_probability = *test_options.query_failure_probability.lock();
if test_options.executor.rng().gen_bool(fail_probability) {
if test_options.executor.rng().random_bool(fail_probability) {
return Err(anyhow!("simulated query failure"))?;
}

View File

@@ -75,10 +75,10 @@ impl TestDb {
static LOCK: Mutex<()> = Mutex::new(());
let _guard = LOCK.lock();
let mut rng = StdRng::from_entropy();
let mut rng = StdRng::from_os_rng();
let url = format!(
"postgres://postgres@localhost/zed-test-{}",
rng.r#gen::<u128>()
rng.random::<u128>()
);
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_io()

View File

@@ -2098,7 +2098,7 @@ async fn test_following_after_replacement(cx_a: &mut TestAppContext, cx_b: &mut
share_workspace(&workspace, cx_a).await.unwrap();
let buffer = workspace.update(cx_a, |workspace, cx| {
workspace.project().update(cx, |project, cx| {
project.create_local_buffer(&sample_text(26, 5, 'a'), None, cx)
project.create_local_buffer(&sample_text(26, 5, 'a'), None, false, cx)
})
});
let multibuffer = cx_a.new(|cx| {

View File

@@ -2506,7 +2506,7 @@ async fn test_propagate_saves_and_fs_changes(
});
let new_buffer_a = project_a
.update(cx_a, |p, cx| p.create_buffer(cx))
.update(cx_a, |p, cx| p.create_buffer(false, cx))
.await
.unwrap();
@@ -5746,7 +5746,7 @@ async fn test_open_buffer_while_getting_definition_pointing_to_it(
let definitions;
let buffer_b2;
if rng.r#gen() {
if rng.random() {
cx_a.run_until_parked();
cx_b.run_until_parked();
definitions = project_b.update(cx_b, |p, cx| p.definitions(&buffer_b1, 23, cx));

View File

@@ -84,7 +84,7 @@ impl RandomizedTest for RandomChannelBufferTest {
}
loop {
match rng.gen_range(0..100_u32) {
match rng.random_range(0..100_u32) {
0..=29 => {
let channel_name = client.channel_store().read_with(cx, |store, cx| {
store.ordered_channels().find_map(|(_, channel)| {

View File

@@ -17,7 +17,7 @@ use project::{
DEFAULT_COMPLETION_CONTEXT, Project, ProjectPath, search::SearchQuery, search::SearchResult,
};
use rand::{
distributions::{Alphanumeric, DistString},
distr::{self, SampleString},
prelude::*,
};
use serde::{Deserialize, Serialize};
@@ -168,19 +168,19 @@ impl RandomizedTest for ProjectCollaborationTest {
) -> ClientOperation {
let call = cx.read(ActiveCall::global);
loop {
match rng.gen_range(0..100_u32) {
match rng.random_range(0..100_u32) {
// Mutate the call
0..=29 => {
// Respond to an incoming call
if call.read_with(cx, |call, _| call.incoming().borrow().is_some()) {
break if rng.gen_bool(0.7) {
break if rng.random_bool(0.7) {
ClientOperation::AcceptIncomingCall
} else {
ClientOperation::RejectIncomingCall
};
}
match rng.gen_range(0..100_u32) {
match rng.random_range(0..100_u32) {
// Invite a contact to the current call
0..=70 => {
let available_contacts =
@@ -212,7 +212,7 @@ impl RandomizedTest for ProjectCollaborationTest {
}
// Mutate projects
30..=59 => match rng.gen_range(0..100_u32) {
30..=59 => match rng.random_range(0..100_u32) {
// Open a new project
0..=70 => {
// Open a remote project
@@ -270,7 +270,7 @@ impl RandomizedTest for ProjectCollaborationTest {
}
// Mutate project worktrees
81.. => match rng.gen_range(0..100_u32) {
81.. => match rng.random_range(0..100_u32) {
// Add a worktree to a local project
0..=50 => {
let Some(project) = client.local_projects().choose(rng).cloned() else {
@@ -279,7 +279,7 @@ impl RandomizedTest for ProjectCollaborationTest {
let project_root_name = root_name_for_project(&project, cx);
let mut paths = client.fs().paths(false);
paths.remove(0);
let new_root_path = if paths.is_empty() || rng.r#gen() {
let new_root_path = if paths.is_empty() || rng.random() {
Path::new(path!("/")).join(plan.next_root_dir_name())
} else {
paths.choose(rng).unwrap().clone()
@@ -309,7 +309,7 @@ impl RandomizedTest for ProjectCollaborationTest {
.choose(rng)
});
let Some(worktree) = worktree else { continue };
let is_dir = rng.r#gen::<bool>();
let is_dir = rng.random::<bool>();
let mut full_path =
worktree.read_with(cx, |w, _| PathBuf::from(w.root_name()));
full_path.push(gen_file_name(rng));
@@ -334,7 +334,7 @@ impl RandomizedTest for ProjectCollaborationTest {
let project_root_name = root_name_for_project(&project, cx);
let is_local = project.read_with(cx, |project, _| project.is_local());
match rng.gen_range(0..100_u32) {
match rng.random_range(0..100_u32) {
// Manipulate an existing buffer
0..=70 => {
let Some(buffer) = client
@@ -349,7 +349,7 @@ impl RandomizedTest for ProjectCollaborationTest {
let full_path = buffer
.read_with(cx, |buffer, cx| buffer.file().unwrap().full_path(cx));
match rng.gen_range(0..100_u32) {
match rng.random_range(0..100_u32) {
// Close the buffer
0..=15 => {
break ClientOperation::CloseBuffer {
@@ -360,7 +360,7 @@ impl RandomizedTest for ProjectCollaborationTest {
}
// Save the buffer
16..=29 if buffer.read_with(cx, |b, _| b.is_dirty()) => {
let detach = rng.gen_bool(0.3);
let detach = rng.random_bool(0.3);
break ClientOperation::SaveBuffer {
project_root_name,
is_local,
@@ -383,17 +383,17 @@ impl RandomizedTest for ProjectCollaborationTest {
_ => {
let offset = buffer.read_with(cx, |buffer, _| {
buffer.clip_offset(
rng.gen_range(0..=buffer.len()),
rng.random_range(0..=buffer.len()),
language::Bias::Left,
)
});
let detach = rng.r#gen();
let detach = rng.random();
break ClientOperation::RequestLspDataInBuffer {
project_root_name,
full_path,
offset,
is_local,
kind: match rng.gen_range(0..5_u32) {
kind: match rng.random_range(0..5_u32) {
0 => LspRequestKind::Rename,
1 => LspRequestKind::Highlights,
2 => LspRequestKind::Definition,
@@ -407,8 +407,8 @@ impl RandomizedTest for ProjectCollaborationTest {
}
71..=80 => {
let query = rng.gen_range('a'..='z').to_string();
let detach = rng.gen_bool(0.3);
let query = rng.random_range('a'..='z').to_string();
let detach = rng.random_bool(0.3);
break ClientOperation::SearchProject {
project_root_name,
is_local,
@@ -460,7 +460,7 @@ impl RandomizedTest for ProjectCollaborationTest {
// Create or update a file or directory
96.. => {
let is_dir = rng.r#gen::<bool>();
let is_dir = rng.random::<bool>();
let content;
let mut path;
let dir_paths = client.fs().directories(false);
@@ -470,11 +470,11 @@ impl RandomizedTest for ProjectCollaborationTest {
path = dir_paths.choose(rng).unwrap().clone();
path.push(gen_file_name(rng));
} else {
content = Alphanumeric.sample_string(rng, 16);
content = distr::Alphanumeric.sample_string(rng, 16);
// Create a new file or overwrite an existing file
let file_paths = client.fs().files();
if file_paths.is_empty() || rng.gen_bool(0.5) {
if file_paths.is_empty() || rng.random_bool(0.5) {
path = dir_paths.choose(rng).unwrap().clone();
path.push(gen_file_name(rng));
path.set_extension("rs");
@@ -1090,7 +1090,7 @@ impl RandomizedTest for ProjectCollaborationTest {
move |_, cx| {
let background = cx.background_executor();
let mut rng = background.rng();
let count = rng.gen_range::<usize, _>(1..3);
let count = rng.random_range::<usize, _>(1..3);
let files = fs.as_fake().files();
let files = (0..count)
.map(|_| files.choose(&mut rng).unwrap().clone())
@@ -1117,12 +1117,12 @@ impl RandomizedTest for ProjectCollaborationTest {
let background = cx.background_executor();
let mut rng = background.rng();
let highlight_count = rng.gen_range(1..=5);
let highlight_count = rng.random_range(1..=5);
for _ in 0..highlight_count {
let start_row = rng.gen_range(0..100);
let start_column = rng.gen_range(0..100);
let end_row = rng.gen_range(0..100);
let end_column = rng.gen_range(0..100);
let start_row = rng.random_range(0..100);
let start_column = rng.random_range(0..100);
let end_row = rng.random_range(0..100);
let end_column = rng.random_range(0..100);
let start = PointUtf16::new(start_row, start_column);
let end = PointUtf16::new(end_row, end_column);
let range =
@@ -1219,8 +1219,8 @@ impl RandomizedTest for ProjectCollaborationTest {
guest_project.remote_id(),
);
assert_eq!(
guest_snapshot.entries(false, 0).collect::<Vec<_>>(),
host_snapshot.entries(false, 0).collect::<Vec<_>>(),
guest_snapshot.entries(false, 0).map(null_out_entry_size).collect::<Vec<_>>(),
host_snapshot.entries(false, 0).map(null_out_entry_size).collect::<Vec<_>>(),
"{} has different snapshot than the host for worktree {:?} ({:?}) and project {:?}",
client.username,
host_snapshot.abs_path(),
@@ -1248,6 +1248,18 @@ impl RandomizedTest for ProjectCollaborationTest {
);
}
});
// A hack to work around a hack in
// https://github.com/zed-industries/zed/pull/16696 that wasn't
// detected until we upgraded the rng crate. This whole crate is
// going away with DeltaDB soon, so we hold our nose and
// continue.
fn null_out_entry_size(entry: &project::Entry) -> project::Entry {
project::Entry {
size: 0,
..entry.clone()
}
}
}
let buffers = client.buffers().clone();
@@ -1422,7 +1434,7 @@ fn generate_git_operation(rng: &mut StdRng, client: &TestClient) -> GitOperation
.filter(|path| path.starts_with(repo_path))
.collect::<Vec<_>>();
let count = rng.gen_range(0..=paths.len());
let count = rng.random_range(0..=paths.len());
paths.shuffle(rng);
paths.truncate(count);
@@ -1434,13 +1446,13 @@ fn generate_git_operation(rng: &mut StdRng, client: &TestClient) -> GitOperation
let repo_path = client.fs().directories(false).choose(rng).unwrap().clone();
match rng.gen_range(0..100_u32) {
match rng.random_range(0..100_u32) {
0..=25 => {
let file_paths = generate_file_paths(&repo_path, rng, client);
let contents = file_paths
.into_iter()
.map(|path| (path, Alphanumeric.sample_string(rng, 16)))
.map(|path| (path, distr::Alphanumeric.sample_string(rng, 16)))
.collect();
GitOperation::WriteGitIndex {
@@ -1449,7 +1461,8 @@ fn generate_git_operation(rng: &mut StdRng, client: &TestClient) -> GitOperation
}
}
26..=63 => {
let new_branch = (rng.gen_range(0..10) > 3).then(|| Alphanumeric.sample_string(rng, 8));
let new_branch =
(rng.random_range(0..10) > 3).then(|| distr::Alphanumeric.sample_string(rng, 8));
GitOperation::WriteGitBranch {
repo_path,
@@ -1596,7 +1609,7 @@ fn choose_random_project(client: &TestClient, rng: &mut StdRng) -> Option<Entity
fn gen_file_name(rng: &mut StdRng) -> String {
let mut name = String::new();
for _ in 0..10 {
let letter = rng.gen_range('a'..='z');
let letter = rng.random_range('a'..='z');
name.push(letter);
}
name
@@ -1604,7 +1617,7 @@ fn gen_file_name(rng: &mut StdRng) -> String {
fn gen_status(rng: &mut StdRng) -> FileStatus {
fn gen_tracked_status(rng: &mut StdRng) -> TrackedStatus {
match rng.gen_range(0..3) {
match rng.random_range(0..3) {
0 => TrackedStatus {
index_status: StatusCode::Unmodified,
worktree_status: StatusCode::Unmodified,
@@ -1626,7 +1639,7 @@ fn gen_status(rng: &mut StdRng) -> FileStatus {
}
fn gen_unmerged_status_code(rng: &mut StdRng) -> UnmergedStatusCode {
match rng.gen_range(0..3) {
match rng.random_range(0..3) {
0 => UnmergedStatusCode::Updated,
1 => UnmergedStatusCode::Added,
2 => UnmergedStatusCode::Deleted,
@@ -1634,7 +1647,7 @@ fn gen_status(rng: &mut StdRng) -> FileStatus {
}
}
match rng.gen_range(0..2) {
match rng.random_range(0..2) {
0 => FileStatus::Unmerged(UnmergedStatus {
first_head: gen_unmerged_status_code(rng),
second_head: gen_unmerged_status_code(rng),

View File

@@ -208,9 +208,9 @@ pub fn save_randomized_test_plan() {
impl<T: RandomizedTest> TestPlan<T> {
pub async fn new(server: &mut TestServer, mut rng: StdRng) -> Arc<Mutex<Self>> {
let allow_server_restarts = rng.gen_bool(0.7);
let allow_client_reconnection = rng.gen_bool(0.7);
let allow_client_disconnection = rng.gen_bool(0.1);
let allow_server_restarts = rng.random_bool(0.7);
let allow_client_reconnection = rng.random_bool(0.7);
let allow_client_disconnection = rng.random_bool(0.1);
let mut users = Vec::new();
for ix in 0..max_peers() {
@@ -407,7 +407,7 @@ impl<T: RandomizedTest> TestPlan<T> {
}
Some(loop {
break match self.rng.gen_range(0..100) {
break match self.rng.random_range(0..100) {
0..=29 if clients.len() < self.users.len() => {
let user = self
.users
@@ -421,13 +421,13 @@ impl<T: RandomizedTest> TestPlan<T> {
}
}
30..=34 if clients.len() > 1 && self.allow_client_disconnection => {
let (client, cx) = &clients[self.rng.gen_range(0..clients.len())];
let (client, cx) = &clients[self.rng.random_range(0..clients.len())];
let user_id = client.current_user_id(cx);
self.operation_ix += 1;
ServerOperation::RemoveConnection { user_id }
}
35..=39 if clients.len() > 1 && self.allow_client_reconnection => {
let (client, cx) = &clients[self.rng.gen_range(0..clients.len())];
let (client, cx) = &clients[self.rng.random_range(0..clients.len())];
let user_id = client.current_user_id(cx);
self.operation_ix += 1;
ServerOperation::BounceConnection { user_id }
@@ -439,12 +439,12 @@ impl<T: RandomizedTest> TestPlan<T> {
_ if !clients.is_empty() => {
let count = self
.rng
.gen_range(1..10)
.random_range(1..10)
.min(self.max_operations - self.operation_ix);
let batch_id = util::post_inc(&mut self.next_batch_id);
let mut user_ids = (0..count)
.map(|_| {
let ix = self.rng.gen_range(0..clients.len());
let ix = self.rng.random_range(0..clients.len());
let (client, cx) = &clients[ix];
client.current_user_id(cx)
})
@@ -453,7 +453,7 @@ impl<T: RandomizedTest> TestPlan<T> {
ServerOperation::MutateClients {
user_ids,
batch_id,
quiesce: self.rng.gen_bool(0.7),
quiesce: self.rng.random_bool(0.7),
}
}
_ => continue,

View File

@@ -1,7 +1,7 @@
use gpui::Pixels;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources, SettingsUi};
use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
use workspace::dock::DockPosition;
#[derive(Deserialize, Debug)]
@@ -27,7 +27,8 @@ pub struct ChatPanelSettings {
pub default_width: Pixels,
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)]
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)]
#[settings_key(key = "chat_panel")]
pub struct ChatPanelSettingsContent {
/// When to show the panel button in the status bar.
///
@@ -43,14 +44,8 @@ pub struct ChatPanelSettingsContent {
pub default_width: Option<f32>,
}
#[derive(Deserialize, Debug)]
pub struct NotificationPanelSettings {
pub button: bool,
pub dock: DockPosition,
pub default_width: Pixels,
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)]
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)]
#[settings_key(key = "collaboration_panel")]
pub struct PanelSettingsContent {
/// Whether to show the panel button in the status bar.
///
@@ -66,7 +61,32 @@ pub struct PanelSettingsContent {
pub default_width: Option<f32>,
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)]
#[derive(Deserialize, Debug)]
pub struct NotificationPanelSettings {
pub button: bool,
pub dock: DockPosition,
pub default_width: Pixels,
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)]
#[settings_key(key = "notification_panel")]
pub struct NotificationPanelSettingsContent {
/// Whether to show the panel button in the status bar.
///
/// Default: true
pub button: Option<bool>,
/// Where to dock the panel.
///
/// Default: right
pub dock: Option<DockPosition>,
/// Default width of the panel in pixels.
///
/// Default: 300
pub default_width: Option<f32>,
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)]
#[settings_key(key = "message_editor")]
pub struct MessageEditorSettings {
/// Whether to automatically replace emoji shortcodes with emoji characters.
/// For example: typing `:wave:` gets replaced with `👋`.
@@ -76,8 +96,6 @@ pub struct MessageEditorSettings {
}
impl Settings for CollaborationPanelSettings {
const KEY: Option<&'static str> = Some("collaboration_panel");
type FileContent = PanelSettingsContent;
fn load(
@@ -91,8 +109,6 @@ impl Settings for CollaborationPanelSettings {
}
impl Settings for ChatPanelSettings {
const KEY: Option<&'static str> = Some("chat_panel");
type FileContent = ChatPanelSettingsContent;
fn load(
@@ -106,9 +122,7 @@ impl Settings for ChatPanelSettings {
}
impl Settings for NotificationPanelSettings {
const KEY: Option<&'static str> = Some("notification_panel");
type FileContent = PanelSettingsContent;
type FileContent = NotificationPanelSettingsContent;
fn load(
sources: SettingsSources<Self::FileContent>,
@@ -121,8 +135,6 @@ impl Settings for NotificationPanelSettings {
}
impl Settings for MessageEditorSettings {
const KEY: Option<&'static str> = Some("message_editor");
type FileContent = MessageEditorSettings;
fn load(

View File

@@ -76,7 +76,7 @@ impl CommandPaletteFilter {
}
/// Hides all actions with the given types.
pub fn hide_action_types(&mut self, action_types: &[TypeId]) {
pub fn hide_action_types<'a>(&mut self, action_types: impl IntoIterator<Item = &'a TypeId>) {
for action_type in action_types {
self.hidden_action_types.insert(*action_type);
self.shown_action_types.remove(action_type);
@@ -84,7 +84,7 @@ impl CommandPaletteFilter {
}
/// Shows all actions with the given types.
pub fn show_action_types<'a>(&mut self, action_types: impl Iterator<Item = &'a TypeId>) {
pub fn show_action_types<'a>(&mut self, action_types: impl IntoIterator<Item = &'a TypeId>) {
for action_type in action_types {
self.shown_action_types.insert(*action_type);
self.hidden_action_types.remove(action_type);

View File

@@ -20,5 +20,8 @@ strum.workspace = true
theme.workspace = true
workspace-hack.workspace = true
[dev-dependencies]
documented.workspace = true
[features]
default = []

View File

@@ -227,6 +227,8 @@ pub trait Component {
/// Example:
///
/// ```
/// use documented::Documented;
///
/// /// This is a doc comment.
/// #[derive(Documented)]
/// struct MyComponent;

View File

@@ -1095,7 +1095,7 @@ impl Copilot {
_ => {
filter.hide_action_types(&signed_in_actions);
filter.hide_action_types(&auth_actions);
filter.show_action_types(no_auth_actions.iter());
filter.show_action_types(&no_auth_actions);
}
}
}

View File

@@ -2,7 +2,7 @@ use dap_types::SteppingGranularity;
use gpui::{App, Global};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources, SettingsUi};
use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, SettingsUi)]
#[serde(rename_all = "snake_case")]
@@ -12,11 +12,12 @@ pub enum DebugPanelDockPosition {
Right,
}
#[derive(Serialize, Deserialize, JsonSchema, Clone, Copy, SettingsUi)]
#[derive(Serialize, Deserialize, JsonSchema, Clone, Copy, SettingsUi, SettingsKey)]
#[serde(default)]
// todo(settings_ui) @ben: I'm pretty sure not having the fields be optional here is a bug,
// it means the defaults will override previously set values if a single key is missing
#[settings_ui(group = "Debugger", path = "debugger")]
#[settings_ui(group = "Debugger")]
#[settings_key(key = "debugger")]
pub struct DebuggerSettings {
/// Determines the stepping granularity.
///
@@ -64,8 +65,6 @@ impl Default for DebuggerSettings {
}
impl Settings for DebuggerSettings {
const KEY: Option<&'static str> = Some("debugger");
type FileContent = Self;
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> anyhow::Result<Self> {

View File

@@ -1,8 +1,10 @@
use dap::{DapRegistry, DebugRequest};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{AppContext, DismissEvent, Entity, EventEmitter, Focusable, Render};
use gpui::{AppContext, DismissEvent, Entity, EventEmitter, Focusable, Render, Task};
use gpui::{Subscription, WeakEntity};
use picker::{Picker, PickerDelegate};
use project::Project;
use rpc::proto;
use task::ZedDebugConfig;
use util::debug_panic;
@@ -56,29 +58,28 @@ impl AttachModal {
pub fn new(
definition: ZedDebugConfig,
workspace: WeakEntity<Workspace>,
project: Entity<Project>,
modal: bool,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let mut processes: Box<[_]> = System::new_all()
.processes()
.values()
.map(|process| {
let name = process.name().to_string_lossy().into_owned();
Candidate {
name: name.into(),
pid: process.pid().as_u32(),
command: process
.cmd()
.iter()
.map(|s| s.to_string_lossy().to_string())
.collect::<Vec<_>>(),
}
})
.collect();
processes.sort_by_key(|k| k.name.clone());
let processes = processes.into_iter().collect();
Self::with_processes(workspace, definition, processes, modal, window, cx)
let processes_task = get_processes_for_project(&project, cx);
let modal = Self::with_processes(workspace, definition, Arc::new([]), modal, window, cx);
cx.spawn_in(window, async move |this, cx| {
let processes = processes_task.await;
this.update_in(cx, |modal, window, cx| {
modal.picker.update(cx, |picker, cx| {
picker.delegate.candidates = processes;
picker.refresh(window, cx);
});
})?;
anyhow::Ok(())
})
.detach_and_log_err(cx);
modal
}
pub(super) fn with_processes(
@@ -332,6 +333,57 @@ impl PickerDelegate for AttachModalDelegate {
}
}
fn get_processes_for_project(project: &Entity<Project>, cx: &mut App) -> Task<Arc<[Candidate]>> {
let project = project.read(cx);
if let Some(remote_client) = project.remote_client() {
let proto_client = remote_client.read(cx).proto_client();
cx.spawn(async move |_cx| {
let response = proto_client
.request(proto::GetProcesses {
project_id: proto::REMOTE_SERVER_PROJECT_ID,
})
.await
.unwrap_or_else(|_| proto::GetProcessesResponse {
processes: Vec::new(),
});
let mut processes: Vec<Candidate> = response
.processes
.into_iter()
.map(|p| Candidate {
pid: p.pid,
name: p.name.into(),
command: p.command,
})
.collect();
processes.sort_by_key(|k| k.name.clone());
Arc::from(processes.into_boxed_slice())
})
} else {
let mut processes: Box<[_]> = System::new_all()
.processes()
.values()
.map(|process| {
let name = process.name().to_string_lossy().into_owned();
Candidate {
name: name.into(),
pid: process.pid().as_u32(),
command: process
.cmd()
.iter()
.map(|s| s.to_string_lossy().to_string())
.collect::<Vec<_>>(),
}
})
.collect();
processes.sort_by_key(|k| k.name.clone());
let processes = processes.into_iter().collect();
Task::ready(processes)
}
}
#[cfg(any(test, feature = "test-support"))]
pub(crate) fn _process_names(modal: &AttachModal, cx: &mut Context<AttachModal>) -> Vec<String> {
modal.picker.read_with(cx, |picker, _| {

View File

@@ -13,11 +13,8 @@ use anyhow::{Context as _, Result, anyhow};
use collections::IndexMap;
use dap::adapters::DebugAdapterName;
use dap::debugger_settings::DebugPanelDockPosition;
use dap::{
ContinuedEvent, LoadedSourceEvent, ModuleEvent, OutputEvent, StoppedEvent, ThreadEvent,
client::SessionId, debugger_settings::DebuggerSettings,
};
use dap::{DapRegistry, StartDebuggingRequestArguments};
use dap::{client::SessionId, debugger_settings::DebuggerSettings};
use editor::Editor;
use gpui::{
Action, App, AsyncWindowContext, ClipboardItem, Context, DismissEvent, Entity, EntityId,
@@ -46,23 +43,6 @@ use workspace::{
};
use zed_actions::ToggleFocus;
pub enum DebugPanelEvent {
Exited(SessionId),
Terminated(SessionId),
Stopped {
client_id: SessionId,
event: StoppedEvent,
go_to_stack_frame: bool,
},
Thread((SessionId, ThreadEvent)),
Continued((SessionId, ContinuedEvent)),
Output((SessionId, OutputEvent)),
Module((SessionId, ModuleEvent)),
LoadedSource((SessionId, LoadedSourceEvent)),
ClientShutdown(SessionId),
CapabilitiesChanged(SessionId),
}
pub struct DebugPanel {
size: Pixels,
active_session: Option<Entity<DebugSession>>,
@@ -1407,7 +1387,6 @@ async fn register_session_inner(
}
impl EventEmitter<PanelEvent> for DebugPanel {}
impl EventEmitter<DebugPanelEvent> for DebugPanel {}
impl Focusable for DebugPanel {
fn focus_handle(&self, _: &App) -> FocusHandle {

View File

@@ -113,23 +113,6 @@ impl DebugPanel {
}
};
session_entries.push(root_entry);
session_entries.extend(
sessions_with_children
.by_ref()
.take_while(|(session, _)| {
session
.read(cx)
.session(cx)
.read(cx)
.parent_id(cx)
.is_some()
})
.map(|(session, _)| SessionListEntry {
leaf: session.clone(),
ancestors: vec![],
}),
);
}
let weak = cx.weak_entity();

View File

@@ -20,7 +20,7 @@ use gpui::{
};
use itertools::Itertools as _;
use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch};
use project::{DebugScenarioContext, TaskContexts, TaskSourceKind, task_store::TaskStore};
use project::{DebugScenarioContext, Project, TaskContexts, TaskSourceKind, task_store::TaskStore};
use settings::Settings;
use task::{DebugScenario, RevealTarget, ZedDebugConfig};
use theme::ThemeSettings;
@@ -88,8 +88,10 @@ impl NewProcessModal {
})?;
workspace.update_in(cx, |workspace, window, cx| {
let workspace_handle = workspace.weak_handle();
let project = workspace.project().clone();
workspace.toggle_modal(window, cx, |window, cx| {
let attach_mode = AttachMode::new(None, workspace_handle.clone(), window, cx);
let attach_mode =
AttachMode::new(None, workspace_handle.clone(), project, window, cx);
let debug_picker = cx.new(|cx| {
let delegate =
@@ -940,6 +942,7 @@ impl AttachMode {
pub(super) fn new(
debugger: Option<DebugAdapterName>,
workspace: WeakEntity<Workspace>,
project: Entity<Project>,
window: &mut Window,
cx: &mut Context<NewProcessModal>,
) -> Entity<Self> {
@@ -950,7 +953,7 @@ impl AttachMode {
stop_on_entry: Some(false),
};
let attach_picker = cx.new(|cx| {
let modal = AttachModal::new(definition.clone(), workspace, false, window, cx);
let modal = AttachModal::new(definition.clone(), workspace, project, false, window, cx);
window.focus(&modal.focus_handle(cx));
modal

View File

@@ -2,9 +2,7 @@ pub mod running;
use crate::{StackTraceView, persistence::SerializedLayout, session::running::DebugTerminal};
use dap::client::SessionId;
use gpui::{
App, Axis, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity,
};
use gpui::{App, Axis, Entity, EventEmitter, FocusHandle, Focusable, Task, WeakEntity};
use project::debugger::session::Session;
use project::worktree_store::WorktreeStore;
use project::{Project, debugger::session::SessionQuirks};
@@ -24,13 +22,6 @@ pub struct DebugSession {
stack_trace_view: OnceCell<Entity<StackTraceView>>,
_worktree_store: WeakEntity<WorktreeStore>,
workspace: WeakEntity<Workspace>,
_subscriptions: [Subscription; 1],
}
#[derive(Debug)]
pub enum DebugPanelItemEvent {
Close,
Stopped { go_to_stack_frame: bool },
}
impl DebugSession {
@@ -59,9 +50,6 @@ impl DebugSession {
let quirks = session.read(cx).quirks();
cx.new(|cx| Self {
_subscriptions: [cx.subscribe(&running_state, |_, _, _, cx| {
cx.notify();
})],
remote_id: None,
running_state,
quirks,
@@ -133,7 +121,7 @@ impl DebugSession {
}
}
impl EventEmitter<DebugPanelItemEvent> for DebugSession {}
impl EventEmitter<()> for DebugSession {}
impl Focusable for DebugSession {
fn focus_handle(&self, cx: &App) -> FocusHandle {
@@ -142,7 +130,7 @@ impl Focusable for DebugSession {
}
impl Item for DebugSession {
type Event = DebugPanelItemEvent;
type Event = ();
fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
"Debugger".into()
}

View File

@@ -14,7 +14,6 @@ use crate::{
session::running::memory_view::MemoryView,
};
use super::DebugPanelItemEvent;
use anyhow::{Context as _, Result, anyhow};
use breakpoint_list::BreakpointList;
use collections::{HashMap, IndexMap};
@@ -1826,8 +1825,6 @@ impl RunningState {
}
}
impl EventEmitter<DebugPanelItemEvent> for RunningState {}
impl Focusable for RunningState {
fn focus_handle(&self, _: &App) -> FocusHandle {
self.focus_handle.clone()

View File

@@ -28,8 +28,8 @@ pub enum StackFrameListEvent {
}
/// Represents the filter applied to the stack frame list
#[derive(PartialEq, Eq, Copy, Clone)]
enum StackFrameFilter {
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
pub(crate) enum StackFrameFilter {
/// Show all frames
All,
/// Show only frames from the user's code
@@ -174,19 +174,29 @@ impl StackFrameList {
#[cfg(test)]
pub(crate) fn dap_stack_frames(&self, cx: &mut App) -> Vec<dap::StackFrame> {
self.stack_frames(cx)
.unwrap_or_default()
.into_iter()
.enumerate()
.filter(|(ix, _)| {
self.list_filter == StackFrameFilter::All
|| self
.filter_entries_indices
.binary_search_by_key(&ix, |ix| ix)
.is_ok()
})
.map(|(_, stack_frame)| stack_frame.dap)
.collect()
match self.list_filter {
StackFrameFilter::All => self
.stack_frames(cx)
.unwrap_or_default()
.into_iter()
.map(|stack_frame| stack_frame.dap)
.collect(),
StackFrameFilter::OnlyUserFrames => self
.filter_entries_indices
.iter()
.map(|ix| match &self.entries[*ix] {
StackFrameEntry::Label(label) => label,
StackFrameEntry::Collapsed(_) => panic!("Collapsed tabs should not be visible"),
StackFrameEntry::Normal(frame) => frame,
})
.cloned()
.collect(),
}
}
#[cfg(test)]
pub(crate) fn list_filter(&self) -> StackFrameFilter {
self.list_filter
}
pub fn opened_stack_frame_id(&self) -> Option<StackFrameId> {
@@ -246,6 +256,7 @@ impl StackFrameList {
self.entries.clear();
self.selected_ix = None;
self.list_state.reset(0);
self.filter_entries_indices.clear();
cx.emit(StackFrameListEvent::BuiltEntries);
cx.notify();
return;
@@ -263,7 +274,7 @@ impl StackFrameList {
.unwrap_or_default();
let mut filter_entries_indices = Vec::default();
for (ix, stack_frame) in stack_frames.iter().enumerate() {
for stack_frame in stack_frames.iter() {
let frame_in_visible_worktree = stack_frame.dap.source.as_ref().is_some_and(|source| {
source.path.as_ref().is_some_and(|path| {
worktree_prefixes
@@ -273,10 +284,6 @@ impl StackFrameList {
})
});
if frame_in_visible_worktree {
filter_entries_indices.push(ix);
}
match stack_frame.dap.presentation_hint {
Some(dap::StackFramePresentationHint::Deemphasize)
| Some(dap::StackFramePresentationHint::Subtle) => {
@@ -302,6 +309,9 @@ impl StackFrameList {
first_stack_frame_with_path.get_or_insert(entries.len());
}
entries.push(StackFrameEntry::Normal(stack_frame.dap.clone()));
if frame_in_visible_worktree {
filter_entries_indices.push(entries.len() - 1);
}
}
}
}
@@ -309,7 +319,6 @@ impl StackFrameList {
let collapsed_entries = std::mem::take(&mut collapsed_entries);
if !collapsed_entries.is_empty() {
entries.push(StackFrameEntry::Collapsed(collapsed_entries));
self.filter_entries_indices.push(entries.len() - 1);
}
self.entries = entries;
self.filter_entries_indices = filter_entries_indices;
@@ -612,7 +621,16 @@ impl StackFrameList {
let entries = std::mem::take(stack_frames)
.into_iter()
.map(StackFrameEntry::Normal);
// HERE
let entries_len = entries.len();
self.entries.splice(ix..ix + 1, entries);
let (Ok(filtered_indices_start) | Err(filtered_indices_start)) =
self.filter_entries_indices.binary_search(&ix);
for idx in &mut self.filter_entries_indices[filtered_indices_start..] {
*idx += entries_len - 1;
}
self.selected_ix = Some(ix);
self.list_state.reset(self.entries.len());
cx.emit(StackFrameListEvent::BuiltEntries);

View File

@@ -1,6 +1,6 @@
use crate::{
debugger_panel::DebugPanel,
session::running::stack_frame_list::StackFrameEntry,
session::running::stack_frame_list::{StackFrameEntry, StackFrameFilter},
tests::{active_debug_session_panel, init_test, init_test_workspace, start_debug_session},
};
use dap::{
@@ -867,6 +867,28 @@ async fn test_stack_frame_filter(executor: BackgroundExecutor, cx: &mut TestAppC
},
StackFrame {
id: 4,
name: "node:internal/modules/run_main2".into(),
source: Some(dap::Source {
name: Some("run_main.js".into()),
path: Some(path!("/usr/lib/node/internal/modules/run_main2.js").into()),
source_reference: None,
presentation_hint: None,
origin: None,
sources: None,
adapter_data: None,
checksums: None,
}),
line: 50,
column: 1,
end_line: None,
end_column: None,
can_restart: None,
instruction_pointer_reference: None,
module_id: None,
presentation_hint: Some(dap::StackFramePresentationHint::Deemphasize),
},
StackFrame {
id: 5,
name: "doSomething".into(),
source: Some(dap::Source {
name: Some("test.js".into()),
@@ -957,83 +979,119 @@ async fn test_stack_frame_filter(executor: BackgroundExecutor, cx: &mut TestAppC
cx.run_until_parked();
active_debug_session_panel(workspace, cx).update_in(cx, |debug_panel_item, window, cx| {
let stack_frame_list = debug_panel_item
.running_state()
.update(cx, |state, _| state.stack_frame_list().clone());
let stack_frame_list =
active_debug_session_panel(workspace, cx).update_in(cx, |debug_panel_item, window, cx| {
let stack_frame_list = debug_panel_item
.running_state()
.update(cx, |state, _| state.stack_frame_list().clone());
stack_frame_list.update(cx, |stack_frame_list, cx| {
stack_frame_list.build_entries(true, window, cx);
stack_frame_list.update(cx, |stack_frame_list, cx| {
stack_frame_list.build_entries(true, window, cx);
// Verify we have the expected collapsed structure
assert_eq!(
stack_frame_list.entries(),
&vec![
StackFrameEntry::Normal(stack_frames_for_assertions[0].clone()),
StackFrameEntry::Collapsed(vec![
stack_frames_for_assertions[1].clone(),
stack_frames_for_assertions[2].clone()
]),
StackFrameEntry::Normal(stack_frames_for_assertions[3].clone()),
]
);
// Verify we have the expected collapsed structure
assert_eq!(
stack_frame_list.entries(),
&vec![
StackFrameEntry::Normal(stack_frames_for_assertions[0].clone()),
StackFrameEntry::Collapsed(vec![
stack_frames_for_assertions[1].clone(),
stack_frames_for_assertions[2].clone(),
stack_frames_for_assertions[3].clone()
]),
StackFrameEntry::Normal(stack_frames_for_assertions[4].clone()),
]
);
});
// Test 1: Verify filtering works
let all_frames = stack_frame_list.flatten_entries(true, false);
assert_eq!(all_frames.len(), 4, "Should see all 4 frames initially");
// Toggle to user frames only
stack_frame_list
.toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx);
let user_frames = stack_frame_list.dap_stack_frames(cx);
assert_eq!(user_frames.len(), 2, "Should only see 2 user frames");
assert_eq!(user_frames[0].name, "main");
assert_eq!(user_frames[1].name, "doSomething");
// Test 2: Verify filtering toggles correctly
// Check we can toggle back and see all frames again
// Toggle back to all frames
stack_frame_list
.toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx);
let all_frames_again = stack_frame_list.flatten_entries(true, false);
assert_eq!(
all_frames_again.len(),
4,
"Should see all 4 frames after toggling back"
);
// Test 3: Verify collapsed entries stay expanded
stack_frame_list.expand_collapsed_entry(1, cx);
assert_eq!(
stack_frame_list.entries(),
&vec![
StackFrameEntry::Normal(stack_frames_for_assertions[0].clone()),
StackFrameEntry::Normal(stack_frames_for_assertions[1].clone()),
StackFrameEntry::Normal(stack_frames_for_assertions[2].clone()),
StackFrameEntry::Normal(stack_frames_for_assertions[3].clone()),
]
);
// Toggle filter twice
stack_frame_list
.toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx);
stack_frame_list
.toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx);
// Verify entries remain expanded
assert_eq!(
stack_frame_list.entries(),
&vec![
StackFrameEntry::Normal(stack_frames_for_assertions[0].clone()),
StackFrameEntry::Normal(stack_frames_for_assertions[1].clone()),
StackFrameEntry::Normal(stack_frames_for_assertions[2].clone()),
StackFrameEntry::Normal(stack_frames_for_assertions[3].clone()),
],
"Expanded entries should remain expanded after toggling filter"
);
});
stack_frame_list.update(cx, |stack_frame_list, cx| {
let all_frames = stack_frame_list.flatten_entries(true, false);
assert_eq!(all_frames.len(), 5, "Should see all 5 frames initially");
stack_frame_list
.toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx);
assert_eq!(
stack_frame_list.list_filter(),
StackFrameFilter::OnlyUserFrames
);
});
stack_frame_list.update(cx, |stack_frame_list, cx| {
let user_frames = stack_frame_list.dap_stack_frames(cx);
assert_eq!(user_frames.len(), 2, "Should only see 2 user frames");
assert_eq!(user_frames[0].name, "main");
assert_eq!(user_frames[1].name, "doSomething");
// Toggle back to all frames
stack_frame_list
.toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx);
assert_eq!(stack_frame_list.list_filter(), StackFrameFilter::All);
});
stack_frame_list.update(cx, |stack_frame_list, cx| {
let all_frames_again = stack_frame_list.flatten_entries(true, false);
assert_eq!(
all_frames_again.len(),
5,
"Should see all 5 frames after toggling back"
);
// Test 3: Verify collapsed entries stay expanded
stack_frame_list.expand_collapsed_entry(1, cx);
assert_eq!(
stack_frame_list.entries(),
&vec![
StackFrameEntry::Normal(stack_frames_for_assertions[0].clone()),
StackFrameEntry::Normal(stack_frames_for_assertions[1].clone()),
StackFrameEntry::Normal(stack_frames_for_assertions[2].clone()),
StackFrameEntry::Normal(stack_frames_for_assertions[3].clone()),
StackFrameEntry::Normal(stack_frames_for_assertions[4].clone()),
]
);
stack_frame_list
.toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx);
assert_eq!(
stack_frame_list.list_filter(),
StackFrameFilter::OnlyUserFrames
);
});
stack_frame_list.update(cx, |stack_frame_list, cx| {
stack_frame_list
.toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx);
assert_eq!(stack_frame_list.list_filter(), StackFrameFilter::All);
});
stack_frame_list.update(cx, |stack_frame_list, cx| {
stack_frame_list
.toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx);
assert_eq!(
stack_frame_list.list_filter(),
StackFrameFilter::OnlyUserFrames
);
assert_eq!(
stack_frame_list.dap_stack_frames(cx).as_slice(),
&[
stack_frames_for_assertions[0].clone(),
stack_frames_for_assertions[4].clone()
]
);
// Verify entries remain expanded
assert_eq!(
stack_frame_list.entries(),
&vec![
StackFrameEntry::Normal(stack_frames_for_assertions[0].clone()),
StackFrameEntry::Normal(stack_frames_for_assertions[1].clone()),
StackFrameEntry::Normal(stack_frames_for_assertions[2].clone()),
StackFrameEntry::Normal(stack_frames_for_assertions[3].clone()),
StackFrameEntry::Normal(stack_frames_for_assertions[4].clone()),
],
"Expanded entries should remain expanded after toggling filter"
);
});
}

View File

@@ -96,7 +96,7 @@ impl Model {
pub fn max_token_count(&self) -> u64 {
match self {
Self::Chat | Self::Reasoner => 64_000,
Self::Chat | Self::Reasoner => 128_000,
Self::Custom { max_tokens, .. } => *max_tokens,
}
}
@@ -104,7 +104,7 @@ impl Model {
pub fn max_output_tokens(&self) -> Option<u64> {
match self {
Self::Chat => Some(8_192),
Self::Reasoner => Some(8_192),
Self::Reasoner => Some(64_000),
Self::Custom {
max_output_tokens, ..
} => *max_output_tokens,

View File

@@ -94,43 +94,44 @@ impl Render for ProjectDiagnosticsEditor {
0
};
let child = if warning_count + self.summary.error_count == 0 {
let label = if self.summary.warning_count == 0 {
SharedString::new_static("No problems in workspace")
let child =
if warning_count + self.summary.error_count == 0 && self.editor.read(cx).is_empty(cx) {
let label = if self.summary.warning_count == 0 {
SharedString::new_static("No problems in workspace")
} else {
SharedString::new_static("No errors in workspace")
};
v_flex()
.key_context("EmptyPane")
.size_full()
.gap_1()
.justify_center()
.items_center()
.text_center()
.bg(cx.theme().colors().editor_background)
.child(Label::new(label).color(Color::Muted))
.when(self.summary.warning_count > 0, |this| {
let plural_suffix = if self.summary.warning_count > 1 {
"s"
} else {
""
};
let label = format!(
"Show {} warning{}",
self.summary.warning_count, plural_suffix
);
this.child(
Button::new("diagnostics-show-warning-label", label).on_click(
cx.listener(|this, _, window, cx| {
this.toggle_warnings(&Default::default(), window, cx);
cx.notify();
}),
),
)
})
} else {
SharedString::new_static("No errors in workspace")
div().size_full().child(self.editor.clone())
};
v_flex()
.key_context("EmptyPane")
.size_full()
.gap_1()
.justify_center()
.items_center()
.text_center()
.bg(cx.theme().colors().editor_background)
.child(Label::new(label).color(Color::Muted))
.when(self.summary.warning_count > 0, |this| {
let plural_suffix = if self.summary.warning_count > 1 {
"s"
} else {
""
};
let label = format!(
"Show {} warning{}",
self.summary.warning_count, plural_suffix
);
this.child(
Button::new("diagnostics-show-warning-label", label).on_click(cx.listener(
|this, _, window, cx| {
this.toggle_warnings(&Default::default(), window, cx);
cx.notify();
},
)),
)
})
} else {
div().size_full().child(self.editor.clone())
};
div()
.key_context("Diagnostics")
@@ -233,6 +234,7 @@ impl ProjectDiagnosticsEditor {
}
}
EditorEvent::Blurred => this.update_stale_excerpts(window, cx),
EditorEvent::Saved => this.update_stale_excerpts(window, cx),
_ => {}
}
},
@@ -277,7 +279,7 @@ impl ProjectDiagnosticsEditor {
}
fn update_stale_excerpts(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if self.update_excerpts_task.is_some() {
if self.update_excerpts_task.is_some() || self.multibuffer.read(cx).is_dirty(cx) {
return;
}

View File

@@ -682,7 +682,7 @@ async fn test_random_diagnostics_blocks(cx: &mut TestAppContext, mut rng: StdRng
Default::default();
for _ in 0..operations {
match rng.gen_range(0..100) {
match rng.random_range(0..100) {
// language server completes its diagnostic check
0..=20 if !updated_language_servers.is_empty() => {
let server_id = *updated_language_servers.iter().choose(&mut rng).unwrap();
@@ -691,7 +691,7 @@ async fn test_random_diagnostics_blocks(cx: &mut TestAppContext, mut rng: StdRng
lsp_store.disk_based_diagnostics_finished(server_id, cx)
});
if rng.gen_bool(0.5) {
if rng.random_bool(0.5) {
cx.run_until_parked();
}
}
@@ -701,7 +701,7 @@ async fn test_random_diagnostics_blocks(cx: &mut TestAppContext, mut rng: StdRng
let (path, server_id, diagnostics) =
match current_diagnostics.iter_mut().choose(&mut rng) {
// update existing set of diagnostics
Some(((path, server_id), diagnostics)) if rng.gen_bool(0.5) => {
Some(((path, server_id), diagnostics)) if rng.random_bool(0.5) => {
(path.clone(), *server_id, diagnostics)
}
@@ -709,13 +709,13 @@ async fn test_random_diagnostics_blocks(cx: &mut TestAppContext, mut rng: StdRng
_ => {
let path: PathBuf =
format!(path!("/test/{}.rs"), post_inc(&mut next_filename)).into();
let len = rng.gen_range(128..256);
let len = rng.random_range(128..256);
let content =
RandomCharIter::new(&mut rng).take(len).collect::<String>();
fs.insert_file(&path, content.into_bytes()).await;
let server_id = match language_server_ids.iter().choose(&mut rng) {
Some(server_id) if rng.gen_bool(0.5) => *server_id,
Some(server_id) if rng.random_bool(0.5) => *server_id,
_ => {
let id = LanguageServerId(language_server_ids.len());
language_server_ids.push(id);
@@ -846,7 +846,7 @@ async fn test_random_diagnostics_with_inlays(cx: &mut TestAppContext, mut rng: S
let mut next_inlay_id = 0;
for _ in 0..operations {
match rng.gen_range(0..100) {
match rng.random_range(0..100) {
// language server completes its diagnostic check
0..=20 if !updated_language_servers.is_empty() => {
let server_id = *updated_language_servers.iter().choose(&mut rng).unwrap();
@@ -855,7 +855,7 @@ async fn test_random_diagnostics_with_inlays(cx: &mut TestAppContext, mut rng: S
lsp_store.disk_based_diagnostics_finished(server_id, cx)
});
if rng.gen_bool(0.5) {
if rng.random_bool(0.5) {
cx.run_until_parked();
}
}
@@ -864,7 +864,7 @@ async fn test_random_diagnostics_with_inlays(cx: &mut TestAppContext, mut rng: S
diagnostics.editor.update(cx, |editor, cx| {
let snapshot = editor.snapshot(window, cx);
if !snapshot.buffer_snapshot.is_empty() {
let position = rng.gen_range(0..snapshot.buffer_snapshot.len());
let position = rng.random_range(0..snapshot.buffer_snapshot.len());
let position = snapshot.buffer_snapshot.clip_offset(position, Bias::Left);
log::info!(
"adding inlay at {position}/{}: {:?}",
@@ -890,7 +890,7 @@ async fn test_random_diagnostics_with_inlays(cx: &mut TestAppContext, mut rng: S
let (path, server_id, diagnostics) =
match current_diagnostics.iter_mut().choose(&mut rng) {
// update existing set of diagnostics
Some(((path, server_id), diagnostics)) if rng.gen_bool(0.5) => {
Some(((path, server_id), diagnostics)) if rng.random_bool(0.5) => {
(path.clone(), *server_id, diagnostics)
}
@@ -898,13 +898,13 @@ async fn test_random_diagnostics_with_inlays(cx: &mut TestAppContext, mut rng: S
_ => {
let path: PathBuf =
format!(path!("/test/{}.rs"), post_inc(&mut next_filename)).into();
let len = rng.gen_range(128..256);
let len = rng.random_range(128..256);
let content =
RandomCharIter::new(&mut rng).take(len).collect::<String>();
fs.insert_file(&path, content.into_bytes()).await;
let server_id = match language_server_ids.iter().choose(&mut rng) {
Some(server_id) if rng.gen_bool(0.5) => *server_id,
Some(server_id) if rng.random_bool(0.5) => *server_id,
_ => {
let id = LanguageServerId(language_server_ids.len());
language_server_ids.push(id);
@@ -1589,10 +1589,10 @@ fn randomly_update_diagnostics_for_path(
next_id: &mut usize,
rng: &mut impl Rng,
) {
let mutation_count = rng.gen_range(1..=3);
let mutation_count = rng.random_range(1..=3);
for _ in 0..mutation_count {
if rng.gen_bool(0.3) && !diagnostics.is_empty() {
let idx = rng.gen_range(0..diagnostics.len());
if rng.random_bool(0.3) && !diagnostics.is_empty() {
let idx = rng.random_range(0..diagnostics.len());
log::info!(" removing diagnostic at index {idx}");
diagnostics.remove(idx);
} else {
@@ -1601,7 +1601,7 @@ fn randomly_update_diagnostics_for_path(
let new_diagnostic = random_lsp_diagnostic(rng, fs, path, unique_id);
let ix = rng.gen_range(0..=diagnostics.len());
let ix = rng.random_range(0..=diagnostics.len());
log::info!(
" inserting {} at index {ix}. {},{}..{},{}",
new_diagnostic.message,
@@ -1638,8 +1638,8 @@ fn random_lsp_diagnostic(
let file_content = fs.read_file_sync(path).unwrap();
let file_text = Rope::from(String::from_utf8_lossy(&file_content).as_ref());
let start = rng.gen_range(0..file_text.len().saturating_add(ERROR_MARGIN));
let end = rng.gen_range(start..file_text.len().saturating_add(ERROR_MARGIN));
let start = rng.random_range(0..file_text.len().saturating_add(ERROR_MARGIN));
let end = rng.random_range(start..file_text.len().saturating_add(ERROR_MARGIN));
let start_point = file_text.offset_to_point_utf16(start);
let end_point = file_text.offset_to_point_utf16(end);
@@ -1649,7 +1649,7 @@ fn random_lsp_diagnostic(
lsp::Position::new(end_point.row, end_point.column),
);
let severity = if rng.gen_bool(0.5) {
let severity = if rng.random_bool(0.5) {
Some(lsp::DiagnosticSeverity::ERROR)
} else {
Some(lsp::DiagnosticSeverity::WARNING)
@@ -1657,13 +1657,14 @@ fn random_lsp_diagnostic(
let message = format!("diagnostic {unique_id}");
let related_information = if rng.gen_bool(0.3) {
let info_count = rng.gen_range(1..=3);
let related_information = if rng.random_bool(0.3) {
let info_count = rng.random_range(1..=3);
let mut related_info = Vec::with_capacity(info_count);
for i in 0..info_count {
let info_start = rng.gen_range(0..file_text.len().saturating_add(ERROR_MARGIN));
let info_end = rng.gen_range(info_start..file_text.len().saturating_add(ERROR_MARGIN));
let info_start = rng.random_range(0..file_text.len().saturating_add(ERROR_MARGIN));
let info_end =
rng.random_range(info_start..file_text.len().saturating_add(ERROR_MARGIN));
let info_start_point = file_text.offset_to_point_utf16(info_start);
let info_end_point = file_text.offset_to_point_utf16(info_end);

View File

@@ -32,49 +32,38 @@ impl Render for DiagnosticIndicator {
}
let diagnostic_indicator = match (self.summary.error_count, self.summary.warning_count) {
(0, 0) => h_flex().map(|this| {
this.child(
Icon::new(IconName::Check)
.size(IconSize::Small)
.color(Color::Default),
)
}),
(0, warning_count) => h_flex()
.gap_1()
.child(
Icon::new(IconName::Warning)
.size(IconSize::Small)
.color(Color::Warning),
)
.child(Label::new(warning_count.to_string()).size(LabelSize::Small)),
(error_count, 0) => h_flex()
.gap_1()
.child(
Icon::new(IconName::XCircle)
.size(IconSize::Small)
.color(Color::Error),
)
.child(Label::new(error_count.to_string()).size(LabelSize::Small)),
(0, 0) => h_flex().child(
Icon::new(IconName::Check)
.size(IconSize::Small)
.color(Color::Default),
),
(error_count, warning_count) => h_flex()
.gap_1()
.child(
Icon::new(IconName::XCircle)
.size(IconSize::Small)
.color(Color::Error),
)
.child(Label::new(error_count.to_string()).size(LabelSize::Small))
.child(
Icon::new(IconName::Warning)
.size(IconSize::Small)
.color(Color::Warning),
)
.child(Label::new(warning_count.to_string()).size(LabelSize::Small)),
.when(error_count > 0, |this| {
this.child(
Icon::new(IconName::XCircle)
.size(IconSize::Small)
.color(Color::Error),
)
.child(Label::new(error_count.to_string()).size(LabelSize::Small))
})
.when(warning_count > 0, |this| {
this.child(
Icon::new(IconName::Warning)
.size(IconSize::Small)
.color(Color::Warning),
)
.child(Label::new(warning_count.to_string()).size(LabelSize::Small))
}),
};
let status = if let Some(diagnostic) = &self.current_diagnostic {
let message = diagnostic.message.split('\n').next().unwrap().to_string();
let message = diagnostic
.message
.split_once('\n')
.map_or(&*diagnostic.message, |(first, _)| first);
Some(
Button::new("diagnostic_message", message)
Button::new("diagnostic_message", SharedString::new(message))
.label_size(LabelSize::Small)
.tooltip(|window, cx| {
Tooltip::for_action(

View File

@@ -251,7 +251,7 @@ enum MarkdownCacheKey {
pub enum CompletionsMenuSource {
Normal,
SnippetChoices,
Words,
Words { ignore_threshold: bool },
}
// TODO: There should really be a wrapper around fuzzy match tasks that does this.

View File

@@ -1552,15 +1552,15 @@ pub mod tests {
.map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
.unwrap_or(10);
let mut tab_size = rng.gen_range(1..=4);
let buffer_start_excerpt_header_height = rng.gen_range(1..=5);
let excerpt_header_height = rng.gen_range(1..=5);
let mut tab_size = rng.random_range(1..=4);
let buffer_start_excerpt_header_height = rng.random_range(1..=5);
let excerpt_header_height = rng.random_range(1..=5);
let font_size = px(14.0);
let max_wrap_width = 300.0;
let mut wrap_width = if rng.gen_bool(0.1) {
let mut wrap_width = if rng.random_bool(0.1) {
None
} else {
Some(px(rng.gen_range(0.0..=max_wrap_width)))
Some(px(rng.random_range(0.0..=max_wrap_width)))
};
log::info!("tab size: {}", tab_size);
@@ -1571,8 +1571,8 @@ pub mod tests {
});
let buffer = cx.update(|cx| {
if rng.r#gen() {
let len = rng.gen_range(0..10);
if rng.random() {
let len = rng.random_range(0..10);
let text = util::RandomCharIter::new(&mut rng)
.take(len)
.collect::<String>();
@@ -1609,12 +1609,12 @@ pub mod tests {
log::info!("display text: {:?}", snapshot.text());
for _i in 0..operations {
match rng.gen_range(0..100) {
match rng.random_range(0..100) {
0..=19 => {
wrap_width = if rng.gen_bool(0.2) {
wrap_width = if rng.random_bool(0.2) {
None
} else {
Some(px(rng.gen_range(0.0..=max_wrap_width)))
Some(px(rng.random_range(0.0..=max_wrap_width)))
};
log::info!("setting wrap width to {:?}", wrap_width);
map.update(cx, |map, cx| map.set_wrap_width(wrap_width, cx));
@@ -1634,28 +1634,27 @@ pub mod tests {
}
30..=44 => {
map.update(cx, |map, cx| {
if rng.r#gen() || blocks.is_empty() {
if rng.random() || blocks.is_empty() {
let buffer = map.snapshot(cx).buffer_snapshot;
let block_properties = (0..rng.gen_range(1..=1))
let block_properties = (0..rng.random_range(1..=1))
.map(|_| {
let position =
buffer.anchor_after(buffer.clip_offset(
rng.gen_range(0..=buffer.len()),
Bias::Left,
));
let position = buffer.anchor_after(buffer.clip_offset(
rng.random_range(0..=buffer.len()),
Bias::Left,
));
let placement = if rng.r#gen() {
let placement = if rng.random() {
BlockPlacement::Above(position)
} else {
BlockPlacement::Below(position)
};
let height = rng.gen_range(1..5);
let height = rng.random_range(1..5);
log::info!(
"inserting block {:?} with height {}",
placement.as_ref().map(|p| p.to_point(&buffer)),
height
);
let priority = rng.gen_range(1..100);
let priority = rng.random_range(1..100);
BlockProperties {
placement,
style: BlockStyle::Fixed,
@@ -1668,9 +1667,9 @@ pub mod tests {
blocks.extend(map.insert_blocks(block_properties, cx));
} else {
blocks.shuffle(&mut rng);
let remove_count = rng.gen_range(1..=4.min(blocks.len()));
let remove_count = rng.random_range(1..=4.min(blocks.len()));
let block_ids_to_remove = (0..remove_count)
.map(|_| blocks.remove(rng.gen_range(0..blocks.len())))
.map(|_| blocks.remove(rng.random_range(0..blocks.len())))
.collect();
log::info!("removing block ids {:?}", block_ids_to_remove);
map.remove_blocks(block_ids_to_remove, cx);
@@ -1679,16 +1678,16 @@ pub mod tests {
}
45..=79 => {
let mut ranges = Vec::new();
for _ in 0..rng.gen_range(1..=3) {
for _ in 0..rng.random_range(1..=3) {
buffer.read_with(cx, |buffer, cx| {
let buffer = buffer.read(cx);
let end = buffer.clip_offset(rng.gen_range(0..=buffer.len()), Right);
let start = buffer.clip_offset(rng.gen_range(0..=end), Left);
let end = buffer.clip_offset(rng.random_range(0..=buffer.len()), Right);
let start = buffer.clip_offset(rng.random_range(0..=end), Left);
ranges.push(start..end);
});
}
if rng.r#gen() && fold_count > 0 {
if rng.random() && fold_count > 0 {
log::info!("unfolding ranges: {:?}", ranges);
map.update(cx, |map, cx| {
map.unfold_intersecting(ranges, true, cx);
@@ -1727,8 +1726,8 @@ pub mod tests {
// Line boundaries
let buffer = &snapshot.buffer_snapshot;
for _ in 0..5 {
let row = rng.gen_range(0..=buffer.max_point().row);
let column = rng.gen_range(0..=buffer.line_len(MultiBufferRow(row)));
let row = rng.random_range(0..=buffer.max_point().row);
let column = rng.random_range(0..=buffer.line_len(MultiBufferRow(row)));
let point = buffer.clip_point(Point::new(row, column), Left);
let (prev_buffer_bound, prev_display_bound) = snapshot.prev_line_boundary(point);
@@ -1776,8 +1775,8 @@ pub mod tests {
let min_point = snapshot.clip_point(DisplayPoint::new(DisplayRow(0), 0), Left);
let max_point = snapshot.clip_point(snapshot.max_point(), Right);
for _ in 0..5 {
let row = rng.gen_range(0..=snapshot.max_point().row().0);
let column = rng.gen_range(0..=snapshot.line_len(DisplayRow(row)));
let row = rng.random_range(0..=snapshot.max_point().row().0);
let column = rng.random_range(0..=snapshot.line_len(DisplayRow(row)));
let point = snapshot.clip_point(DisplayPoint::new(DisplayRow(row), column), Left);
log::info!("Moving from point {:?}", point);

View File

@@ -128,10 +128,10 @@ impl<T> BlockPlacement<T> {
}
}
fn sort_order(&self) -> u8 {
fn tie_break(&self) -> u8 {
match self {
BlockPlacement::Above(_) => 0,
BlockPlacement::Replace(_) => 1,
BlockPlacement::Replace(_) => 0,
BlockPlacement::Above(_) => 1,
BlockPlacement::Near(_) => 2,
BlockPlacement::Below(_) => 3,
}
@@ -143,7 +143,7 @@ impl BlockPlacement<Anchor> {
self.start()
.cmp(other.start(), buffer)
.then_with(|| other.end().cmp(self.end(), buffer))
.then_with(|| self.sort_order().cmp(&other.sort_order()))
.then_with(|| self.tie_break().cmp(&other.tie_break()))
}
fn to_wrap_row(&self, wrap_snapshot: &WrapSnapshot) -> Option<BlockPlacement<WrapRow>> {
@@ -847,6 +847,7 @@ impl BlockMap {
.start()
.cmp(placement_b.start())
.then_with(|| placement_b.end().cmp(placement_a.end()))
.then_with(|| placement_a.tie_break().cmp(&placement_b.tie_break()))
.then_with(|| {
if block_a.is_header() {
Ordering::Less
@@ -856,7 +857,6 @@ impl BlockMap {
Ordering::Equal
}
})
.then_with(|| placement_a.sort_order().cmp(&placement_b.sort_order()))
.then_with(|| match (block_a, block_b) {
(
Block::ExcerptBoundary {
@@ -2922,21 +2922,21 @@ mod tests {
.map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
.unwrap_or(10);
let wrap_width = if rng.gen_bool(0.2) {
let wrap_width = if rng.random_bool(0.2) {
None
} else {
Some(px(rng.gen_range(0.0..=100.0)))
Some(px(rng.random_range(0.0..=100.0)))
};
let tab_size = 1.try_into().unwrap();
let font_size = px(14.0);
let buffer_start_header_height = rng.gen_range(1..=5);
let excerpt_header_height = rng.gen_range(1..=5);
let buffer_start_header_height = rng.random_range(1..=5);
let excerpt_header_height = rng.random_range(1..=5);
log::info!("Wrap width: {:?}", wrap_width);
log::info!("Excerpt Header Height: {:?}", excerpt_header_height);
let is_singleton = rng.r#gen();
let is_singleton = rng.random();
let buffer = if is_singleton {
let len = rng.gen_range(0..10);
let len = rng.random_range(0..10);
let text = RandomCharIter::new(&mut rng).take(len).collect::<String>();
log::info!("initial singleton buffer text: {:?}", text);
cx.update(|cx| MultiBuffer::build_simple(&text, cx))
@@ -2966,30 +2966,30 @@ mod tests {
for _ in 0..operations {
let mut buffer_edits = Vec::new();
match rng.gen_range(0..=100) {
match rng.random_range(0..=100) {
0..=19 => {
let wrap_width = if rng.gen_bool(0.2) {
let wrap_width = if rng.random_bool(0.2) {
None
} else {
Some(px(rng.gen_range(0.0..=100.0)))
Some(px(rng.random_range(0.0..=100.0)))
};
log::info!("Setting wrap width to {:?}", wrap_width);
wrap_map.update(cx, |map, cx| map.set_wrap_width(wrap_width, cx));
}
20..=39 => {
let block_count = rng.gen_range(1..=5);
let block_count = rng.random_range(1..=5);
let block_properties = (0..block_count)
.map(|_| {
let buffer = cx.update(|cx| buffer.read(cx).read(cx).clone());
let offset =
buffer.clip_offset(rng.gen_range(0..=buffer.len()), Bias::Left);
buffer.clip_offset(rng.random_range(0..=buffer.len()), Bias::Left);
let mut min_height = 0;
let placement = match rng.gen_range(0..3) {
let placement = match rng.random_range(0..3) {
0 => {
min_height = 1;
let start = buffer.anchor_after(offset);
let end = buffer.anchor_after(buffer.clip_offset(
rng.gen_range(offset..=buffer.len()),
rng.random_range(offset..=buffer.len()),
Bias::Left,
));
BlockPlacement::Replace(start..=end)
@@ -2998,7 +2998,7 @@ mod tests {
_ => BlockPlacement::Below(buffer.anchor_after(offset)),
};
let height = rng.gen_range(min_height..5);
let height = rng.random_range(min_height..5);
BlockProperties {
style: BlockStyle::Fixed,
placement,
@@ -3040,7 +3040,7 @@ mod tests {
}
}
40..=59 if !block_map.custom_blocks.is_empty() => {
let block_count = rng.gen_range(1..=4.min(block_map.custom_blocks.len()));
let block_count = rng.random_range(1..=4.min(block_map.custom_blocks.len()));
let block_ids_to_remove = block_map
.custom_blocks
.choose_multiple(&mut rng, block_count)
@@ -3095,8 +3095,8 @@ mod tests {
let mut folded_count = folded_buffers.len();
let mut unfolded_count = unfolded_buffers.len();
let fold = !unfolded_buffers.is_empty() && rng.gen_bool(0.5);
let unfold = !folded_buffers.is_empty() && rng.gen_bool(0.5);
let fold = !unfolded_buffers.is_empty() && rng.random_bool(0.5);
let unfold = !folded_buffers.is_empty() && rng.random_bool(0.5);
if !fold && !unfold {
log::info!(
"Noop fold/unfold operation. Unfolded buffers: {unfolded_count}, folded buffers: {folded_count}"
@@ -3107,7 +3107,7 @@ mod tests {
buffer.update(cx, |buffer, cx| {
if fold {
let buffer_to_fold =
unfolded_buffers[rng.gen_range(0..unfolded_buffers.len())];
unfolded_buffers[rng.random_range(0..unfolded_buffers.len())];
log::info!("Folding {buffer_to_fold:?}");
let related_excerpts = buffer_snapshot
.excerpts()
@@ -3133,7 +3133,7 @@ mod tests {
}
if unfold {
let buffer_to_unfold =
folded_buffers[rng.gen_range(0..folded_buffers.len())];
folded_buffers[rng.random_range(0..folded_buffers.len())];
log::info!("Unfolding {buffer_to_unfold:?}");
unfolded_count += 1;
folded_count -= 1;
@@ -3146,7 +3146,7 @@ mod tests {
}
_ => {
buffer.update(cx, |buffer, cx| {
let mutation_count = rng.gen_range(1..=5);
let mutation_count = rng.random_range(1..=5);
let subscription = buffer.subscribe();
buffer.randomly_mutate(&mut rng, mutation_count, cx);
buffer_snapshot = buffer.snapshot(cx);
@@ -3331,7 +3331,7 @@ mod tests {
);
for start_row in 0..expected_row_count {
let end_row = rng.gen_range(start_row + 1..=expected_row_count);
let end_row = rng.random_range(start_row + 1..=expected_row_count);
let mut expected_text = expected_lines[start_row..end_row].join("\n");
if end_row < expected_row_count {
expected_text.push('\n');
@@ -3426,8 +3426,8 @@ mod tests {
);
for _ in 0..10 {
let end_row = rng.gen_range(1..=expected_lines.len());
let start_row = rng.gen_range(0..end_row);
let end_row = rng.random_range(1..=expected_lines.len());
let start_row = rng.random_range(0..end_row);
let mut expected_longest_rows_in_range = vec![];
let mut longest_line_len_in_range = 0;

View File

@@ -1771,9 +1771,9 @@ mod tests {
.map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
.unwrap_or(10);
let len = rng.gen_range(0..10);
let len = rng.random_range(0..10);
let text = RandomCharIter::new(&mut rng).take(len).collect::<String>();
let buffer = if rng.r#gen() {
let buffer = if rng.random() {
MultiBuffer::build_simple(&text, cx)
} else {
MultiBuffer::build_random(&mut rng, cx)
@@ -1790,7 +1790,7 @@ mod tests {
log::info!("text: {:?}", buffer_snapshot.text());
let mut buffer_edits = Vec::new();
let mut inlay_edits = Vec::new();
match rng.gen_range(0..=100) {
match rng.random_range(0..=100) {
0..=39 => {
snapshot_edits.extend(map.randomly_mutate(&mut rng));
}
@@ -1800,7 +1800,7 @@ mod tests {
}
_ => buffer.update(cx, |buffer, cx| {
let subscription = buffer.subscribe();
let edit_count = rng.gen_range(1..=5);
let edit_count = rng.random_range(1..=5);
buffer.randomly_mutate(&mut rng, edit_count, cx);
buffer_snapshot = buffer.snapshot(cx);
let edits = subscription.consume().into_inner();
@@ -1917,10 +1917,14 @@ mod tests {
}
for _ in 0..5 {
let mut start = snapshot
.clip_offset(FoldOffset(rng.gen_range(0..=snapshot.len().0)), Bias::Left);
let mut end = snapshot
.clip_offset(FoldOffset(rng.gen_range(0..=snapshot.len().0)), Bias::Right);
let mut start = snapshot.clip_offset(
FoldOffset(rng.random_range(0..=snapshot.len().0)),
Bias::Left,
);
let mut end = snapshot.clip_offset(
FoldOffset(rng.random_range(0..=snapshot.len().0)),
Bias::Right,
);
if start > end {
mem::swap(&mut start, &mut end);
}
@@ -1975,8 +1979,8 @@ mod tests {
for _ in 0..5 {
let end =
buffer_snapshot.clip_offset(rng.gen_range(0..=buffer_snapshot.len()), Right);
let start = buffer_snapshot.clip_offset(rng.gen_range(0..=end), Left);
buffer_snapshot.clip_offset(rng.random_range(0..=buffer_snapshot.len()), Right);
let start = buffer_snapshot.clip_offset(rng.random_range(0..=end), Left);
let expected_folds = map
.snapshot
.folds
@@ -2001,10 +2005,10 @@ mod tests {
let text = snapshot.text();
for _ in 0..5 {
let start_row = rng.gen_range(0..=snapshot.max_point().row());
let start_column = rng.gen_range(0..=snapshot.line_len(start_row));
let end_row = rng.gen_range(0..=snapshot.max_point().row());
let end_column = rng.gen_range(0..=snapshot.line_len(end_row));
let start_row = rng.random_range(0..=snapshot.max_point().row());
let start_column = rng.random_range(0..=snapshot.line_len(start_row));
let end_row = rng.random_range(0..=snapshot.max_point().row());
let end_column = rng.random_range(0..=snapshot.line_len(end_row));
let mut start =
snapshot.clip_point(FoldPoint::new(start_row, start_column), Bias::Left);
let mut end = snapshot.clip_point(FoldPoint::new(end_row, end_column), Bias::Right);
@@ -2109,17 +2113,17 @@ mod tests {
rng: &mut impl Rng,
) -> Vec<(FoldSnapshot, Vec<FoldEdit>)> {
let mut snapshot_edits = Vec::new();
match rng.gen_range(0..=100) {
match rng.random_range(0..=100) {
0..=39 if !self.snapshot.folds.is_empty() => {
let inlay_snapshot = self.snapshot.inlay_snapshot.clone();
let buffer = &inlay_snapshot.buffer;
let mut to_unfold = Vec::new();
for _ in 0..rng.gen_range(1..=3) {
let end = buffer.clip_offset(rng.gen_range(0..=buffer.len()), Right);
let start = buffer.clip_offset(rng.gen_range(0..=end), Left);
for _ in 0..rng.random_range(1..=3) {
let end = buffer.clip_offset(rng.random_range(0..=buffer.len()), Right);
let start = buffer.clip_offset(rng.random_range(0..=end), Left);
to_unfold.push(start..end);
}
let inclusive = rng.r#gen();
let inclusive = rng.random();
log::info!("unfolding {:?} (inclusive: {})", to_unfold, inclusive);
let (mut writer, snapshot, edits) = self.write(inlay_snapshot, vec![]);
snapshot_edits.push((snapshot, edits));
@@ -2130,9 +2134,9 @@ mod tests {
let inlay_snapshot = self.snapshot.inlay_snapshot.clone();
let buffer = &inlay_snapshot.buffer;
let mut to_fold = Vec::new();
for _ in 0..rng.gen_range(1..=2) {
let end = buffer.clip_offset(rng.gen_range(0..=buffer.len()), Right);
let start = buffer.clip_offset(rng.gen_range(0..=end), Left);
for _ in 0..rng.random_range(1..=2) {
let end = buffer.clip_offset(rng.random_range(0..=buffer.len()), Right);
let start = buffer.clip_offset(rng.random_range(0..=end), Left);
to_fold.push((start..end, FoldPlaceholder::test()));
}
log::info!("folding {:?}", to_fold);

View File

@@ -719,14 +719,18 @@ impl InlayMap {
let mut to_remove = Vec::new();
let mut to_insert = Vec::new();
let snapshot = &mut self.snapshot;
for i in 0..rng.gen_range(1..=5) {
if self.inlays.is_empty() || rng.r#gen() {
for i in 0..rng.random_range(1..=5) {
if self.inlays.is_empty() || rng.random() {
let position = snapshot.buffer.random_byte_range(0, rng).start;
let bias = if rng.r#gen() { Bias::Left } else { Bias::Right };
let len = if rng.gen_bool(0.01) {
let bias = if rng.random() {
Bias::Left
} else {
Bias::Right
};
let len = if rng.random_bool(0.01) {
0
} else {
rng.gen_range(1..=5)
rng.random_range(1..=5)
};
let text = util::RandomCharIter::new(&mut *rng)
.filter(|ch| *ch != '\r')
@@ -1665,8 +1669,8 @@ mod tests {
.map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
.unwrap_or(10);
let len = rng.gen_range(0..30);
let buffer = if rng.r#gen() {
let len = rng.random_range(0..30);
let buffer = if rng.random() {
let text = util::RandomCharIter::new(&mut rng)
.take(len)
.collect::<String>();
@@ -1683,7 +1687,7 @@ mod tests {
let mut prev_inlay_text = inlay_snapshot.text();
let mut buffer_edits = Vec::new();
match rng.gen_range(0..=100) {
match rng.random_range(0..=100) {
0..=50 => {
let (snapshot, edits) = inlay_map.randomly_mutate(&mut next_inlay_id, &mut rng);
log::info!("mutated text: {:?}", snapshot.text());
@@ -1691,7 +1695,7 @@ mod tests {
}
_ => buffer.update(cx, |buffer, cx| {
let subscription = buffer.subscribe();
let edit_count = rng.gen_range(1..=5);
let edit_count = rng.random_range(1..=5);
buffer.randomly_mutate(&mut rng, edit_count, cx);
buffer_snapshot = buffer.snapshot(cx);
let edits = subscription.consume().into_inner();
@@ -1740,7 +1744,7 @@ mod tests {
}
let mut text_highlights = TextHighlights::default();
let text_highlight_count = rng.gen_range(0_usize..10);
let text_highlight_count = rng.random_range(0_usize..10);
let mut text_highlight_ranges = (0..text_highlight_count)
.map(|_| buffer_snapshot.random_byte_range(0, &mut rng))
.collect::<Vec<_>>();
@@ -1762,10 +1766,10 @@ mod tests {
let mut inlay_highlights = InlayHighlights::default();
if !inlays.is_empty() {
let inlay_highlight_count = rng.gen_range(0..inlays.len());
let inlay_highlight_count = rng.random_range(0..inlays.len());
let mut inlay_indices = BTreeSet::default();
while inlay_indices.len() < inlay_highlight_count {
inlay_indices.insert(rng.gen_range(0..inlays.len()));
inlay_indices.insert(rng.random_range(0..inlays.len()));
}
let new_highlights = TreeMap::from_ordered_entries(
inlay_indices
@@ -1782,8 +1786,8 @@ mod tests {
}),
n => {
let inlay_text = inlay.text.to_string();
let mut highlight_end = rng.gen_range(1..n);
let mut highlight_start = rng.gen_range(0..highlight_end);
let mut highlight_end = rng.random_range(1..n);
let mut highlight_start = rng.random_range(0..highlight_end);
while !inlay_text.is_char_boundary(highlight_end) {
highlight_end += 1;
}
@@ -1805,9 +1809,9 @@ mod tests {
}
for _ in 0..5 {
let mut end = rng.gen_range(0..=inlay_snapshot.len().0);
let mut end = rng.random_range(0..=inlay_snapshot.len().0);
end = expected_text.clip_offset(end, Bias::Right);
let mut start = rng.gen_range(0..=end);
let mut start = rng.random_range(0..=end);
start = expected_text.clip_offset(start, Bias::Right);
let range = InlayOffset(start)..InlayOffset(end);

View File

@@ -736,9 +736,9 @@ mod tests {
#[gpui::test(iterations = 100)]
fn test_random_tabs(cx: &mut gpui::App, mut rng: StdRng) {
let tab_size = NonZeroU32::new(rng.gen_range(1..=4)).unwrap();
let len = rng.gen_range(0..30);
let buffer = if rng.r#gen() {
let tab_size = NonZeroU32::new(rng.random_range(1..=4)).unwrap();
let len = rng.random_range(0..30);
let buffer = if rng.random() {
let text = util::RandomCharIter::new(&mut rng)
.take(len)
.collect::<String>();
@@ -769,11 +769,11 @@ mod tests {
);
for _ in 0..5 {
let end_row = rng.gen_range(0..=text.max_point().row);
let end_column = rng.gen_range(0..=text.line_len(end_row));
let end_row = rng.random_range(0..=text.max_point().row);
let end_column = rng.random_range(0..=text.line_len(end_row));
let mut end = TabPoint(text.clip_point(Point::new(end_row, end_column), Bias::Right));
let start_row = rng.gen_range(0..=text.max_point().row);
let start_column = rng.gen_range(0..=text.line_len(start_row));
let start_row = rng.random_range(0..=text.max_point().row);
let start_column = rng.random_range(0..=text.line_len(start_row));
let mut start =
TabPoint(text.clip_point(Point::new(start_row, start_column), Bias::Left));
if start > end {

View File

@@ -1215,12 +1215,12 @@ mod tests {
.unwrap_or(10);
let text_system = cx.read(|cx| cx.text_system().clone());
let mut wrap_width = if rng.gen_bool(0.1) {
let mut wrap_width = if rng.random_bool(0.1) {
None
} else {
Some(px(rng.gen_range(0.0..=1000.0)))
Some(px(rng.random_range(0.0..=1000.0)))
};
let tab_size = NonZeroU32::new(rng.gen_range(1..=4)).unwrap();
let tab_size = NonZeroU32::new(rng.random_range(1..=4)).unwrap();
let font = test_font();
let _font_id = text_system.resolve_font(&font);
@@ -1230,10 +1230,10 @@ mod tests {
log::info!("Wrap width: {:?}", wrap_width);
let buffer = cx.update(|cx| {
if rng.r#gen() {
if rng.random() {
MultiBuffer::build_random(&mut rng, cx)
} else {
let len = rng.gen_range(0..10);
let len = rng.random_range(0..10);
let text = util::RandomCharIter::new(&mut rng)
.take(len)
.collect::<String>();
@@ -1281,12 +1281,12 @@ mod tests {
log::info!("{} ==============================================", _i);
let mut buffer_edits = Vec::new();
match rng.gen_range(0..=100) {
match rng.random_range(0..=100) {
0..=19 => {
wrap_width = if rng.gen_bool(0.2) {
wrap_width = if rng.random_bool(0.2) {
None
} else {
Some(px(rng.gen_range(0.0..=1000.0)))
Some(px(rng.random_range(0.0..=1000.0)))
};
log::info!("Setting wrap width to {:?}", wrap_width);
wrap_map.update(cx, |map, cx| map.set_wrap_width(wrap_width, cx));
@@ -1317,7 +1317,7 @@ mod tests {
_ => {
buffer.update(cx, |buffer, cx| {
let subscription = buffer.subscribe();
let edit_count = rng.gen_range(1..=5);
let edit_count = rng.random_range(1..=5);
buffer.randomly_mutate(&mut rng, edit_count, cx);
buffer_snapshot = buffer.snapshot(cx);
buffer_edits.extend(subscription.consume());
@@ -1341,7 +1341,7 @@ mod tests {
snapshot.verify_chunks(&mut rng);
edits.push((snapshot, wrap_edits));
if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) && rng.gen_bool(0.4) {
if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) && rng.random_bool(0.4) {
log::info!("Waiting for wrapping to finish");
while wrap_map.read_with(cx, |map, _| map.is_rewrapping()) {
notifications.next().await.unwrap();
@@ -1479,8 +1479,8 @@ mod tests {
impl WrapSnapshot {
fn verify_chunks(&mut self, rng: &mut impl Rng) {
for _ in 0..5 {
let mut end_row = rng.gen_range(0..=self.max_point().row());
let start_row = rng.gen_range(0..=end_row);
let mut end_row = rng.random_range(0..=self.max_point().row());
let start_row = rng.random_range(0..=end_row);
end_row += 1;
let mut expected_text = self.text_chunks(start_row).collect::<String>();

View File

@@ -164,7 +164,7 @@ use project::{
DiagnosticSeverity, GitGutterSetting, GoToDiagnosticSeverityFilter, ProjectSettings,
},
};
use rand::{seq::SliceRandom, thread_rng};
use rand::seq::SliceRandom;
use rpc::{ErrorCode, ErrorExt, proto::PeerId};
use scroll::{Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide};
use selections_collection::{
@@ -1030,6 +1030,7 @@ pub struct Editor {
inline_diagnostics_update: Task<()>,
inline_diagnostics_enabled: bool,
diagnostics_enabled: bool,
word_completions_enabled: bool,
inline_diagnostics: Vec<(Anchor, InlineDiagnostic)>,
soft_wrap_mode_override: Option<language_settings::SoftWrap>,
hard_wrap: Option<usize>,
@@ -1794,7 +1795,7 @@ impl Editor {
let font_size = style.font_size.to_pixels(window.rem_size());
let editor = cx.entity().downgrade();
let fold_placeholder = FoldPlaceholder {
constrain_width: true,
constrain_width: false,
render: Arc::new(move |fold_id, fold_range, cx| {
let editor = editor.clone();
div()
@@ -2163,6 +2164,7 @@ impl Editor {
},
inline_diagnostics_enabled: full_mode,
diagnostics_enabled: full_mode,
word_completions_enabled: full_mode,
inline_value_cache: InlineValueCache::new(inlay_hint_settings.show_value_hints),
inlay_hint_cache: InlayHintCache::new(inlay_hint_settings),
gutter_hovered: false,
@@ -2617,7 +2619,7 @@ impl Editor {
cx: &mut Context<Workspace>,
) -> Task<Result<Entity<Editor>>> {
let project = workspace.project().clone();
let create = project.update(cx, |project, cx| project.create_buffer(cx));
let create = project.update(cx, |project, cx| project.create_buffer(true, cx));
cx.spawn_in(window, async move |workspace, cx| {
let buffer = create.await?;
@@ -2655,7 +2657,7 @@ impl Editor {
cx: &mut Context<Workspace>,
) {
let project = workspace.project().clone();
let create = project.update(cx, |project, cx| project.create_buffer(cx));
let create = project.update(cx, |project, cx| project.create_buffer(true, cx));
cx.spawn_in(window, async move |workspace, cx| {
let buffer = create.await?;
@@ -4892,8 +4894,15 @@ impl Editor {
});
match completions_source {
Some(CompletionsMenuSource::Words) => {
self.show_word_completions(&ShowWordCompletions, window, cx)
Some(CompletionsMenuSource::Words { .. }) => {
self.open_or_update_completions_menu(
Some(CompletionsMenuSource::Words {
ignore_threshold: false,
}),
None,
window,
cx,
);
}
Some(CompletionsMenuSource::Normal)
| Some(CompletionsMenuSource::SnippetChoices)
@@ -5401,7 +5410,14 @@ impl Editor {
window: &mut Window,
cx: &mut Context<Self>,
) {
self.open_or_update_completions_menu(Some(CompletionsMenuSource::Words), None, window, cx);
self.open_or_update_completions_menu(
Some(CompletionsMenuSource::Words {
ignore_threshold: true,
}),
None,
window,
cx,
);
}
pub fn show_completions(
@@ -5450,9 +5466,13 @@ impl Editor {
drop(multibuffer_snapshot);
let mut ignore_word_threshold = false;
let provider = match requested_source {
Some(CompletionsMenuSource::Normal) | None => self.completion_provider.clone(),
Some(CompletionsMenuSource::Words) => None,
Some(CompletionsMenuSource::Words { ignore_threshold }) => {
ignore_word_threshold = ignore_threshold;
None
}
Some(CompletionsMenuSource::SnippetChoices) => {
log::error!("bug: SnippetChoices requested_source is not handled");
None
@@ -5573,10 +5593,12 @@ impl Editor {
.as_ref()
.is_none_or(|query| !query.chars().any(|c| c.is_digit(10)));
let omit_word_completions = match &query {
Some(query) => query.chars().count() < completion_settings.words_min_length,
None => completion_settings.words_min_length != 0,
};
let omit_word_completions = !self.word_completions_enabled
|| (!ignore_word_threshold
&& match &query {
Some(query) => query.chars().count() < completion_settings.words_min_length,
None => completion_settings.words_min_length != 0,
});
let (mut words, provider_responses) = match &provider {
Some(provider) => {
@@ -10971,7 +10993,7 @@ impl Editor {
}
pub fn shuffle_lines(&mut self, _: &ShuffleLines, window: &mut Window, cx: &mut Context<Self>) {
self.manipulate_immutable_lines(window, cx, |lines| lines.shuffle(&mut thread_rng()))
self.manipulate_immutable_lines(window, cx, |lines| lines.shuffle(&mut rand::rng()))
}
fn manipulate_lines<M>(
@@ -11391,14 +11413,17 @@ impl Editor {
let mut edits = Vec::new();
let mut selection_adjustment = 0i32;
for selection in self.selections.all::<usize>(cx) {
for selection in self.selections.all_adjusted(cx) {
let selection_is_empty = selection.is_empty();
let (start, end) = if selection_is_empty {
let (word_range, _) = buffer.surrounding_word(selection.start, false);
(word_range.start, word_range.end)
} else {
(selection.start, selection.end)
(
buffer.point_to_offset(selection.start),
buffer.point_to_offset(selection.end),
)
};
let text = buffer.text_for_range(start..end).collect::<String>();
@@ -11409,7 +11434,8 @@ impl Editor {
start: (start as i32 - selection_adjustment) as usize,
end: ((start + text.len()) as i32 - selection_adjustment) as usize,
goal: SelectionGoal::None,
..selection
id: selection.id,
reversed: selection.reversed,
});
selection_adjustment += old_length - text.len() as i32;
@@ -17117,6 +17143,10 @@ impl Editor {
self.inline_diagnostics.clear();
}
pub fn disable_word_completions(&mut self) {
self.word_completions_enabled = false;
}
pub fn diagnostics_enabled(&self) -> bool {
self.diagnostics_enabled && self.mode.is_full()
}
@@ -20326,7 +20356,6 @@ impl Editor {
multi_buffer::Event::FileHandleChanged
| multi_buffer::Event::Reloaded
| multi_buffer::Event::BufferDiffChanged => cx.emit(EditorEvent::TitleChanged),
multi_buffer::Event::Closed => cx.emit(EditorEvent::Closed),
multi_buffer::Event::DiagnosticsUpdated => {
self.update_diagnostics_state(window, cx);
}
@@ -23024,7 +23053,6 @@ pub enum EditorEvent {
DirtyChanged,
Saved,
TitleChanged,
DiffBaseChanged,
SelectionsChanged {
local: bool,
},
@@ -23032,14 +23060,12 @@ pub enum EditorEvent {
local: bool,
autoscroll: bool,
},
Closed,
TransactionUndone {
transaction_id: clock::Lamport,
},
TransactionBegun {
transaction_id: clock::Lamport,
},
Reloaded,
CursorShapeChanged,
BreadcrumbsChanged,
PushedToNavHistory {

View File

@@ -6,7 +6,7 @@ use language::CursorShape;
use project::project_settings::DiagnosticSeverity;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources, SettingsUi, VsCodeSettings};
use settings::{Settings, SettingsKey, SettingsSources, SettingsUi, VsCodeSettings};
use util::serde::default_true;
/// Imports from the VSCode settings at
@@ -431,8 +431,9 @@ pub enum SnippetSortOrder {
None,
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi)]
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)]
#[settings_ui(group = "Editor")]
#[settings_key(None)]
pub struct EditorSettingsContent {
/// Whether the cursor blinks in the editor.
///
@@ -747,6 +748,7 @@ pub struct ScrollbarAxesContent {
#[derive(
Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq, SettingsUi,
)]
#[settings_ui(group = "Gutter")]
pub struct GutterContent {
/// Whether to show line numbers in the gutter.
///
@@ -777,8 +779,6 @@ impl EditorSettings {
}
impl Settings for EditorSettings {
const KEY: Option<&'static str> = None;
type FileContent = EditorSettingsContent;
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> anyhow::Result<Self> {

View File

@@ -5363,6 +5363,20 @@ async fn test_manipulate_text(cx: &mut TestAppContext) {
cx.assert_editor_state(indoc! {"
«HeLlO, wOrLD!ˇ»
"});
// Test selections with `line_mode = true`.
cx.update_editor(|editor, _window, _cx| editor.selections.line_mode = true);
cx.set_state(indoc! {"
«The quick brown
fox jumps over
tˇ»he lazy dog
"});
cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
cx.assert_editor_state(indoc! {"
«THE QUICK BROWN
FOX JUMPS OVER
THE LAZY DOGˇ»
"});
}
#[gpui::test]
@@ -14264,6 +14278,26 @@ async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppCont
}
});
cx.update_editor(|editor, window, cx| {
editor.show_word_completions(&ShowWordCompletions, window, cx);
});
cx.executor().run_until_parked();
cx.update_editor(|editor, window, cx| {
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
{
assert_eq!(completion_menu_entries(menu), &["wowser", "wowen", "wow"], "Even though the threshold is not met, invoking word completions with an action should provide the completions");
} else {
panic!("expected completion menu to be open after the word completions are called with an action");
}
editor.cancel(&Cancel, window, cx);
});
cx.update_editor(|editor, _, _| {
if editor.context_menu.borrow_mut().is_some() {
panic!("expected completion menu to be hidden after canceling");
}
});
cx.simulate_keystroke("o");
cx.executor().run_until_parked();
cx.update_editor(|editor, _, _| {
@@ -14286,6 +14320,50 @@ async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppCont
});
}
#[gpui::test]
async fn test_word_completions_disabled(cx: &mut TestAppContext) {
init_test(cx, |language_settings| {
language_settings.defaults.completions = Some(CompletionSettings {
words: WordsCompletionMode::Enabled,
words_min_length: 0,
lsp: true,
lsp_fetch_timeout_ms: 0,
lsp_insert_mode: LspInsertMode::Insert,
});
});
let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
cx.update_editor(|editor, _, _| {
editor.disable_word_completions();
});
cx.set_state(indoc! {"ˇ
wow
wowen
wowser
"});
cx.simulate_keystroke("w");
cx.executor().run_until_parked();
cx.update_editor(|editor, _, _| {
if editor.context_menu.borrow_mut().is_some() {
panic!(
"expected completion menu to be hidden, as words completion are disabled for this editor"
);
}
});
cx.update_editor(|editor, window, cx| {
editor.show_word_completions(&ShowWordCompletions, window, cx);
});
cx.executor().run_until_parked();
cx.update_editor(|editor, _, _| {
if editor.context_menu.borrow_mut().is_some() {
panic!(
"expected completion menu to be hidden even if called for explicitly, as words completion are disabled for this editor"
);
}
});
}
fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
let position = || lsp::Position {
line: params.text_document_position.position.line,
@@ -15835,7 +15913,7 @@ async fn test_following(cx: &mut TestAppContext) {
let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
let buffer = project.update(cx, |project, cx| {
let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, false, cx);
cx.new(|cx| MultiBuffer::singleton(buffer, cx))
});
let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
@@ -16087,8 +16165,8 @@ async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
(
project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
project.create_local_buffer("abc\ndef\nghi\njkl\n", None, false, cx),
project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, false, cx),
)
});
@@ -16783,6 +16861,7 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppCon
"some other init value": false
})),
enable_lsp_tasks: false,
fetch: None,
},
);
});
@@ -16803,6 +16882,7 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppCon
"anotherInitValue": false
})),
enable_lsp_tasks: false,
fetch: None,
},
);
});
@@ -16823,6 +16903,7 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppCon
"anotherInitValue": false
})),
enable_lsp_tasks: false,
fetch: None,
},
);
});
@@ -16841,6 +16922,7 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppCon
settings: None,
initialization_options: None,
enable_lsp_tasks: false,
fetch: None,
},
);
});

View File

@@ -1107,7 +1107,7 @@ mod tests {
init_test(cx);
let fs = FakeFs::new(cx.executor());
let buffer_initial_text_len = rng.gen_range(5..15);
let buffer_initial_text_len = rng.random_range(5..15);
let mut buffer_initial_text = Rope::from(
RandomCharIter::new(&mut rng)
.take(buffer_initial_text_len)
@@ -1159,7 +1159,7 @@ mod tests {
git_blame.update(cx, |blame, cx| blame.check_invariants(cx));
for _ in 0..operations {
match rng.gen_range(0..100) {
match rng.random_range(0..100) {
0..=19 => {
log::info!("quiescing");
cx.executor().run_until_parked();
@@ -1202,8 +1202,8 @@ mod tests {
let mut blame_entries = Vec::new();
for ix in 0..5 {
if last_row < max_row {
let row_start = rng.gen_range(last_row..max_row);
let row_end = rng.gen_range(row_start + 1..cmp::min(row_start + 3, max_row) + 1);
let row_start = rng.random_range(last_row..max_row);
let row_end = rng.random_range(row_start + 1..cmp::min(row_start + 3, max_row) + 1);
blame_entries.push(blame_entry(&ix.to_string(), row_start..row_end));
last_row = row_end;
} else {

View File

@@ -651,7 +651,8 @@ impl Item for Editor {
if let Some(path) = path_for_buffer(&self.buffer, detail, true, cx) {
path.to_string_lossy().to_string().into()
} else {
"untitled".into()
// Use the same logic as the displayed title for consistency
self.buffer.read(cx).title(cx).to_string().into()
}
}
@@ -775,12 +776,6 @@ impl Item for Editor {
self.nav_history = Some(history);
}
fn discarded(&self, _project: Entity<Project>, _: &mut Window, cx: &mut Context<Self>) {
for buffer in self.buffer().clone().read(cx).all_buffers() {
buffer.update(cx, |buffer, cx| buffer.discarded(cx))
}
}
fn on_removed(&self, cx: &App) {
self.report_editor_event(ReportEditorEvent::Closed, None, cx);
}
@@ -1022,8 +1017,6 @@ impl Item for Editor {
fn to_item_events(event: &EditorEvent, mut f: impl FnMut(ItemEvent)) {
match event {
EditorEvent::Closed => f(ItemEvent::CloseItem),
EditorEvent::Saved | EditorEvent::TitleChanged => {
f(ItemEvent::UpdateTab);
f(ItemEvent::UpdateBreadcrumbs);
@@ -1137,7 +1130,7 @@ impl SerializableItem for Editor {
// First create the empty buffer
let buffer = project
.update(cx, |project, cx| project.create_buffer(cx))?
.update(cx, |project, cx| project.create_buffer(true, cx))?
.await?;
// Then set the text so that the dirty bit is set correctly
@@ -1245,7 +1238,7 @@ impl SerializableItem for Editor {
..
} => window.spawn(cx, async move |cx| {
let buffer = project
.update(cx, |project, cx| project.create_buffer(cx))?
.update(cx, |project, cx| project.create_buffer(true, cx))?
.await?;
cx.update(|window, cx| {

View File

@@ -200,7 +200,7 @@ pub fn expand_macro_recursively(
}
let buffer = project
.update(cx, |project, cx| project.create_buffer(cx))?
.update(cx, |project, cx| project.create_buffer(false, cx))?
.await?;
workspace.update_in(cx, |workspace, window, cx| {
buffer.update(cx, |buffer, cx| {

View File

@@ -3,10 +3,11 @@ use collections::HashMap;
use gpui::App;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources, SettingsUi};
use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
use std::sync::Arc;
#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema, SettingsUi)]
#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema, SettingsUi, SettingsKey)]
#[settings_key(None)]
pub struct ExtensionSettings {
/// The extensions that should be automatically installed by Zed.
///
@@ -38,8 +39,6 @@ impl ExtensionSettings {
}
impl Settings for ExtensionSettings {
const KEY: Option<&'static str> = None;
type FileContent = Self;
fn load(sources: SettingsSources<Self::FileContent>, _cx: &mut App) -> Result<Self> {

View File

@@ -1345,7 +1345,7 @@ impl ExtensionsPage {
this.update_settings::<VimModeSetting>(
selection,
cx,
|setting, value| *setting = Some(value),
|setting, value| setting.vim_mode = Some(value),
);
}),
)),

View File

@@ -1,7 +1,7 @@
use anyhow::Result;
use schemars::JsonSchema;
use serde_derive::{Deserialize, Serialize};
use settings::{Settings, SettingsSources, SettingsUi};
use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
#[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
pub struct FileFinderSettings {
@@ -11,7 +11,8 @@ pub struct FileFinderSettings {
pub include_ignored: Option<bool>,
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)]
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)]
#[settings_key(key = "file_finder")]
pub struct FileFinderSettingsContent {
/// Whether to show file icons in the file finder.
///
@@ -42,8 +43,6 @@ pub struct FileFinderSettingsContent {
}
impl Settings for FileFinderSettings {
const KEY: Option<&'static str> = Some("file_finder");
type FileContent = FileFinderSettingsContent;
fn load(sources: SettingsSources<Self::FileContent>, _: &mut gpui::App) -> Result<Self> {

View File

@@ -1,7 +1,7 @@
use crate::file_finder_settings::FileFinderSettings;
use file_icons::FileIcons;
use futures::channel::oneshot;
use fuzzy::{StringMatch, StringMatchCandidate};
use fuzzy::{CharBag, StringMatch, StringMatchCandidate};
use gpui::{HighlightStyle, StyledText, Task};
use picker::{Picker, PickerDelegate};
use project::{DirectoryItem, DirectoryLister};
@@ -23,7 +23,6 @@ use workspace::Workspace;
pub(crate) struct OpenPathPrompt;
#[derive(Debug)]
pub struct OpenPathDelegate {
tx: Option<oneshot::Sender<Option<Vec<PathBuf>>>>,
lister: DirectoryLister,
@@ -35,6 +34,9 @@ pub struct OpenPathDelegate {
prompt_root: String,
path_style: PathStyle,
replace_prompt: Task<()>,
render_footer:
Arc<dyn Fn(&mut Window, &mut Context<Picker<Self>>) -> Option<AnyElement> + 'static>,
hidden_entries: bool,
}
impl OpenPathDelegate {
@@ -60,9 +62,25 @@ impl OpenPathDelegate {
},
path_style,
replace_prompt: Task::ready(()),
render_footer: Arc::new(|_, _| None),
hidden_entries: false,
}
}
pub fn with_footer(
mut self,
footer: Arc<
dyn Fn(&mut Window, &mut Context<Picker<Self>>) -> Option<AnyElement> + 'static,
>,
) -> Self {
self.render_footer = footer;
self
}
pub fn show_hidden(mut self) -> Self {
self.hidden_entries = true;
self
}
fn get_entry(&self, selected_match_index: usize) -> Option<CandidateInfo> {
match &self.directory_state {
DirectoryState::List { entries, .. } => {
@@ -125,6 +143,13 @@ impl OpenPathDelegate {
DirectoryState::None { .. } => Vec::new(),
}
}
fn current_dir(&self) -> &'static str {
match self.path_style {
PathStyle::Posix => "./",
PathStyle::Windows => ".\\",
}
}
}
#[derive(Debug)]
@@ -233,6 +258,7 @@ impl PickerDelegate for OpenPathDelegate {
cx: &mut Context<Picker<Self>>,
) -> Task<()> {
let lister = &self.lister;
let input_is_empty = query.is_empty();
let (dir, suffix) = get_dir_and_suffix(query, self.path_style);
let query = match &self.directory_state {
@@ -261,8 +287,9 @@ impl PickerDelegate for OpenPathDelegate {
self.cancel_flag.store(true, atomic::Ordering::Release);
self.cancel_flag = Arc::new(AtomicBool::new(false));
let cancel_flag = self.cancel_flag.clone();
let hidden_entries = self.hidden_entries;
let parent_path_is_root = self.prompt_root == dir;
let current_dir = self.current_dir();
cx.spawn_in(window, async move |this, cx| {
if let Some(query) = query {
let paths = query.await;
@@ -353,10 +380,38 @@ impl PickerDelegate for OpenPathDelegate {
return;
};
if !suffix.starts_with('.') {
new_entries.retain(|entry| !entry.path.string.starts_with('.'));
let mut max_id = 0;
if !suffix.starts_with('.') && !hidden_entries {
new_entries.retain(|entry| {
max_id = max_id.max(entry.path.id);
!entry.path.string.starts_with('.')
});
}
if suffix.is_empty() {
let should_prepend_with_current_dir = this
.read_with(cx, |picker, _| {
!input_is_empty
&& !matches!(
picker.delegate.directory_state,
DirectoryState::Create { .. }
)
})
.unwrap_or(false);
if should_prepend_with_current_dir {
new_entries.insert(
0,
CandidateInfo {
path: StringMatchCandidate {
id: max_id + 1,
string: current_dir.to_string(),
char_bag: CharBag::from(current_dir),
},
is_dir: true,
},
);
}
this.update(cx, |this, cx| {
this.delegate.selected_index = 0;
this.delegate.string_matches = new_entries
@@ -485,6 +540,10 @@ impl PickerDelegate for OpenPathDelegate {
_: &mut Context<Picker<Self>>,
) -> Option<String> {
let candidate = self.get_entry(self.selected_index)?;
if candidate.path.string.is_empty() || candidate.path.string == self.current_dir() {
return None;
}
let path_style = self.path_style;
Some(
maybe!({
@@ -629,12 +688,18 @@ impl PickerDelegate for OpenPathDelegate {
DirectoryState::None { .. } => Vec::new(),
};
let is_current_dir_candidate = candidate.path.string == self.current_dir();
let file_icon = maybe!({
if !settings.file_icons {
return None;
}
let icon = if candidate.is_dir {
FileIcons::get_folder_icon(false, cx)?
if is_current_dir_candidate {
return Some(Icon::new(IconName::ReplyArrowRight).color(Color::Muted));
} else {
FileIcons::get_folder_icon(false, cx)?
}
} else {
let path = path::Path::new(&candidate.path.string);
FileIcons::get_icon(path, cx)?
@@ -652,6 +717,8 @@ impl PickerDelegate for OpenPathDelegate {
.child(HighlightedLabel::new(
if parent_path == &self.prompt_root {
format!("{}{}", self.prompt_root, candidate.path.string)
} else if is_current_dir_candidate {
"open this directory".to_string()
} else {
candidate.path.string
},
@@ -732,6 +799,14 @@ impl PickerDelegate for OpenPathDelegate {
}
}
fn render_footer(
&self,
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<AnyElement> {
(self.render_footer)(window, cx)
}
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
Some(match &self.directory_state {
DirectoryState::Create { .. } => SharedString::from("Type a path…"),
@@ -747,6 +822,17 @@ impl PickerDelegate for OpenPathDelegate {
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
Arc::from(format!("[directory{MAIN_SEPARATOR_STR}]filename.ext"))
}
fn separators_after_indices(&self) -> Vec<usize> {
let Some(m) = self.string_matches.first() else {
return Vec::new();
};
if m.string == self.current_dir() {
vec![0]
} else {
Vec::new()
}
}
}
fn path_candidates(

View File

@@ -43,12 +43,17 @@ async fn test_open_path_prompt(cx: &mut TestAppContext) {
insert_query(query, &picker, cx).await;
assert_eq!(collect_match_candidates(&picker, cx), vec!["root"]);
#[cfg(not(windows))]
let expected_separator = "./";
#[cfg(windows)]
let expected_separator = ".\\";
// If the query ends with a slash, the picker should show the contents of the directory.
let query = path!("/root/");
insert_query(query, &picker, cx).await;
assert_eq!(
collect_match_candidates(&picker, cx),
vec!["a1", "a2", "a3", "dir1", "dir2"]
vec![expected_separator, "a1", "a2", "a3", "dir1", "dir2"]
);
// Show candidates for the query "a".
@@ -72,7 +77,7 @@ async fn test_open_path_prompt(cx: &mut TestAppContext) {
insert_query(query, &picker, cx).await;
assert_eq!(
collect_match_candidates(&picker, cx),
vec!["c", "d1", "d2", "d3", "dir3", "dir4"]
vec![expected_separator, "c", "d1", "d2", "d3", "dir3", "dir4"]
);
// Show candidates for the query "d".
@@ -116,71 +121,86 @@ async fn test_open_path_prompt_completion(cx: &mut TestAppContext) {
// Confirm completion for the query "/root", since it's a directory, it should add a trailing slash.
let query = path!("/root");
insert_query(query, &picker, cx).await;
assert_eq!(confirm_completion(query, 0, &picker, cx), path!("/root/"));
assert_eq!(
confirm_completion(query, 0, &picker, cx).unwrap(),
path!("/root/")
);
// Confirm completion for the query "/root/", selecting the first candidate "a", since it's a file, it should not add a trailing slash.
let query = path!("/root/");
insert_query(query, &picker, cx).await;
assert_eq!(confirm_completion(query, 0, &picker, cx), path!("/root/a"));
assert_eq!(
confirm_completion(query, 0, &picker, cx),
None,
"First entry is `./` and when we confirm completion, it is tabbed below"
);
assert_eq!(
confirm_completion(query, 1, &picker, cx).unwrap(),
path!("/root/a"),
"Second entry is the first entry of a directory that we want to be completed"
);
// Confirm completion for the query "/root/", selecting the second candidate "dir1", since it's a directory, it should add a trailing slash.
let query = path!("/root/");
insert_query(query, &picker, cx).await;
assert_eq!(
confirm_completion(query, 1, &picker, cx),
confirm_completion(query, 2, &picker, cx).unwrap(),
path!("/root/dir1/")
);
let query = path!("/root/a");
insert_query(query, &picker, cx).await;
assert_eq!(confirm_completion(query, 0, &picker, cx), path!("/root/a"));
assert_eq!(
confirm_completion(query, 0, &picker, cx).unwrap(),
path!("/root/a")
);
let query = path!("/root/d");
insert_query(query, &picker, cx).await;
assert_eq!(
confirm_completion(query, 1, &picker, cx),
confirm_completion(query, 1, &picker, cx).unwrap(),
path!("/root/dir2/")
);
let query = path!("/root/dir2");
insert_query(query, &picker, cx).await;
assert_eq!(
confirm_completion(query, 0, &picker, cx),
confirm_completion(query, 0, &picker, cx).unwrap(),
path!("/root/dir2/")
);
let query = path!("/root/dir2/");
insert_query(query, &picker, cx).await;
assert_eq!(
confirm_completion(query, 0, &picker, cx),
confirm_completion(query, 1, &picker, cx).unwrap(),
path!("/root/dir2/c")
);
let query = path!("/root/dir2/");
insert_query(query, &picker, cx).await;
assert_eq!(
confirm_completion(query, 2, &picker, cx),
confirm_completion(query, 3, &picker, cx).unwrap(),
path!("/root/dir2/dir3/")
);
let query = path!("/root/dir2/d");
insert_query(query, &picker, cx).await;
assert_eq!(
confirm_completion(query, 0, &picker, cx),
confirm_completion(query, 0, &picker, cx).unwrap(),
path!("/root/dir2/d")
);
let query = path!("/root/dir2/d");
insert_query(query, &picker, cx).await;
assert_eq!(
confirm_completion(query, 1, &picker, cx),
confirm_completion(query, 1, &picker, cx).unwrap(),
path!("/root/dir2/dir3/")
);
let query = path!("/root/dir2/di");
insert_query(query, &picker, cx).await;
assert_eq!(
confirm_completion(query, 1, &picker, cx),
confirm_completion(query, 1, &picker, cx).unwrap(),
path!("/root/dir2/dir4/")
);
}
@@ -211,42 +231,63 @@ async fn test_open_path_prompt_on_windows(cx: &mut TestAppContext) {
insert_query(query, &picker, cx).await;
assert_eq!(
collect_match_candidates(&picker, cx),
vec!["a", "dir1", "dir2"]
vec![".\\", "a", "dir1", "dir2"]
);
assert_eq!(
confirm_completion(query, 0, &picker, cx),
None,
"First entry is `.\\` and when we confirm completion, it is tabbed below"
);
assert_eq!(
confirm_completion(query, 1, &picker, cx).unwrap(),
"C:/root/a",
"Second entry is the first entry of a directory that we want to be completed"
);
assert_eq!(confirm_completion(query, 0, &picker, cx), "C:/root/a");
let query = "C:\\root/";
insert_query(query, &picker, cx).await;
assert_eq!(
collect_match_candidates(&picker, cx),
vec!["a", "dir1", "dir2"]
vec![".\\", "a", "dir1", "dir2"]
);
assert_eq!(
confirm_completion(query, 1, &picker, cx).unwrap(),
"C:\\root/a"
);
assert_eq!(confirm_completion(query, 0, &picker, cx), "C:\\root/a");
let query = "C:\\root\\";
insert_query(query, &picker, cx).await;
assert_eq!(
collect_match_candidates(&picker, cx),
vec!["a", "dir1", "dir2"]
vec![".\\", "a", "dir1", "dir2"]
);
assert_eq!(
confirm_completion(query, 1, &picker, cx).unwrap(),
"C:\\root\\a"
);
assert_eq!(confirm_completion(query, 0, &picker, cx), "C:\\root\\a");
// Confirm completion for the query "C:/root/d", selecting the second candidate "dir2", since it's a directory, it should add a trailing slash.
let query = "C:/root/d";
insert_query(query, &picker, cx).await;
assert_eq!(collect_match_candidates(&picker, cx), vec!["dir1", "dir2"]);
assert_eq!(confirm_completion(query, 1, &picker, cx), "C:/root/dir2\\");
assert_eq!(
confirm_completion(query, 1, &picker, cx).unwrap(),
"C:/root/dir2\\"
);
let query = "C:\\root/d";
insert_query(query, &picker, cx).await;
assert_eq!(collect_match_candidates(&picker, cx), vec!["dir1", "dir2"]);
assert_eq!(confirm_completion(query, 0, &picker, cx), "C:\\root/dir1\\");
assert_eq!(
confirm_completion(query, 0, &picker, cx).unwrap(),
"C:\\root/dir1\\"
);
let query = "C:\\root\\d";
insert_query(query, &picker, cx).await;
assert_eq!(collect_match_candidates(&picker, cx), vec!["dir1", "dir2"]);
assert_eq!(
confirm_completion(query, 0, &picker, cx),
confirm_completion(query, 0, &picker, cx).unwrap(),
"C:\\root\\dir1\\"
);
}
@@ -276,20 +317,29 @@ async fn test_open_path_prompt_on_windows_with_remote(cx: &mut TestAppContext) {
insert_query(query, &picker, cx).await;
assert_eq!(
collect_match_candidates(&picker, cx),
vec!["a", "dir1", "dir2"]
vec!["./", "a", "dir1", "dir2"]
);
assert_eq!(
confirm_completion(query, 1, &picker, cx).unwrap(),
"/root/a"
);
assert_eq!(confirm_completion(query, 0, &picker, cx), "/root/a");
// Confirm completion for the query "/root/d", selecting the second candidate "dir2", since it's a directory, it should add a trailing slash.
let query = "/root/d";
insert_query(query, &picker, cx).await;
assert_eq!(collect_match_candidates(&picker, cx), vec!["dir1", "dir2"]);
assert_eq!(confirm_completion(query, 1, &picker, cx), "/root/dir2/");
assert_eq!(
confirm_completion(query, 1, &picker, cx).unwrap(),
"/root/dir2/"
);
let query = "/root/d";
insert_query(query, &picker, cx).await;
assert_eq!(collect_match_candidates(&picker, cx), vec!["dir1", "dir2"]);
assert_eq!(confirm_completion(query, 0, &picker, cx), "/root/dir1/");
assert_eq!(
confirm_completion(query, 0, &picker, cx).unwrap(),
"/root/dir1/"
);
}
#[gpui::test]
@@ -396,15 +446,13 @@ fn confirm_completion(
select: usize,
picker: &Entity<Picker<OpenPathDelegate>>,
cx: &mut VisualTestContext,
) -> String {
picker
.update_in(cx, |f, window, cx| {
if f.delegate.selected_index() != select {
f.delegate.set_selected_index(select, window, cx);
}
f.delegate.confirm_completion(query.to_string(), window, cx)
})
.unwrap()
) -> Option<String> {
picker.update_in(cx, |f, window, cx| {
if f.delegate.selected_index() != select {
f.delegate.set_selected_index(select, window, cx);
}
f.delegate.confirm_completion(query.to_string(), window, cx)
})
}
fn collect_match_candidates(

View File

@@ -20,6 +20,9 @@ use std::os::fd::{AsFd, AsRawFd};
#[cfg(unix)]
use std::os::unix::fs::{FileTypeExt, MetadataExt};
#[cfg(any(target_os = "macos", target_os = "freebsd"))]
use std::mem::MaybeUninit;
use async_tar::Archive;
use futures::{AsyncRead, Stream, StreamExt, future::BoxFuture};
use git::repository::{GitRepository, RealGitRepository};
@@ -261,14 +264,15 @@ impl FileHandle for std::fs::File {
};
let fd = self.as_fd();
let mut path_buf: [libc::c_char; libc::PATH_MAX as usize] = [0; libc::PATH_MAX as usize];
let mut path_buf = MaybeUninit::<[u8; libc::PATH_MAX as usize]>::uninit();
let result = unsafe { libc::fcntl(fd.as_raw_fd(), libc::F_GETPATH, path_buf.as_mut_ptr()) };
if result == -1 {
anyhow::bail!("fcntl returned -1".to_string());
}
let c_str = unsafe { CStr::from_ptr(path_buf.as_ptr()) };
// SAFETY: `fcntl` will initialize the path buffer.
let c_str = unsafe { CStr::from_ptr(path_buf.as_ptr().cast()) };
let path = PathBuf::from(OsStr::from_bytes(c_str.to_bytes()));
Ok(path)
}
@@ -296,15 +300,16 @@ impl FileHandle for std::fs::File {
};
let fd = self.as_fd();
let mut kif: libc::kinfo_file = unsafe { std::mem::zeroed() };
let mut kif = MaybeUninit::<libc::kinfo_file>::uninit();
kif.kf_structsize = libc::KINFO_FILE_SIZE;
let result = unsafe { libc::fcntl(fd.as_raw_fd(), libc::F_KINFO, &mut kif) };
let result = unsafe { libc::fcntl(fd.as_raw_fd(), libc::F_KINFO, kif.as_mut_ptr()) };
if result == -1 {
anyhow::bail!("fcntl returned -1".to_string());
}
let c_str = unsafe { CStr::from_ptr(kif.kf_path.as_ptr()) };
// SAFETY: `fcntl` will initialize the kif.
let c_str = unsafe { CStr::from_ptr(kif.assume_init().kf_path.as_ptr()) };
let path = PathBuf::from(OsStr::from_bytes(c_str.to_bytes()));
Ok(path)
}

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