Compare commits

..

65 Commits

Author SHA1 Message Date
Conrad Irwin
0b81c19fa1 vim: Add zH/zL/zh/zl 2025-03-14 12:41:11 -06:00
Danilo Leal
83dfdb0cfe assistant2: Add "running" status feedback in the disclosure (#26786)
Just a tiny bit of polish here, so that if the user expands the
disclosure, an equivalent loading state is at the response container.

<img
src="https://github.com/user-attachments/assets/a2ecb7f4-c9ea-4a14-8a60-9f7f2983a1a1"
width="600px" />

Release Notes:

- N/A
2025-03-14 12:31:26 -03:00
Kirill Bulatov
566c5f91a7 Refine word completions (#26779)
Follow-up of https://github.com/zed-industries/zed/pull/26410

* Extract word completions into their own, `editor::ShowWordCompletions`
action so those could be triggered independently of completions
* Assign `ctrl-shift-space` binding to this new action
* Still keep words returned along the completions as in the original PR,
but:
* Tone down regular completions' fallback logic, skip words when the
language server responds with empty list of completions, but keep on
adding words if nothing or an error were returned instead
    * Adjust the defaults to wait for LSP completions infinitely
* Skip "words" with digits such as `0_usize` or `2.f32` from completion
items, unless a completion query has digits in it

Release Notes:

- N/A
2025-03-14 15:18:55 +00:00
Danilo Leal
21057e3af7 assistant2: Refine thread design (#26783)
Just some light design polish while we're in-flight with this.

<img
src="https://github.com/user-attachments/assets/40a68fe6-f37e-4df1-b669-824c7dd8ff11"
width="600px" />

---

Release Notes:

- N/A
2025-03-14 12:09:24 -03:00
Antonio Scandurra
f68a475eca Introduce rating for assistant threads (#26780)
Release Notes:

- N/A

---------

Co-authored-by: Richard Feldman <oss@rtfeldman.com>
Co-authored-by: Agus Zubiaga <hi@aguz.me>
2025-03-14 14:41:50 +00:00
Smit Barmase
c62210b178 copilot: Handle sign out when copilot language server is not running (#26776)
When copilot is not being used as the edit prediction provider and you
open a fresh Zed instance, we don’t run the copilot language server.
This is because copilot chat is purely handled via oauth token and
doesn’t require the language server.

In this case, if you click sign out, instead of asking the language
server to sign out (which isn’t running), we can manually clear the
config directory, which contains the oauth tokens. We already watch this
directory, and if the token is not found, we update the sign-in status.

Release Notes:

- N/A
2025-03-14 19:41:27 +05:30
Danilo Leal
ad14dcc57b assistant2: Truncate thread title in context picker (#26775)
Similar issue as in https://github.com/zed-industries/zed/pull/26721.

Release Notes:

- N/A
2025-03-14 11:03:57 -03:00
Smit Barmase
b9432dbe42 macOS: Disable fullscreen window tabbing (take 2) (#26774)
Take 2 on https://github.com/zed-industries/zed/pull/26600. Now, it
doesn't break remote development.

Instead of using it in `build_classes`, it's now used in the `open`
method while creating a window. I found similar usage in other places
over internet.

Release Notes:

- Fixed issue where Zed would show mac native tabs when opening new
fullscreen windows on macOS.
2025-03-14 19:13:01 +05:30
Kamal Ahmad
41c373eff1 gpui: Add support for text in SVGs (#26335)
Closes #21319
Before: 

![image](https://github.com/user-attachments/assets/f75d7d59-75b1-4836-ae3b-6a1f526a5833)
After:

![image](https://github.com/user-attachments/assets/5fa28a6d-c417-4777-99f8-2a17edf759a0)

Use fontdb to load system fonts and pass it to resvg renderer. This adds
a small increase in startup time (around 30ms on my Linux system to
traverse fonts on a cold start). In the future once cosmic-text bumps
their version of fontdb we could clone the Database from
CosmicTextSystem

Release Notes: 
- Added: support for rendering text in SVGs
2025-03-14 08:25:11 -05:00
Smit Barmase
6a95ec6a64 copilot: Decouple copilot sign in from edit prediction settings (#26689)
Closes #25883

This PR allows you to use copilot chat for assistant without setting
copilot as the edit prediction provider.


[copilot.webm](https://github.com/user-attachments/assets/fecfbde1-d72c-4c0c-b080-a07671fb846e)

Todos:
- [x] Remove redudant "copilot" key from settings
- [x] Do not disable copilot LSP when `edit_prediction_provider` is not
set to `copilot`
- [x] Start copilot LSP when:
  - [x]  `edit_prediction_provider` is set to `copilot`
  - [x] Copilot sign in clicked from assistant settings
- [x] Handle flicker for frame after starting LSP, but before signing in
caused due to signed out status
- [x] Fixed this by adding intermediate state for awaiting signing in in
sign out enum
- [x] Handle cancel button should sign out from `copilot` (existing bug)
- [x] Handle modal dismissal should sign out if not in signed in state
(existing bug)

Release Notes:

- You can now sign into Copilot from assistant settings without making
it your edit prediction provider. This is useful if you want to use
Copilot chat while keeping a different provider, like Zed, for
predictions.
- Removed the `copilot` key from `features` in settings. Use
`edit_prediction_provider` instead.
2025-03-14 15:10:56 +05:30
Anthony Eid
8d7b021f92 Fix editor's outline view confirm not working before any queries have (#26761)
## Summary
This PR fixes a minor bug where editor's outline view wouldn't move the
cursor on confirm before any outline queries have been made.

### Before 

https://github.com/user-attachments/assets/6ccca0c1-c0fa-46cb-b700-28a666d62ce8

### After

https://github.com/user-attachments/assets/d508e20b-90fb-471a-b974-431205501c89

Release Notes:

- Fixes bug where editor's outline view wouldn't move cursor on confirm
action
2025-03-14 07:19:43 +00:00
Conrad Irwin
798a34bfc2 Show git toasts for 10s (#26714)
Release Notes:

- N/A
2025-03-13 22:51:07 -06:00
Conrad Irwin
a4a9f6bd07 Merge excerpts in project diff (#26739)
This adds code to merge excerpts when you expand them and they would
overlap. It is only enabled for callers who use the
`set_excerpts_for_path` API for multibuffers (which is currently just
project diff), as other users of multibuffer care too much about the
exact excerpts that they have.

Release Notes:

- N/A
2025-03-13 22:50:42 -06:00
Conrad Irwin
bfe4c40f73 Revert "Disable automatic window tabbing (cherry-pick #26600) (#26652)" (#26749)
This reverts commit 391eb380b5.

For some reason that is very unclear to me, this broke ssh'ing into
macOS remotes.
The remote process aborts with:

```
-------------------------------------
Translated Report (Full Report Below)
-------------------------------------

Process:               zed-remote-server-dev-build [78088]
Path:                  /Users/USER/*/zed-remote-server-dev-build
Identifier:            zed-remote-server-dev-build
Version:               ???
Code Type:             ARM-64 (Native)
Parent Process:        launchd [1]
Responsible:           iTerm2 [62245]
User ID:               501

Date/Time:             2025-03-13 19:30:37.6827 -0600
OS Version:            macOS 15.3.1 (24D70)
Report Version:        12
Anonymous UUID:        3A9631EB-5468-8CA4-7A0F-E36C3FF9D04F

Sleep/Wake UUID:       C935AE4C-E06A-4F6D-BE97-101E4E03482F

Time Awake Since Boot: 910000 seconds
Time Since Wake:       1265 seconds

System Integrity Protection: enabled

Crashed Thread:        0  Dispatch queue: com.apple.main-thread

Exception Type:        EXC_CRASH (SIGABRT)
Exception Codes:       0x0000000000000000, 0x0000000000000000

Termination Reason:    Namespace OBJC, Code 1 

Application Specific Information:
crashed on child side of fork pre-exec


Thread 0 Crashed::  Dispatch queue: com.apple.main-thread
0   libsystem_kernel.dylib        	       0x18653fc6c __abort_with_payload + 8
1   libsystem_kernel.dylib        	       0x186565eb8 abort_with_payload_wrapper_internal + 104
2   libsystem_kernel.dylib        	       0x186565e50 abort_with_reason + 32
3   libobjc.A.dylib               	       0x1861dc040 _objc_fatalv(unsigned long long, unsigned long long, char const*, char*) + 128
4   libobjc.A.dylib               	       0x1861dbfc0 _objc_fatal(char const*, ...) + 44
5   libobjc.A.dylib               	       0x1861c1674 performForkChildInitialize(objc_class*, objc_class*) + 400
6   libobjc.A.dylib               	       0x1861a67f0 initializeNonMetaClass + 592
7   libobjc.A.dylib               	       0x1861c4a3c initializeAndMaybeRelock(objc_class*, objc_object*, locker_mixin<lockdebug::lock_mixin<objc_lock_base_t>>&, bool) + 164
8   libobjc.A.dylib               	       0x1861a5f98 lookUpImpOrForward + 304
9   libobjc.A.dylib               	       0x1861a5b84 _objc_msgSend_uncached + 68
10  zed-remote-server-dev-build   	       0x104f9ec4c _$LT$$LP$$RP$$u20$as$u20$objc..message..MessageArguments$GT$::invoke::hf68c58806f4b5702 + 56
11  zed-remote-server-dev-build   	       0x104f9d4c8 objc::message::platform::send_unverified::h2ec8392957fd6551 + 120
12  zed-remote-server-dev-build   	       0x104e5631c cocoa::appkit::NSPasteboard::generalPasteboard::h68122d7f32549cba + 512
13  zed-remote-server-dev-build   	       0x104e3b3b4 gpui::platform::mac::platform::MacPlatform::new::hb68d7ae2c5fdea7e + 336
14  zed-remote-server-dev-build   	       0x104e48008 gpui::platform::current_platform::h931999673c8c6468 + 28
15  zed-remote-server-dev-build   	       0x104ee4284 gpui::app::Application::headless::h3bffec62c65240ce + 32
16  zed-remote-server-dev-build   	       0x1023746ac remote_server::unix::execute_run::h7ac8de1a7e257f61 + 1200
17  zed-remote-server-dev-build   	       0x102368e1c remote_server::main::h42e4b18462b32dcf + 252 (main.rs:56)
18  zed-remote-server-dev-build   	       0x10236717c core::ops::function::FnOnce::call_once::h8534244cea12c898 + 16 (function.rs:250)
19  zed-remote-server-dev-build   	       0x102368154 std::sys::backtrace::__rust_begin_short_backtrace::h22fd48e0f46eb10b + 12 (backtrace.rs:152)
20  zed-remote-server-dev-build   	       0x10236bf74 std::rt::lang_start::_$u7b$$u7b$closure$u7d$$u7d$::hf8bd0081bf8d785b + 16 (rt.rs:195)
21  zed-remote-server-dev-build   	       0x105723d20 std::rt::lang_start_internal::h5f91760815528aa2 + 1092
22  zed-remote-server-dev-build   	       0x10236bf50 std::rt::lang_start::hb88fe48ac1498ea6 + 60 (rt.rs:194)
23  zed-remote-server-dev-build   	       0x10236b67c main + 36
24  dyld                          	       0x1861f4274 start + 2840
```

Which is not even (apparently) on the line that calls this function.

To reproduce this, run `ZED_BUILD_REMOTE_SERVER=true cargo run
ssh://127.0.0.1/~/`.

Release Notes:

- N/A
2025-03-13 20:55:22 -06:00
Ben Kunkle
daa16bcf42 cli: Support opening anonymous file descriptors via the cli on MacOS and Linux (#26744)
Closes #4770

(really closes issue described in [this
comment](https://github.com/zed-industries/zed/issues/4770#issuecomment-2258728884)
on #4770)

Only implemented for MacOS and Linux for now as I have no way to test on
Windows or BSD.
PRs welcome!

Release Notes:

- Added support for reading from anonymous file descriptors (e.g.
created as part of process substitution) on MacOS and Linux
2025-03-13 20:53:47 -05:00
renovate[bot]
22ad7b17c5 Update Rust crate clap to v4.5.32 (#26592)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [clap](https://redirect.github.com/clap-rs/clap) |
workspace.dependencies | patch | `4.5.31` -> `4.5.32` |

---

### Release Notes

<details>
<summary>clap-rs/clap (clap)</summary>

###
[`v4.5.32`](https://redirect.github.com/clap-rs/clap/blob/HEAD/CHANGELOG.md#4532---2025-03-10)

[Compare
Source](https://redirect.github.com/clap-rs/clap/compare/v4.5.31...v4.5.32)

##### Features

-   Add `Error::remove`

##### Documentation

-   *(cookbook)* Switch from `humantime` to `jiff`
-   *(tutorial)* Better cover required vs optional

##### Internal

-   Update `pulldown-cmark`

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone
America/New_York, Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4xOTQuMSIsInVwZGF0ZWRJblZlciI6IjM5LjIwMC4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-13 23:36:04 +00:00
renovate[bot]
728a5eb388 Update Rust crate ctor to v0.4.1 (#26593)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [ctor](https://redirect.github.com/mmastrac/rust-ctor) |
workspace.dependencies | patch | `0.4.0` -> `0.4.1` |

---

### Configuration

📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone
America/New_York, Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4xOTQuMSIsInVwZGF0ZWRJblZlciI6IjM5LjIwMC4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-14 01:17:43 +02:00
renovate[bot]
8d8e5d3635 Update Rust crate mdbook to v0.4.47 (#26611)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [mdbook](https://redirect.github.com/rust-lang/mdBook) | dependencies
| patch | `0.4.45` -> `0.4.47` |

---

### Release Notes

<details>
<summary>rust-lang/mdBook (mdbook)</summary>

###
[`v0.4.47`](https://redirect.github.com/rust-lang/mdBook/blob/HEAD/CHANGELOG.md#mdBook-0447)

[Compare
Source](https://redirect.github.com/rust-lang/mdBook/compare/v0.4.46...v0.4.47)


[v0.4.46...v0.4.47](https://redirect.github.com/rust-lang/mdBook/compare/v0.4.46...v0.4.47)

##### Fixed

-   Fixed search not showing up in sub-directories.
[#&#8203;2586](https://redirect.github.com/rust-lang/mdBook/pull/2586)

###
[`v0.4.46`](https://redirect.github.com/rust-lang/mdBook/blob/HEAD/CHANGELOG.md#mdBook-0446)

[Compare
Source](https://redirect.github.com/rust-lang/mdBook/compare/v0.4.45...v0.4.46)


[v0.4.45...v0.4.46](https://redirect.github.com/rust-lang/mdBook/compare/v0.4.45...v0.4.46)

##### Changed

- The `output.html.hash-files` config option has been added to add
hashes to static filenames to bust any caches when a book is updated.
`{{resource}}` template tags have been added so that links can be
properly generated to those files.
[#&#8203;1368](https://redirect.github.com/rust-lang/mdBook/pull/1368)

##### Fixed

-   Playground links for Rust 2024 now set the edition correctly.
[#&#8203;2557](https://redirect.github.com/rust-lang/mdBook/pull/2557)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone
America/New_York, Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4xOTQuMSIsInVwZGF0ZWRJblZlciI6IjM5LjIwMC4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-14 01:17:26 +02:00
renovate[bot]
a05a480ed9 Update Rust crate rsa to v0.9.8 (#26619)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [rsa](https://redirect.github.com/RustCrypto/RSA) |
workspace.dependencies | patch | `0.9.7` -> `0.9.8` |

---

### Release Notes

<details>
<summary>RustCrypto/RSA (rsa)</summary>

###
[`v0.9.8`](https://redirect.github.com/RustCrypto/RSA/blob/HEAD/CHANGELOG.md#098-2025-03-12)

[Compare
Source](https://redirect.github.com/RustCrypto/RSA/compare/v0.9.7...v0.9.8)

##### Added

-   Doc comments to specify the `rand` version ([#&#8203;473])

[#&#8203;473]: https://redirect.github.com/RustCrypto/RSA/pull/473

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone
America/New_York, Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4xOTQuMSIsInVwZGF0ZWRJblZlciI6IjM5LjIwMC4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-14 01:17:14 +02:00
renovate[bot]
d141fa027e Update Rust crate schemars to v0.8.22 (#26626)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [schemars](https://graham.cool/schemars/)
([source](https://redirect.github.com/GREsau/schemars)) |
workspace.dependencies | patch | `0.8.21` -> `0.8.22` |

---

### Release Notes

<details>
<summary>GREsau/schemars (schemars)</summary>

###
[`v0.8.22`](https://redirect.github.com/GREsau/schemars/blob/HEAD/CHANGELOG.md#0822---2025-02-25)

[Compare
Source](https://redirect.github.com/GREsau/schemars/compare/v0.8.21...v0.8.22)

##### Fixed:

- Fix compatibility with rust 2024 edition
([https://github.com/GREsau/schemars/pull/378](https://redirect.github.com/GREsau/schemars/pull/378))

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone
America/New_York, Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4xOTQuMSIsInVwZGF0ZWRJblZlciI6IjM5LjIwMC4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-14 01:16:57 +02:00
Michael Sloan
8e0e291bd5 Track cumulative token usage in assistant2 when using anthropic API (#26738)
Release Notes:

- N/A
2025-03-13 22:56:16 +00:00
Conrad Irwin
e3c0f56a96 New excerpt controls (#24428)
Release Notes:

- Multibuffers now use less vertical space for excerpt boundaries.
Additionally the expand up/down arrows are hidden at the start and end
of the buffers

---------

Co-authored-by: Nate Butler <iamnbutler@gmail.com>
Co-authored-by: Zed AI <claude-3.5-sonnet@zed.dev>
2025-03-13 15:52:47 -06:00
Conrad Irwin
3935e8343a Allow parsing commits when we can't resolve the permalink (#26709)
Closes #26577

Release Notes:

- git: Fix showing commit messages for all repos
2025-03-13 15:41:08 -06:00
renovate[bot]
0c84170071 Update Rust crate quote to v1.0.40 (#26618)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [quote](https://redirect.github.com/dtolnay/quote) |
workspace.dependencies | patch | `1.0.38` -> `1.0.40` |

---

### Release Notes

<details>
<summary>dtolnay/quote (quote)</summary>

###
[`v1.0.40`](https://redirect.github.com/dtolnay/quote/releases/tag/1.0.40)

[Compare
Source](https://redirect.github.com/dtolnay/quote/compare/1.0.39...1.0.40)

- Optimize construction of lifetime tokens
([#&#8203;293](https://redirect.github.com/dtolnay/quote/issues/293),
thanks [@&#8203;aatifsyed](https://redirect.github.com/aatifsyed))

###
[`v1.0.39`](https://redirect.github.com/dtolnay/quote/releases/tag/1.0.39)

[Compare
Source](https://redirect.github.com/dtolnay/quote/compare/1.0.38...1.0.39)

-   Documentation improvements

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone
America/New_York, Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4xOTQuMSIsInVwZGF0ZWRJblZlciI6IjM5LjE5NC4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-13 23:14:13 +02:00
renovate[bot]
a38687d278 Update Rust crate libc to v0.2.171 (#26604)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [libc](https://redirect.github.com/rust-lang/libc) |
workspace.dependencies | patch | `0.2.170` -> `0.2.171` |

---

### Release Notes

<details>
<summary>rust-lang/libc (libc)</summary>

###
[`v0.2.171`](https://redirect.github.com/rust-lang/libc/releases/tag/0.2.171)

[Compare
Source](https://redirect.github.com/rust-lang/libc/compare/0.2.170...0.2.171)

##### Added

- Android: Add `if_nameindex`/`if_freenameindex` support
([#&#8203;4247](https://redirect.github.com/rust-lang/libc/pull/4247))
- Apple: Add missing proc types and constants
([#&#8203;4310](https://redirect.github.com/rust-lang/libc/pull/4310))
- BSD: Add `devname`
([#&#8203;4285](https://redirect.github.com/rust-lang/libc/pull/4285))
- Cygwin: Add PTY and group API
([#&#8203;4309](https://redirect.github.com/rust-lang/libc/pull/4309))
- Cygwin: Add support
([#&#8203;4279](https://redirect.github.com/rust-lang/libc/pull/4279))
- FreeBSD: Make `spawn.h` interfaces available on all FreeBSD-like
systems
([#&#8203;4294](https://redirect.github.com/rust-lang/libc/pull/4294))
- Linux: Add `AF_XDP` structs for all Linux environments
([#&#8203;4163](https://redirect.github.com/rust-lang/libc/pull/4163))
- Linux: Add SysV semaphore constants
([#&#8203;4286](https://redirect.github.com/rust-lang/libc/pull/4286))
- Linux: Add `F_SEAL_EXEC`
([#&#8203;4316](https://redirect.github.com/rust-lang/libc/pull/4316))
- Linux: Add `SO_PREFER_BUSY_POLL` and `SO_BUSY_POLL_BUDGET`
([#&#8203;3917](https://redirect.github.com/rust-lang/libc/pull/3917))
- Linux: Add `devmem` structs
([#&#8203;4299](https://redirect.github.com/rust-lang/libc/pull/4299))
- Linux: Add socket constants up to `SO_DEVMEM_DONTNEED`
([#&#8203;4299](https://redirect.github.com/rust-lang/libc/pull/4299))
- NetBSD, OpenBSD, DragonflyBSD: Add `closefrom`
([#&#8203;4290](https://redirect.github.com/rust-lang/libc/pull/4290))
- NuttX: Add `pw_passwd` field to `passwd`
([#&#8203;4222](https://redirect.github.com/rust-lang/libc/pull/4222))
- Solarish: define `IP_BOUND_IF` and `IPV6_BOUND_IF`
([#&#8203;4287](https://redirect.github.com/rust-lang/libc/pull/4287))
- Wali: Add bindings for `wasm32-wali-linux-musl` target
([#&#8203;4244](https://redirect.github.com/rust-lang/libc/pull/4244))

##### Changed

- AIX: Use `sa_sigaction` instead of a union
([#&#8203;4250](https://redirect.github.com/rust-lang/libc/pull/4250))
- Make `msqid_ds.__msg_cbytes` public
([#&#8203;4301](https://redirect.github.com/rust-lang/libc/pull/4301))
- Unix: Make all `major`, `minor`, `makedev` into `const fn`
([#&#8203;4208](https://redirect.github.com/rust-lang/libc/pull/4208))

##### Deprecated

- Linux: Deprecate obsolete packet filter interfaces
([#&#8203;4267](https://redirect.github.com/rust-lang/libc/pull/4267))

##### Fixed

- Cygwin: Fix strerror_r
([#&#8203;4308](https://redirect.github.com/rust-lang/libc/pull/4308))
- Cygwin: Fix usage of f!
([#&#8203;4308](https://redirect.github.com/rust-lang/libc/pull/4308))
- Hermit: Make `stat::st_size` signed
([#&#8203;4298](https://redirect.github.com/rust-lang/libc/pull/4298))
- Linux: Correct values for `SI_TIMER`, `SI_MESGQ`, `SI_ASYNCIO`
([#&#8203;4292](https://redirect.github.com/rust-lang/libc/pull/4292))
- NuttX: Update `tm_zone` and `d_name` fields to use `c_char` type
([#&#8203;4222](https://redirect.github.com/rust-lang/libc/pull/4222))
- Xous: Include the prelude to define `c_int`
([#&#8203;4304](https://redirect.github.com/rust-lang/libc/pull/4304))

##### Other

- Add labels to FIXMEs
([#&#8203;4231](https://redirect.github.com/rust-lang/libc/pull/4231),
[#&#8203;4232](https://redirect.github.com/rust-lang/libc/pull/4232),
[#&#8203;4234](https://redirect.github.com/rust-lang/libc/pull/4234),
[#&#8203;4235](https://redirect.github.com/rust-lang/libc/pull/4235),
[#&#8203;4236](https://redirect.github.com/rust-lang/libc/pull/4236))
- CI: Fix "cannot find libc" error on Sparc64
([#&#8203;4317](https://redirect.github.com/rust-lang/libc/pull/4317))
- CI: Fix "cannot find libc" error on s390x
([#&#8203;4317](https://redirect.github.com/rust-lang/libc/pull/4317))
- CI: Pass `--no-self-update` to `rustup update`
([#&#8203;4306](https://redirect.github.com/rust-lang/libc/pull/4306))
- CI: Remove tests for the `i586-pc-windows-msvc` target
([#&#8203;4311](https://redirect.github.com/rust-lang/libc/pull/4311))
- CI: Remove the `check_cfg` job
([#&#8203;4322](https://redirect.github.com/rust-lang/libc/pull/4312))
- Change the range syntax that is giving `ctest` problems
([#&#8203;4311](https://redirect.github.com/rust-lang/libc/pull/4311))
- Linux: Split out the stat struct for gnu/b32/mips
([#&#8203;4276](https://redirect.github.com/rust-lang/libc/pull/4276))

##### Removed

- NuttX: Remove `pthread_set_name_np`
([#&#8203;4251](https://redirect.github.com/rust-lang/libc/pull/4251))

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone
America/New_York, Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4xOTQuMSIsInVwZGF0ZWRJblZlciI6IjM5LjE5NC4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-13 23:13:58 +02:00
renovate[bot]
b75b308459 Update Rust crate env_logger to v0.11.7 (#26603)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [env_logger](https://redirect.github.com/rust-cli/env_logger) |
workspace.dependencies | patch | `0.11.6` -> `0.11.7` |

---

### Release Notes

<details>
<summary>rust-cli/env_logger (env_logger)</summary>

###
[`v0.11.7`](https://redirect.github.com/rust-cli/env_logger/blob/HEAD/CHANGELOG.md#0117---2025-03-10)

[Compare
Source](https://redirect.github.com/rust-cli/env_logger/compare/v0.11.6...v0.11.7)

##### Internal

-   Replaced `humantime` with `jiff`

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone
America/New_York, Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4xOTQuMSIsInVwZGF0ZWRJblZlciI6IjM5LjE5NC4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-13 23:13:43 +02:00
Cole Miller
dffa725c7d worktree: Disable flaky test_file_status test (#26729)
See also:
- https://github.com/zed-industries/zed/pull/26684
- https://github.com/zed-industries/zed/pull/26710

Release Notes:

- N/A
2025-03-13 21:09:16 +00:00
Marshall Bowers
22f1429f97 assistant2: Prevent sending messages when the button is disabled (#26722)
This PR updates the `Chat` action handler to prevent sending messages in
the states when the submit button is disabled.

Release Notes:

- N/A
2025-03-13 20:49:21 +00:00
KyleBarton
6bdd2cf7db Consider the colon to be a word character when inside a string in JSON (#26574)
Partially addresses #25698

Part of why autocomplete suggestions for `keymap.json` aren't great is
because `:` is (correctly) considered a punctuation character, rather
than a word character, in JSON. But since `::` is part of the name of
zed commands, it means that the autocomplete context window loses
context after the user types colon:

Suggestion here is to use overrides for JSON and JSONC such that colon
is considered a word character when it's inside a string. This improves
the experience:

I believe this is more broadly correct anyway, since `:` loses it's
punctuation meaning when inside a string.

Hope this is helpful!

Release Notes:

- Improved autocomplete for keymap.json by treating `::` like word characters when inside a string.
2025-03-13 16:21:34 -04:00
Cole Miller
a7f3b22051 Don't render "Initialize Repository" button when no worktrees (#26713)
Closes #26676  

Release Notes:

- Fixed the git panel to not show an "Initialize Repositories" button in
empty projects
2025-03-13 16:17:23 -04:00
Cole Miller
f3703fa8be Use system git for committing (#26705)
Closes #26472

Release Notes:

- On macOS, switched to using the system's git binary to create commits.
This fixes issues that some users were seeing with pre-commit hooks.
Compatibility note: after this change, it is no longer possible to
commit from Zed unless git is installed.
2025-03-13 16:14:28 -04:00
Marshall Bowers
a0be6c8cb2 assistant2: Consider tool use as part of the "streaming" state (#26723)
This PR updates the `Thread::is_streaming` method so that it includes
tool use in the "streaming" state.

This will prevent the streaming indicator from disappearing when we're
doing tool use.

Release Notes:

- N/A
2025-03-13 20:11:44 +00:00
Cole Miller
b5a7fb13c3 Remove github issue template for git beta and improve related CI (#26707)
Remove the git beta issue template.
Improve ci.yml `job_spec` so that changes like this will not require CI in the future.
Improve ci.yml `job_spec` ensuring `output.run_license` exported for Cargo.lock.

Release Notes:

- N/A

---------

Co-authored-by: Peter Tripp <peter@zed.dev>
2025-03-13 16:07:32 -04:00
Danilo Leal
2183fc674d assistant2: Truncate context pill labels (#26721)
To solve a problem that mostly happens if the pill is of kind `Thread`
and the corresponding thread has a super long title.

<img
src="https://github.com/user-attachments/assets/4ee8038d-9467-41a9-9b30-76019d0b9c0b"
width="500px"/>

Release Notes:

- N/A
2025-03-13 16:56:49 -03:00
renovate[bot]
0ad5979f19 Update Rust crate proc-macro2 to v1.0.94 (#26612)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [proc-macro2](https://redirect.github.com/dtolnay/proc-macro2) |
workspace.dependencies | patch | `1.0.93` -> `1.0.94` |

---

### Release Notes

<details>
<summary>dtolnay/proc-macro2 (proc-macro2)</summary>

###
[`v1.0.94`](https://redirect.github.com/dtolnay/proc-macro2/releases/tag/1.0.94)

[Compare
Source](https://redirect.github.com/dtolnay/proc-macro2/compare/1.0.93...1.0.94)

-   Documentation improvements

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone
America/New_York, Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4xOTQuMSIsInVwZGF0ZWRJblZlciI6IjM5LjE5NC4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-13 15:38:02 -04:00
Peter Tripp
ed1938dd9a worktree: Disable flaky tests (test_write_file, test_git_status_postprocessing) (#26710)
Comment out flaky tests:
- `worktree_tests::test_write_file`
- `worktree_tests::test_git_status_postprocessing`

Job links:
- windows fail:
https://github.com/zed-industries/zed/actions/runs/13841766606/job/38730766252
- macos fail:
https://github.com/zed-industries/zed/actions/runs/13841766606/job/38730764118

That
[commit](85384fb9c6)
was a non-op script change, but in the [prior
commit](00359271d1)
[windows/macos
pass](https://github.com/zed-industries/zed/actions/runs/13841135221).

Similar experience with `worktree_tests::test_write_file` on both macOS
windows too.

- See also: https://github.com/zed-industries/zed/pull/26684

Release Notes:

- N/A
2025-03-13 15:16:30 -04:00
Peter Tripp
f7927d3fa4 ci: Fix 'Run Tests' not always running (#26685)
Follow up to:
- https://github.com/zed-industries/zed/pull/26551

We need the "Tests Pass" step to run `if: always()`. 
Turns out when it's 'skipped', it counts as 'passing' with respect to
required status checks.


Release Notes:

- N/A
2025-03-13 19:02:59 +00:00
Conrad Irwin
8361c32a34 Fix flicker when reverting last hunk from the project diff view (#26706)
Closes #26696

Closes #ISSUE

Release Notes:

- git: Fix flicker when reverting last hunk in project diff view
2025-03-13 18:49:18 +00:00
Agus Zubiaga
2edadd9352 bash tool: Rename working_directory to cd and improve command wrap (#26702)
This helps its do the right thing

Release Notes:

- N/A

Co-authored-by: Antonio Scandurra <me@as-cii.com>
2025-03-13 18:29:25 +00:00
Joseph T. Lyons
85384fb9c6 Update issue response script to only consider replies from staff (#26703)
Release Notes:

- N/A

Co-authored-by: Ben Kunkle <ben.kunkle@gmail.com>
2025-03-13 14:15:38 -04:00
João Marcos
00359271d1 git: Fix race condition when [un]staging hunks in quick succession (#26422)
- [x] Fix `[un]stage` hunk operations cancelling pending ones
  - [x] Add test
- [ ] bugs I stumbled upon (try to repro again before merging)
  - [x] holding `git::StageAndNext` skips hunks randomly 
    - [x] Add test
  - [x] restoring a file keeps it in the git panel
- [x] Double clicking on `toggle staged` fast makes Zed disagree with
`git` CLI
- [x] checkbox shows ✔️ (fully staged) after a single
stage

Release Notes:

- N/A

---------

Co-authored-by: Cole <cole@zed.dev>
Co-authored-by: Max <max@zed.dev>
2025-03-13 10:41:04 -07:00
Ben Kunkle
18fcdf1d2c terminal: Fix issues with highlighted ranges of paths (#26695)
Fixes a few problems,

- Uses `Boundary::Grid` instead of `Boundary::Cursor` for highlighted
range adjustments.

This fixes quite a few wierd behaviors around highlighting paths that
had to be scrolled into view (i.e. were in the terminal history)
including the issue described in the release notes as well as a
regression caused by #26401 where the highlight range would span from
the start of the path to the cursor location in the shell prompt

- Strips all trailing `:`s from the paths, updating the highlighted
range accordingly.

This worked fine before and is just a visual improvement.


Release Notes:

- Fixed an issue where file paths in the terminal surrounded by `()` or
`[]` would not be highlighted properly
2025-03-13 12:25:20 -05:00
renovate[bot]
55c927b039 Update Rust crate async-trait to v0.1.87 (#26578)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [async-trait](https://redirect.github.com/dtolnay/async-trait) |
workspace.dependencies | patch | `0.1.86` -> `0.1.87` |

---

### Release Notes

<details>
<summary>dtolnay/async-trait (async-trait)</summary>

###
[`v0.1.87`](https://redirect.github.com/dtolnay/async-trait/releases/tag/0.1.87)

[Compare
Source](https://redirect.github.com/dtolnay/async-trait/compare/0.1.86...0.1.87)

-   Documentation improvements

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone
America/New_York, Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4xOTQuMSIsInVwZGF0ZWRJblZlciI6IjM5LjE5NC4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Marshall Bowers <git@maxdeviant.com>
2025-03-13 17:07:01 +00:00
Marshall Bowers
1be3f81920 assistant2: Include the thread summary in the Markdown representation (#26693)
This PR adds the thread's summary (if it has one) as a heading in the
Markdown representation.

Release Notes:

- N/A
2025-03-13 16:59:35 +00:00
Cole Miller
2eb4d6b7eb Fix being unable to put a cursor after trailing deletion hunks (#26621)
Closes #26541

Release Notes:

- Fixed a bug that prevented putting the cursor after a deletion hunk at
the end of a file, in the absence of trailing newlines

---------

Co-authored-by: Max <max@zed.dev>
2025-03-13 16:56:54 +00:00
Ben Kunkle
25f407baab settings: Auto-update JSON schemas for settings when extensions are un/installed (#26633)
Because of #26562, it is now possible to subscribe to extension update
events within the LSP store, where we can then update the Schemas sent
to the JSON LSP resulting in dynamic updates to the auto-complete
suggestions and diagnostics in settings. Notably, this means newly
installed languages and (icon) themes will auto-complete correctly as
soon as the extension is installed.

Closes #15436

Release Notes:

- Fixed an issue where autocomplete suggestions and diagnostics for
languages and (icon) themes in settings would not update when the
extension with which they were added was installed or uninstalled
2025-03-13 16:50:07 +00:00
Marshall Bowers
79874872cb assistant2: Add ability to open the active thread as Markdown (#26690)
This PR adds a new `assistant2: open active thread as markdown` action
that opens up the active thread in a Markdown representation:

<img width="1394" alt="Screenshot 2025-03-13 at 12 25 33 PM"
src="https://github.com/user-attachments/assets/363baaaa-c74b-4e93-af36-a3e04a114af0"
/>

Release Notes:

- N/A
2025-03-13 12:39:01 -04:00
Marshall Bowers
95208a6576 worktree: Disable flaky test_git_repository_status test (#26684)
This PR disables the flaky `test_git_repository_status` test.

Release Notes:

- N/A
2025-03-13 12:06:44 -04:00
Danilo Leal
1034d1a6b5 docs: Add section about Edit Prediction modes (#26683)
To go along the upcoming blog post as well as the new menu item options
(https://github.com/zed-industries/zed/pull/26680).

Release Notes:

- N/A
2025-03-13 12:48:39 -03:00
Danilo Leal
d4eab557b2 edit prediction: Add eager and subtle modes toggle to menu (#26680)
Now, users can toggle the display modes for Edit Prediction via the UI.

<img
src="https://github.com/user-attachments/assets/974cd3cc-43b4-46ba-9ce5-b2345ef3323d"
width="600px"/>

Release Notes:

- N/A
2025-03-13 12:46:22 -03:00
Nate Butler
b75964a636 Revert "ui: Color cleanup (#26673)" (#26681)
This reverts commit 6767e98e00.

Somehow that PR automerged itself even with failed CI checks.

Release Notes:

- N/A
2025-03-13 15:40:57 +00:00
Peter Tripp
87cdb68cca ci: Use smaller windows runners (#26674)
Let's see if the speed of `windows-2025-32` for `windows_tests` is
fast-enough for PRs and everywhere else use `windows-2025-16`. Leaving
`windows_clippy` unchanged with `windows-2025-16`.

Release Notes:

- N/A
2025-03-13 15:39:12 +00:00
Kirill Bulatov
b0b65420f6 Do not repeat proposed LSP completions in the word completions (#26682)
Follow-up of https://github.com/zed-industries/zed/pull/26410

Release Notes:

- N/A
2025-03-13 15:37:46 +00:00
Agus Zubiaga
8ec0309645 assistant edit tool: Use buffer search and replace in background (#26679)
Instead of getting the whole text from the buffer, replacing with
`String::replace`, and getting a whole diff, we'll now use `SearchQuery`
to get a range, diff only that range, and apply it (all in the
background).

When we match zero strings, we'll record a "bad search", keep going and
report it to the model at the end.

Release Notes:

- N/A

---------

Co-authored-by: Max <max@zed.dev>
2025-03-13 12:25:49 -03:00
Nate Butler
6767e98e00 ui: Color cleanup (#26673)
This PR cleans up some color & elevation misc.

### Don't allow deriving Color from Hsla

The point of the [ui::Color] enum is to encourage consistent color
usage, and the the Color::Custom case is really only meant for cases
where we have no other choice.

`impl From<Hsla> for Color` encourages blindly passing colors into
`Color::Custom` – with this in place we might as well remove the entire
`Color` enum.

The usages that were updated due to this removal were for colors that
already exist in the Color enum, making it even more clear that it
didn't make sense to have this.

### `ElevationIndex` -> `Elevation`

This name would make more sense if we had an `Elevation` in the first
place. The new name is more clear.

#### `Button::elevation`

As part of this change I also updated button's `layer` method to
`elevation`, since it takes an elevation. This method still has the
following issue:

You want to use `Button::elevation` when it's default colors are
invisible on the layer you are rendering the button on. However, current
this method uses the elevation's `bg` color, rather than it's
`on_elevation_bg`.

Ideally when you use `Button::elevation` you want to pass the elevation
you are _on_, not choosing one that will show up the elevation you are
on.

This change will be in a separate PR, as it likely will have widespread
visual impact across the app.

Release Notes:

- N/A
2025-03-13 15:18:40 +00:00
Antonio Scandurra
8cf5af1a84 Introduce DiagnosticsTool (#26670)
Release Notes:

- N/A
2025-03-13 14:53:00 +01:00
Albin Kocheril Chacko
247ee880d2 Fix typo in default.json (#26666)
minor typo fix

Release Notes:

- N/A
2025-03-13 13:39:28 +00:00
Nate Butler
2e217759c0 gruvbox: version_control_ -> version_control. (#26665)
Missed this in PR #26606 

Before:

![CleanShot 2025-03-13 at 08 58
59@2x](https://github.com/user-attachments/assets/021df4b1-5a70-4fae-a109-9b8bb35949e3)

After:

![CleanShot 2025-03-13 at 08 59
22@2x](https://github.com/user-attachments/assets/01dca26d-77ec-4a54-8b7c-aa2fb160ff7d)

Release Notes:

- theme: Fixed an issue where version control colors weren't applying
correctly. (again)
2025-03-13 13:13:35 +00:00
Danilo Leal
0a0c163692 assistant2: Use icons for tool call status communication (#26617)
It was hard to catch the running & pending states, though. When running,
it will appear as a spinning arrow circle icon.

<img
src="https://github.com/user-attachments/assets/dbf1bc0a-6fa3-41c6-bcd7-2226e89c87b4"
width="500px" />

Release Notes:

- N/A
2025-03-13 10:01:20 -03:00
Antonio Scandurra
e80df25386 Iterate on tools some more (#26663)
Release Notes:

- N/A
2025-03-13 12:42:02 +00:00
Danilo Leal
d9590f3f0e docs: Improve introduction to Edit Prediction (#26620)
As I was writing a blog post about Edit Prediction, I realized we didn't
have a great section in the docs I could link to talking about
configuring it. We weren't: 1) explicitly exposing the settings code to
add Zed as the edit prediction provider, and 2) not showing an image of
the title bar banner.

Release Notes:

- N/A
2025-03-13 09:03:49 -03:00
Antonio Scandurra
4ecd1b5174 Fix bad cd sometimes used by BashTool and set edit model temperature to 0 (#26656)
Release Notes:

- N/A
2025-03-13 10:47:00 +00:00
Antonio Scandurra
70c973f6c3 Fix issues in EditFilesTool, ListDirectoryTool and BashTool (#26647)
Release Notes:

- N/A
2025-03-13 09:41:27 +00:00
Stanislav Alekseev
e842b4eade macOS: Disable automatic window tabbing in fullscreen mode (#26600)
Fixes #26534 (this time for real)

Release Notes:

- Fixed issue where Zed would behave weirdly when opening new fullscreen
windows by disabling window tabbing

Apple docs:
https://developer.apple.com/documentation/appkit/nswindow/allowsautomaticwindowtabbing
2025-03-13 12:45:01 +05:30
Agus Zubiaga
606aa7a78c Edit tool debugging (#26637)
Adds an `debug: edit tool` action that opens a new view which will help
us debug the edit tool internals. As the edit tool runs, the log
displays:

- Instructions provided by the main model
- Response stream from the editor model
- Parsed edit blocks
- Tool output provided back to main model

The log automatically records all edit tool interactions for staff, so
if you notice something weird, you can debug it retroactively without
having to open the debug tool first. We may want to limit the number of
recorded requests later.

I have a few more ideas for it, but this seems like a good starting
point.


https://github.com/user-attachments/assets/c61f5ce8-08b1-4500-accb-db2a480eb3ab


Release Notes:

- N/A
2025-03-13 04:03:01 +00:00
125 changed files with 4421 additions and 1998 deletions

View File

@@ -1,51 +0,0 @@
name: Git Beta
description: There is a bug related to new Git features in Zed
type: "Bug"
labels: [git]
title: "Git Beta: <a short description of the Git bug>"
body:
- type: textarea
attributes:
label: Summary
description: Describe the bug with a one line summary, and provide detailed reproduction steps
value: |
<!-- Please insert a one line summary of the issue below -->
<!-- Include all steps necessary to reproduce from a clean Zed installation. Be verbose -->
Steps to trigger the problem:
1.
2.
3.
Actual Behavior:
Expected Behavior:
validations:
required: true
- type: textarea
id: environment
attributes:
label: Zed Version and System Specs
description: 'Open Zed, and in the command palette select "zed: Copy System Specs Into Clipboard"'
placeholder: |
Output of "zed: Copy System Specs Into Clipboard"
validations:
required: true
- type: textarea
attributes:
label: If applicable, attach your `~/Library/Logs/Zed/Zed.log` file to this issue.
description: |
macOS: `~/Library/Logs/Zed/Zed.log`
Linux: `~/.local/share/zed/logs/Zed.log` or $XDG_DATA_HOME
If you only need the most recent lines, you can run the `zed: open log` command palette action to see the last 1000.
value: |
<details><summary>Zed.log</summary>
<!-- Click below this line and paste or drag-and-drop your log-->
```
```
<!-- Click above this line and paste or drag-and-drop your log--></details>
validations:
required: false

View File

@@ -28,6 +28,7 @@ jobs:
if: github.repository_owner == 'zed-industries'
outputs:
run_tests: ${{ steps.filter.outputs.run_tests }}
run_license: ${{ steps.filter.outputs.run_license }}
runs-on:
- ubuntu-latest
steps:
@@ -47,7 +48,12 @@ jobs:
git fetch origin "$GITHUB_BASE_REF" --depth=350
COMPARE_REV=$(git merge-base "origin/${GITHUB_BASE_REF}" HEAD)
fi
if [[ $(git diff --name-only $COMPARE_REV ${{ github.sha }} | grep -v "^docs/") ]]; then
# Specify anything which should skip full CI in this regex:
# - docs/
# - .github/ISSUE_TEMPLATE/
# - .github/workflows/ (except .github/workflows/ci.yml)
SKIP_REGEX='^(docs/|\.github/(ISSUE_TEMPLATE|workflows/(?!ci)))'
if [[ $(git diff --name-only $COMPARE_REV ${{ github.sha }} | grep -vP "$SKIP_REGEX") ]]; then
echo "run_tests=true" >> $GITHUB_OUTPUT
else
echo "run_tests=false" >> $GITHUB_OUTPUT
@@ -366,7 +372,8 @@ jobs:
if: |
github.repository_owner == 'zed-industries' &&
needs.job_spec.outputs.run_tests == 'true'
runs-on: windows-2025-64
# Use bigger runners for PRs (speed); smaller for async (cost)
runs-on: ${{ github.event_name == 'pull_request' && 'windows-2025-32' || 'windows-2025-16' }}
steps:
# more info here:- https://github.com/rust-lang/cargo/issues/13020
- name: Enable longer pathnames for git
@@ -431,22 +438,28 @@ jobs:
- macos_tests
- windows_clippy
- windows_tests
if: |
always() && (
needs.style.result == 'success'
&& (
needs.job_spec.outputs.run_tests == 'false'
|| (needs.macos_tests.result == 'success'
&& needs.linux_tests.result == 'success'
&& needs.windows_tests.result == 'success'
&& needs.windows_clippy.result == 'success'
&& needs.build_remote_server.result == 'success'
&& needs.migration_checks.result == 'success')
)
)
if: always()
steps:
- name: All tests passed
run: echo "All tests passed successfully!"
- name: Check all tests passed
run: |
# Check dependent jobs...
RET_CODE=0
# Always check style
[[ "${{ needs.style.result }}" != 'success' ]] && { RET_CODE=1; echo "style tests failed"; }
# Only check test jobs if they were supposed to run
if [[ "${{ needs.job_spec.outputs.run_tests }}" == "true" ]]; then
[[ "${{ needs.macos_tests.result }}" != 'success' ]] && { RET_CODE=1; echo "macOS tests failed"; }
[[ "${{ needs.linux_tests.result }}" != 'success' ]] && { RET_CODE=1; echo "Linux tests failed"; }
[[ "${{ needs.windows_tests.result }}" != 'success' ]] && { RET_CODE=1; echo "Windows tests failed"; }
[[ "${{ needs.windows_clippy.result }}" != 'success' ]] && { RET_CODE=1; echo "Windows clippy failed"; }
[[ "${{ needs.migration_checks.result }}" != 'success' ]] && { RET_CODE=1; echo "Migration checks failed"; }
[[ "${{ needs.build_remote_server.result }}" != 'success' ]] && { RET_CODE=1; echo "Remote server build failed"; }
fi
if [[ "$RET_CODE" -eq 0 ]]; then
echo "All tests passed successfully!"
fi
exit $RET_CODE
bundle-mac:
timeout-minutes: 120

451
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,4 @@
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.5 8.5L7.5 11.5M7.5 11.5L4.5 8.5M7.5 11.5L7.5 5.5" stroke="black" stroke-linecap="square"/>
<path d="M5 3.5L10 3.5" stroke="black"/>
</svg>

After

Width:  |  Height:  |  Size: 248 B

View File

@@ -0,0 +1,4 @@
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.5 6.5L7.5 3.5M7.5 3.5L10.5 6.5M7.5 3.5V9.5" stroke="black" stroke-linecap="square"/>
<path d="M5 11.5H10" stroke="black"/>
</svg>

After

Width:  |  Height:  |  Size: 238 B

View File

@@ -1,6 +1,6 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 4H8" stroke="black" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6 10L11 10" stroke="black" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="4" cy="10" r="1.875" stroke="black" stroke-width="1.75"/>
<circle cx="10" cy="4" r="1.875" stroke="black" stroke-width="1.75"/>
<path d="M3 4H8" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6 10L11 10" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="4" cy="10" r="1.875" stroke="black" stroke-width="1.5"/>
<circle cx="10" cy="4" r="1.875" stroke="black" stroke-width="1.5"/>
</svg>

Before

Width:  |  Height:  |  Size: 450 B

After

Width:  |  Height:  |  Size: 446 B

View File

@@ -362,6 +362,7 @@
"ctrl-k ctrl-0": "editor::FoldAll",
"ctrl-k ctrl-j": "editor::UnfoldAll",
"ctrl-space": "editor::ShowCompletions",
"ctrl-shift-space": "editor::ShowWordCompletions",
"ctrl-.": "editor::ToggleCodeActions",
"ctrl-k r": "editor::RevealInFileManager",
"ctrl-k p": "editor::CopyPath",

View File

@@ -466,6 +466,7 @@
// Using `ctrl-space` in Zed requires disabling the macOS global shortcut.
// System Preferences->Keyboard->Keyboard Shortcuts->Input Sources->Select the previous input source (uncheck)
"ctrl-space": "editor::ShowCompletions",
"ctrl-shift-space": "editor::ShowWordCompletions",
"cmd-.": "editor::ToggleCodeActions",
"cmd-k r": "editor::RevealInFileManager",
"cmd-k p": "editor::CopyPath",

View File

@@ -155,6 +155,7 @@
"z +": ["workspace::SendKeystrokes", "shift-l j z t ^"],
"z t": "editor::ScrollCursorTop",
"z z": "editor::ScrollCursorCenter",
"z l": "vim::ScrollLeftHalfWay",
"z .": ["workspace::SendKeystrokes", "z z ^"],
"z b": "editor::ScrollCursorBottom",
"z a": "editor::ToggleFold",

View File

@@ -11,8 +11,8 @@ You should only perform actions that modify the users system if explicitly re
Be concise and direct in your responses.
The user has opened a project that contains the following top-level directories/files:
The user has opened a project that contains the following root directories/files:
{{#each worktree_root_names}}
- {{this}}
{{#each worktrees}}
- {{root_name}} (absolute path: {{abs_path}})
{{/each}}

View File

@@ -547,7 +547,7 @@
"git_panel": {
// Whether to show the git panel button in the status bar.
"button": true,
// Where to the git panel. Can be 'left' or 'right'.
// Where to show the git panel. Can be 'left' or 'right'.
"dock": "left",
// Default width of the git panel.
"default_width": 360,
@@ -1092,11 +1092,12 @@
//
// May take 3 values:
// 1. "enabled"
// Always fetch document's words for completions.
// Always fetch document's words for completions along with LSP completions.
// 2. "fallback"
// Only if LSP response errors/times out/is empty, use document's words to show completions.
// Only if LSP response errors or times out, use document's words to show completions.
// 3. "disabled"
// Never fetch or complete document's words for completions.
// (Word-based completions can still be queried via a separate action)
//
// Default: fallback
"words": "fallback",
@@ -1107,8 +1108,8 @@
// When fetching LSP completions, determines how long to wait for a response of a particular server.
// When set to 0, waits indefinitely.
//
// Default: 500
"lsp_fetch_timeout_ms": 500
// Default: 0
"lsp_fetch_timeout_ms": 0
},
// Different settings for specific languages.
"languages": {

View File

@@ -6,15 +6,7 @@
{
"name": "Gruvbox Dark",
"appearance": "dark",
"accents": [
"#cc241dff",
"#98971aff",
"#d79921ff",
"#458588ff",
"#b16286ff",
"#689d6aff",
"#d65d0eff"
],
"accents": ["#cc241dff", "#98971aff", "#d79921ff", "#458588ff", "#b16286ff", "#689d6aff", "#d65d0eff"],
"style": {
"border": "#5b534dff",
"border.variant": "#494340ff",
@@ -105,9 +97,9 @@
"terminal.ansi.bright_white": "#fbf1c7ff",
"terminal.ansi.dim_white": "#b0a189ff",
"link_text.hover": "#83a598ff",
"version_control_added": "#b7bb26ff",
"version_control_modified": "#f9bd2fff",
"version_control_deleted": "#fb4a35ff",
"version_control.added": "#b7bb26ff",
"version_control.modified": "#f9bd2fff",
"version_control.deleted": "#fb4a35ff",
"conflict": "#f9bd2fff",
"conflict.background": "#572e10ff",
"conflict.border": "#754916ff",
@@ -399,15 +391,7 @@
{
"name": "Gruvbox Dark Hard",
"appearance": "dark",
"accents": [
"#cc241dff",
"#98971aff",
"#d79921ff",
"#458588ff",
"#b16286ff",
"#689d6aff",
"#d65d0eff"
],
"accents": ["#cc241dff", "#98971aff", "#d79921ff", "#458588ff", "#b16286ff", "#689d6aff", "#d65d0eff"],
"style": {
"border": "#5b534dff",
"border.variant": "#494340ff",
@@ -498,9 +482,9 @@
"terminal.ansi.bright_white": "#fbf1c7ff",
"terminal.ansi.dim_white": "#b0a189ff",
"link_text.hover": "#83a598ff",
"version_control_added": "#b7bb26ff",
"version_control_modified": "#f9bd2fff",
"version_control_deleted": "#fb4a35ff",
"version_control.added": "#b7bb26ff",
"version_control.modified": "#f9bd2fff",
"version_control.deleted": "#fb4a35ff",
"conflict": "#f9bd2fff",
"conflict.background": "#572e10ff",
"conflict.border": "#754916ff",
@@ -792,15 +776,7 @@
{
"name": "Gruvbox Dark Soft",
"appearance": "dark",
"accents": [
"#cc241dff",
"#98971aff",
"#d79921ff",
"#458588ff",
"#b16286ff",
"#689d6aff",
"#d65d0eff"
],
"accents": ["#cc241dff", "#98971aff", "#d79921ff", "#458588ff", "#b16286ff", "#689d6aff", "#d65d0eff"],
"style": {
"border": "#5b534dff",
"border.variant": "#494340ff",
@@ -891,9 +867,9 @@
"terminal.ansi.bright_white": "#fbf1c7ff",
"terminal.ansi.dim_white": "#b0a189ff",
"link_text.hover": "#83a598ff",
"version_control_added": "#b7bb26ff",
"version_control_modified": "#f9bd2fff",
"version_control_deleted": "#fb4a35ff",
"version_control.added": "#b7bb26ff",
"version_control.modified": "#f9bd2fff",
"version_control.deleted": "#fb4a35ff",
"conflict": "#f9bd2fff",
"conflict.background": "#572e10ff",
"conflict.border": "#754916ff",
@@ -1185,15 +1161,7 @@
{
"name": "Gruvbox Light",
"appearance": "light",
"accents": [
"#cc241dff",
"#98971aff",
"#d79921ff",
"#458588ff",
"#b16286ff",
"#689d6aff",
"#d65d0eff"
],
"accents": ["#cc241dff", "#98971aff", "#d79921ff", "#458588ff", "#b16286ff", "#689d6aff", "#d65d0eff"],
"style": {
"border": "#c8b899ff",
"border.variant": "#ddcca7ff",
@@ -1284,9 +1252,9 @@
"terminal.ansi.bright_white": "#282828ff",
"terminal.ansi.dim_white": "#73675eff",
"link_text.hover": "#0b6678ff",
"version_control_added": "#797410ff",
"version_control_modified": "#b57615ff",
"version_control_deleted": "#9d0308ff",
"version_control.added": "#797410ff",
"version_control.modified": "#b57615ff",
"version_control.deleted": "#9d0308ff",
"conflict": "#b57615ff",
"conflict.background": "#f5e2d0ff",
"conflict.border": "#ebccabff",
@@ -1578,15 +1546,7 @@
{
"name": "Gruvbox Light Hard",
"appearance": "light",
"accents": [
"#cc241dff",
"#98971aff",
"#d79921ff",
"#458588ff",
"#b16286ff",
"#689d6aff",
"#d65d0eff"
],
"accents": ["#cc241dff", "#98971aff", "#d79921ff", "#458588ff", "#b16286ff", "#689d6aff", "#d65d0eff"],
"style": {
"border": "#c8b899ff",
"border.variant": "#ddcca7ff",
@@ -1677,9 +1637,9 @@
"terminal.ansi.bright_white": "#282828ff",
"terminal.ansi.dim_white": "#73675eff",
"link_text.hover": "#0b6678ff",
"version_control_added": "#797410ff",
"version_control_modified": "#b57615ff",
"version_control_deleted": "#9d0308ff",
"version_control.added": "#797410ff",
"version_control.modified": "#b57615ff",
"version_control.deleted": "#9d0308ff",
"conflict": "#b57615ff",
"conflict.background": "#f5e2d0ff",
"conflict.border": "#ebccabff",
@@ -1971,15 +1931,7 @@
{
"name": "Gruvbox Light Soft",
"appearance": "light",
"accents": [
"#cc241dff",
"#98971aff",
"#d79921ff",
"#458588ff",
"#b16286ff",
"#689d6aff",
"#d65d0eff"
],
"accents": ["#cc241dff", "#98971aff", "#d79921ff", "#458588ff", "#b16286ff", "#689d6aff", "#d65d0eff"],
"style": {
"border": "#c8b899ff",
"border.variant": "#ddcca7ff",
@@ -2070,9 +2022,9 @@
"terminal.ansi.bright_white": "#282828ff",
"terminal.ansi.dim_white": "#73675eff",
"link_text.hover": "#0b6678ff",
"version_control_added": "#797410ff",
"version_control_modified": "#b57615ff",
"version_control_deleted": "#9d0308ff",
"version_control.added": "#797410ff",
"version_control.modified": "#b57615ff",
"version_control.deleted": "#9d0308ff",
"conflict": "#b57615ff",
"conflict.background": "#f5e2d0ff",
"conflict.border": "#ebccabff",

View File

@@ -553,7 +553,7 @@ pub struct Metadata {
pub user_id: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct Usage {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub input_tokens: Option<u32>,

View File

@@ -1246,7 +1246,7 @@ impl InlineAssistant {
});
enum DeletedLines {}
let mut editor = Editor::for_multibuffer(multi_buffer, None, true, window, cx);
let mut editor = Editor::for_multibuffer(multi_buffer, None, window, cx);
editor.set_soft_wrap_mode(language::language_settings::SoftWrap::None, cx);
editor.set_show_wrap_guides(false, cx);
editor.set_show_gutter(false, cx);
@@ -1693,7 +1693,6 @@ impl PromptEditor {
},
prompt_buffer,
None,
false,
window,
cx,
);

View File

@@ -720,7 +720,6 @@ impl PromptEditor {
},
prompt_buffer,
None,
false,
window,
cx,
);

View File

@@ -38,6 +38,7 @@ file_icons.workspace = true
fs.workspace = true
futures.workspace = true
fuzzy.workspace = true
git.workspace = true
gpui.workspace = true
heed.workspace = true
html_to_markdown.workspace = true
@@ -65,6 +66,7 @@ serde_json.workspace = true
settings.workspace = true
smol.workspace = true
streaming_diff.workspace = true
telemetry.workspace = true
telemetry_events.workspace = true
terminal.workspace = true
terminal_view.workspace = true

View File

@@ -1,26 +1,27 @@
use std::sync::Arc;
use crate::thread::{MessageId, RequestKind, Thread, ThreadError, ThreadEvent};
use crate::thread_store::ThreadStore;
use crate::tool_use::{ToolUse, ToolUseStatus};
use crate::ui::ContextPill;
use collections::HashMap;
use editor::{Editor, MultiBuffer};
use gpui::{
list, AbsoluteLength, AnyElement, App, ClickEvent, DefiniteLength, EdgesRefinement, Empty,
Entity, Focusable, Length, ListAlignment, ListOffset, ListState, StyleRefinement, Subscription,
Task, TextStyleRefinement, UnderlineStyle,
list, percentage, AbsoluteLength, Animation, AnimationExt, AnyElement, App, ClickEvent,
DefiniteLength, EdgesRefinement, Empty, Entity, Focusable, Length, ListAlignment, ListOffset,
ListState, StyleRefinement, Subscription, Task, TextStyleRefinement, Transformation,
UnderlineStyle,
};
use language::{Buffer, LanguageRegistry};
use language_model::{LanguageModelRegistry, LanguageModelToolUseId, Role};
use markdown::{Markdown, MarkdownStyle};
use scripting_tool::{ScriptingTool, ScriptingToolInput};
use settings::Settings as _;
use std::sync::Arc;
use std::time::Duration;
use theme::ThemeSettings;
use ui::Color;
use ui::{prelude::*, Disclosure, KeyBinding};
use util::ResultExt as _;
use crate::thread::{MessageId, RequestKind, Thread, ThreadError, ThreadEvent};
use crate::thread_store::ThreadStore;
use crate::tool_use::{ToolUse, ToolUseStatus};
use crate::ui::ContextPill;
pub struct ActiveThread {
language_registry: Arc<LanguageRegistry>,
thread_store: Entity<ThreadStore>,
@@ -394,7 +395,6 @@ impl ActiveThread {
editor::EditorMode::AutoHeight { max_lines: 8 },
buffer,
None,
false,
window,
cx,
);
@@ -497,7 +497,7 @@ impl ActiveThread {
};
let thread = self.thread.read(cx);
// Get all the data we need from thread before we start using it in closures
let context = thread.context_for_message(message_id);
let tool_uses = thread.tool_uses_for_message(message_id);
let scripting_tool_uses = thread.scripting_tool_uses_for_message(message_id);
@@ -652,28 +652,27 @@ impl ActiveThread {
)
.child(message_content),
),
Role::Assistant => v_flex()
.id(("message-container", ix))
.child(message_content)
.map(|parent| {
if tool_uses.is_empty() && scripting_tool_uses.is_empty() {
return parent;
}
parent.child(
v_flex()
.children(
tool_uses
.into_iter()
.map(|tool_use| self.render_tool_use(tool_use, cx)),
Role::Assistant => {
v_flex()
.id(("message-container", ix))
.child(message_content)
.when(
!tool_uses.is_empty() || !scripting_tool_uses.is_empty(),
|parent| {
parent.child(
v_flex()
.children(
tool_uses
.into_iter()
.map(|tool_use| self.render_tool_use(tool_use, cx)),
)
.children(scripting_tool_uses.into_iter().map(|tool_use| {
self.render_scripting_tool_use(tool_use, cx)
})),
)
.children(
scripting_tool_uses
.into_iter()
.map(|tool_use| self.render_scripting_tool_use(tool_use, cx)),
),
},
)
}),
}
Role::System => div().id(("message-container", ix)).py_1().px_2().child(
v_flex()
.bg(colors.editor_background)
@@ -692,27 +691,28 @@ impl ActiveThread {
.copied()
.unwrap_or_default();
let lighter_border = cx.theme().colors().border.opacity(0.5);
div().px_2p5().child(
v_flex()
.gap_1()
.rounded_lg()
.border_1()
.border_color(cx.theme().colors().border)
.border_color(lighter_border)
.child(
h_flex()
.justify_between()
.py_0p5()
.py_1()
.pl_1()
.pr_2()
.bg(cx.theme().colors().editor_foreground.opacity(0.02))
.bg(cx.theme().colors().editor_foreground.opacity(0.025))
.map(|element| {
if is_open {
element.border_b_1().rounded_t(px(6.))
element.border_b_1().rounded_t_md()
} else {
element.rounded_md()
}
})
.border_color(cx.theme().colors().border)
.border_color(lighter_border)
.child(
h_flex()
.gap_1()
@@ -729,57 +729,129 @@ impl ActiveThread {
}
}),
))
.child(Label::new(tool_use.name)),
.child(
Label::new(tool_use.name)
.size(LabelSize::Small)
.buffer_font(cx),
),
)
.child(
Label::new(match tool_use.status {
ToolUseStatus::Pending => "Pending",
ToolUseStatus::Running => "Running",
ToolUseStatus::Finished(_) => "Finished",
ToolUseStatus::Error(_) => "Error",
})
.size(LabelSize::XSmall)
.buffer_font(cx),
),
.child({
let (icon_name, color, animated) = match &tool_use.status {
ToolUseStatus::Pending => {
(IconName::Warning, Color::Warning, false)
}
ToolUseStatus::Running => {
(IconName::ArrowCircle, Color::Accent, true)
}
ToolUseStatus::Finished(_) => {
(IconName::Check, Color::Success, false)
}
ToolUseStatus::Error(_) => (IconName::Close, Color::Error, false),
};
let icon = Icon::new(icon_name).color(color).size(IconSize::Small);
if animated {
icon.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| {
icon.transform(Transformation::rotate(percentage(delta)))
},
)
.into_any_element()
} else {
icon.into_any_element()
}
}),
)
.map(|parent| {
if !is_open {
return parent;
}
let content_container = || v_flex().py_1().gap_0p5().px_2p5();
parent.child(
v_flex()
.gap_1()
.bg(cx.theme().colors().editor_background)
.rounded_b_lg()
.child(
v_flex()
.gap_0p5()
.py_1()
.px_2p5()
content_container()
.border_b_1()
.border_color(cx.theme().colors().border)
.child(Label::new("Input:"))
.child(Label::new(
serde_json::to_string_pretty(&tool_use.input)
.unwrap_or_default(),
)),
.border_color(lighter_border)
.child(
Label::new("Input")
.size(LabelSize::XSmall)
.color(Color::Muted)
.buffer_font(cx),
)
.child(
Label::new(
serde_json::to_string_pretty(&tool_use.input)
.unwrap_or_default(),
)
.size(LabelSize::Small)
.buffer_font(cx),
),
)
.map(|parent| match tool_use.status {
ToolUseStatus::Finished(output) => parent.child(
v_flex()
.gap_0p5()
.py_1()
.px_2p5()
.child(Label::new("Result:"))
.child(Label::new(output)),
.map(|container| match tool_use.status {
ToolUseStatus::Finished(output) => container.child(
content_container()
.child(
Label::new("Result")
.size(LabelSize::XSmall)
.color(Color::Muted)
.buffer_font(cx),
)
.child(
Label::new(output)
.size(LabelSize::Small)
.buffer_font(cx),
),
),
ToolUseStatus::Error(err) => parent.child(
v_flex()
.gap_0p5()
.py_1()
.px_2p5()
.child(Label::new("Error:"))
.child(Label::new(err)),
ToolUseStatus::Running => container.child(
content_container().child(
h_flex()
.gap_1()
.pb_1()
.child(
Icon::new(IconName::ArrowCircle)
.size(IconSize::Small)
.color(Color::Accent)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2))
.repeat(),
|icon, delta| {
icon.transform(Transformation::rotate(
percentage(delta),
))
},
),
)
.child(
Label::new("Running…")
.size(LabelSize::XSmall)
.color(Color::Muted)
.buffer_font(cx),
),
),
),
ToolUseStatus::Pending | ToolUseStatus::Running => parent,
ToolUseStatus::Error(err) => container.child(
content_container()
.child(
Label::new("Error")
.size(LabelSize::XSmall)
.color(Color::Muted)
.buffer_font(cx),
)
.child(
Label::new(err).size(LabelSize::Small).buffer_font(cx),
),
),
ToolUseStatus::Pending => container,
}),
)
}),
@@ -812,7 +884,7 @@ impl ActiveThread {
.bg(cx.theme().colors().editor_foreground.opacity(0.02))
.map(|element| {
if is_open {
element.border_b_1().rounded_t(px(6.))
element.border_b_1().rounded_t_md()
} else {
element.rounded_md()
}

View File

@@ -53,7 +53,8 @@ actions!(
FocusLeft,
FocusRight,
RemoveFocusedContext,
AcceptSuggestedContext
AcceptSuggestedContext,
OpenActiveThreadAsMarkdown
]
);

View File

@@ -11,7 +11,7 @@ use assistant_slash_command::SlashCommandWorkingSet;
use assistant_tool::ToolWorkingSet;
use client::zed_urls;
use editor::Editor;
use editor::{Editor, MultiBuffer};
use fs::Fs;
use gpui::{
prelude::*, Action, AnyElement, App, AsyncWindowContext, Corner, Entity, EventEmitter,
@@ -38,7 +38,10 @@ use crate::message_editor::MessageEditor;
use crate::thread::{Thread, ThreadError, ThreadId};
use crate::thread_history::{PastContext, PastThread, ThreadHistory};
use crate::thread_store::ThreadStore;
use crate::{InlineAssistant, NewPromptEditor, NewThread, OpenConfiguration, OpenHistory};
use crate::{
InlineAssistant, NewPromptEditor, NewThread, OpenActiveThreadAsMarkdown, OpenConfiguration,
OpenHistory,
};
pub fn init(cx: &mut App) {
cx.observe_new(
@@ -411,6 +414,65 @@ impl AssistantPanel {
}
}
pub(crate) fn open_active_thread_as_markdown(
&mut self,
_: &OpenActiveThreadAsMarkdown,
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(workspace) = self
.workspace
.upgrade()
.ok_or_else(|| anyhow!("workspace dropped"))
.log_err()
else {
return;
};
let markdown_language_task = workspace
.read(cx)
.app_state()
.languages
.language_for_name("Markdown");
let thread = self.active_thread(cx);
cx.spawn_in(window, |_this, mut cx| async move {
let markdown_language = markdown_language_task.await?;
workspace.update_in(&mut cx, |workspace, window, cx| {
let thread = thread.read(cx);
let markdown = thread.to_markdown()?;
let thread_summary = thread
.summary()
.map(|summary| summary.to_string())
.unwrap_or_else(|| "Thread".to_string());
let project = workspace.project().clone();
let buffer = project.update(cx, |project, cx| {
project.create_local_buffer(&markdown, Some(markdown_language), cx)
});
let buffer = cx.new(|cx| {
MultiBuffer::singleton(buffer, cx).with_title(thread_summary.clone())
});
workspace.add_item_to_active_pane(
Box::new(cx.new(|cx| {
let mut editor =
Editor::for_multibuffer(buffer, Some(project.clone()), window, cx);
editor.set_breadcrumb_header(thread_summary);
editor
})),
None,
true,
window,
cx,
);
anyhow::Ok(())
})
})
.detach_and_log_err(cx);
}
fn handle_assistant_configuration_event(
&mut self,
_entity: &Entity<AssistantConfiguration>,
@@ -1011,6 +1073,7 @@ impl Render for AssistantPanel {
.on_action(cx.listener(|this, _: &OpenHistory, window, cx| {
this.open_history(window, cx);
}))
.on_action(cx.listener(Self::open_active_thread_as_markdown))
.on_action(cx.listener(Self::deploy_prompt_library))
.child(self.render_toolbar(cx))
.map(|parent| match self.active_view {

View File

@@ -223,13 +223,18 @@ pub fn render_thread_context_entry(
h_flex()
.gap_1p5()
.w_full()
.justify_between()
.child(
Icon::new(IconName::MessageCircle)
.size(IconSize::XSmall)
.color(Color::Muted),
h_flex()
.gap_1p5()
.max_w_72()
.child(
Icon::new(IconName::MessageCircle)
.size(IconSize::XSmall)
.color(Color::Muted),
)
.child(Label::new(thread.summary.clone()).truncate()),
)
.child(Label::new(thread.summary.clone()))
.child(div().w_full())
.when(added, |el| {
el.child(
h_flex()

View File

@@ -2,10 +2,10 @@ use assistant_context_editor::SavedContextMetadata;
use chrono::{DateTime, Utc};
use gpui::{prelude::*, Entity};
use crate::thread_store::{SavedThreadMetadata, ThreadStore};
use crate::thread_store::{SerializedThreadMetadata, ThreadStore};
pub enum HistoryEntry {
Thread(SavedThreadMetadata),
Thread(SerializedThreadMetadata),
Context(SavedContextMetadata),
}

View File

@@ -1341,7 +1341,7 @@ impl InlineAssistant {
});
enum DeletedLines {}
let mut editor = Editor::for_multibuffer(multi_buffer, None, true, window, cx);
let mut editor = Editor::for_multibuffer(multi_buffer, None, window, cx);
editor.set_soft_wrap_mode(language::language_settings::SoftWrap::None, cx);
editor.set_show_wrap_guides(false, cx);
editor.set_show_gutter(false, cx);

View File

@@ -843,7 +843,6 @@ impl PromptEditor<BufferCodegen> {
},
prompt_buffer,
None,
false,
window,
cx,
);
@@ -1001,7 +1000,6 @@ impl PromptEditor<TerminalCodegen> {
},
prompt_buffer,
None,
false,
window,
cx,
);

View File

@@ -20,7 +20,8 @@ use ui::{
Tooltip,
};
use vim_mode_setting::VimModeSetting;
use workspace::Workspace;
use workspace::notifications::{NotificationId, NotifyTaskExt};
use workspace::{Toast, Workspace};
use crate::assistant_model_selector::AssistantModelSelector;
use crate::context_picker::{ConfirmBehavior, ContextPicker};
@@ -34,6 +35,7 @@ use crate::{Chat, ChatMode, RemoveAllContext, ToggleContextPicker};
pub struct MessageEditor {
thread: Entity<Thread>,
editor: Entity<Editor>,
workspace: WeakEntity<Workspace>,
context_store: Entity<ContextStore>,
context_strip: Entity<ContextStrip>,
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
@@ -106,6 +108,7 @@ impl MessageEditor {
Self {
thread,
editor: editor.clone(),
workspace,
context_store,
context_strip,
context_picker_menu_handle,
@@ -150,6 +153,14 @@ impl MessageEditor {
}
fn chat(&mut self, _: &Chat, window: &mut Window, cx: &mut Context<Self>) {
if self.is_editor_empty(cx) {
return;
}
if self.thread.read(cx).is_streaming() {
return;
}
self.send_to_model(RequestKind::Chat, window, cx);
}
@@ -272,6 +283,34 @@ impl MessageEditor {
self.context_strip.focus_handle(cx).focus(window);
}
}
fn handle_feedback_click(
&mut self,
is_positive: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
let workspace = self.workspace.clone();
let report = self
.thread
.update(cx, |thread, cx| thread.report_feedback(is_positive, cx));
cx.spawn(|_, mut cx| async move {
report.await?;
workspace.update(&mut cx, |workspace, cx| {
let message = if is_positive {
"Positive feedback recorded. Thank you!"
} else {
"Negative feedback recorded. Thank you for helping us improve!"
};
struct ThreadFeedback;
let id = NotificationId::unique::<ThreadFeedback>();
workspace.show_toast(Toast::new(id, message).autohide(), cx)
})
})
.detach_and_notify_err(window, cx);
}
}
impl Focusable for MessageEditor {
@@ -489,7 +528,45 @@ impl Render for MessageEditor {
.bg(bg_color)
.border_t_1()
.border_color(cx.theme().colors().border)
.child(self.context_strip.clone())
.child(
h_flex()
.justify_between()
.child(self.context_strip.clone())
.when(!self.thread.read(cx).is_empty(), |this| {
this.child(
h_flex()
.gap_2()
.child(
IconButton::new(
"feedback-thumbs-up",
IconName::ThumbsUp,
)
.style(ButtonStyle::Subtle)
.icon_size(IconSize::Small)
.tooltip(Tooltip::text("Helpful"))
.on_click(
cx.listener(|this, _, window, cx| {
this.handle_feedback_click(true, window, cx);
}),
),
)
.child(
IconButton::new(
"feedback-thumbs-down",
IconName::ThumbsDown,
)
.style(ButtonStyle::Subtle)
.icon_size(IconSize::Small)
.tooltip(Tooltip::text("Not Helpful"))
.on_click(
cx.listener(|this, _, window, cx| {
this.handle_feedback_click(false, window, cx);
}),
),
),
)
}),
)
.child(
v_flex()
.gap_5()

View File

@@ -1,26 +1,31 @@
use std::io::Write;
use std::sync::Arc;
use anyhow::{Context as _, Result};
use assistant_tool::ToolWorkingSet;
use chrono::{DateTime, Utc};
use collections::{BTreeMap, HashMap, HashSet};
use futures::StreamExt as _;
use futures::future::Shared;
use futures::{FutureExt, StreamExt as _};
use git;
use gpui::{App, AppContext, Context, Entity, EventEmitter, SharedString, Task};
use language_model::{
LanguageModel, LanguageModelCompletionEvent, LanguageModelRegistry, LanguageModelRequest,
LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolResult,
LanguageModelToolUseId, MaxMonthlySpendReachedError, MessageContent, PaymentRequiredError,
Role, StopReason,
Role, StopReason, TokenUsage,
};
use project::Project;
use prompt_store::PromptBuilder;
use prompt_store::{AssistantSystemPromptWorktree, PromptBuilder};
use scripting_tool::{ScriptingSession, ScriptingTool};
use serde::{Deserialize, Serialize};
use util::{post_inc, ResultExt, TryFutureExt as _};
use uuid::Uuid;
use crate::context::{attach_context_to_message, ContextId, ContextSnapshot};
use crate::thread_store::SavedThread;
use crate::thread_store::{
SerializedMessage, SerializedThread, SerializedToolResult, SerializedToolUse,
};
use crate::tool_use::{PendingToolUse, ToolUse, ToolUseState};
#[derive(Debug, Clone, Copy)]
@@ -62,6 +67,27 @@ pub struct Message {
pub text: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProjectSnapshot {
pub worktree_snapshots: Vec<WorktreeSnapshot>,
pub unsaved_buffer_paths: Vec<String>,
pub timestamp: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorktreeSnapshot {
pub worktree_path: String,
pub git_state: Option<GitState>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GitState {
pub remote_url: Option<String>,
pub head_sha: Option<String>,
pub current_branch: Option<String>,
pub diff: Option<String>,
}
/// A thread of conversation with the LLM.
pub struct Thread {
id: ThreadId,
@@ -80,6 +106,8 @@ pub struct Thread {
tool_use: ToolUseState,
scripting_session: Entity<ScriptingSession>,
scripting_tool_use: ToolUseState,
initial_project_snapshot: Shared<Task<Option<Arc<ProjectSnapshot>>>>,
cumulative_token_usage: TokenUsage,
}
impl Thread {
@@ -89,8 +117,6 @@ impl Thread {
prompt_builder: Arc<PromptBuilder>,
cx: &mut Context<Self>,
) -> Self {
let scripting_session = cx.new(|cx| ScriptingSession::new(project.clone(), cx));
Self {
id: ThreadId::new(),
updated_at: Utc::now(),
@@ -102,42 +128,52 @@ impl Thread {
context_by_message: HashMap::default(),
completion_count: 0,
pending_completions: Vec::new(),
project,
project: project.clone(),
prompt_builder,
tools,
tool_use: ToolUseState::new(),
scripting_session,
scripting_session: cx.new(|cx| ScriptingSession::new(project.clone(), cx)),
scripting_tool_use: ToolUseState::new(),
initial_project_snapshot: {
let project_snapshot = Self::project_snapshot(project, cx);
cx.foreground_executor()
.spawn(async move { Some(project_snapshot.await) })
.shared()
},
cumulative_token_usage: TokenUsage::default(),
}
}
pub fn from_saved(
pub fn deserialize(
id: ThreadId,
saved: SavedThread,
serialized: SerializedThread,
project: Entity<Project>,
tools: Arc<ToolWorkingSet>,
prompt_builder: Arc<PromptBuilder>,
cx: &mut Context<Self>,
) -> Self {
let next_message_id = MessageId(
saved
serialized
.messages
.last()
.map(|message| message.id.0 + 1)
.unwrap_or(0),
);
let tool_use =
ToolUseState::from_saved_messages(&saved.messages, |name| name != ScriptingTool::NAME);
let tool_use = ToolUseState::from_serialized_messages(&serialized.messages, |name| {
name != ScriptingTool::NAME
});
let scripting_tool_use =
ToolUseState::from_saved_messages(&saved.messages, |name| name == ScriptingTool::NAME);
ToolUseState::from_serialized_messages(&serialized.messages, |name| {
name == ScriptingTool::NAME
});
let scripting_session = cx.new(|cx| ScriptingSession::new(project.clone(), cx));
Self {
id,
updated_at: saved.updated_at,
summary: Some(saved.summary),
updated_at: serialized.updated_at,
summary: Some(serialized.summary),
pending_summary: Task::ready(None),
messages: saved
messages: serialized
.messages
.into_iter()
.map(|message| Message {
@@ -157,6 +193,9 @@ impl Thread {
tool_use,
scripting_session,
scripting_tool_use,
initial_project_snapshot: Task::ready(serialized.initial_project_snapshot).shared(),
// TODO: persist token usage?
cumulative_token_usage: TokenUsage::default(),
}
}
@@ -199,7 +238,7 @@ impl Thread {
}
pub fn is_streaming(&self) -> bool {
!self.pending_completions.is_empty()
!self.pending_completions.is_empty() || !self.all_tools_finished()
}
pub fn tools(&self) -> &Arc<ToolWorkingSet> {
@@ -344,6 +383,47 @@ impl Thread {
text
}
/// Serializes this thread into a format for storage or telemetry.
pub fn serialize(&self, cx: &mut Context<Self>) -> Task<Result<SerializedThread>> {
let initial_project_snapshot = self.initial_project_snapshot.clone();
cx.spawn(|this, cx| async move {
let initial_project_snapshot = initial_project_snapshot.await;
this.read_with(&cx, |this, _| SerializedThread {
summary: this.summary_or_default(),
updated_at: this.updated_at(),
messages: this
.messages()
.map(|message| SerializedMessage {
id: message.id,
role: message.role,
text: message.text.clone(),
tool_uses: this
.tool_uses_for_message(message.id)
.into_iter()
.chain(this.scripting_tool_uses_for_message(message.id))
.map(|tool_use| SerializedToolUse {
id: tool_use.id,
name: tool_use.name,
input: tool_use.input,
})
.collect(),
tool_results: this
.tool_results_for_message(message.id)
.into_iter()
.chain(this.scripting_tool_results_for_message(message.id))
.map(|tool_result| SerializedToolResult {
tool_use_id: tool_result.tool_use_id.clone(),
is_error: tool_result.is_error,
content: tool_result.content.clone(),
})
.collect(),
})
.collect(),
initial_project_snapshot,
})
})
}
pub fn send_to_model(
&mut self,
model: Arc<dyn LanguageModel>,
@@ -384,8 +464,14 @@ impl Thread {
let worktree_root_names = self
.project
.read(cx)
.worktree_root_names(cx)
.map(ToString::to_string)
.visible_worktrees(cx)
.map(|worktree| {
let worktree = worktree.read(cx);
AssistantSystemPromptWorktree {
root_name: worktree.root_name().into(),
abs_path: worktree.abs_path(),
}
})
.collect::<Vec<_>>();
let system_prompt = self
.prompt_builder
@@ -483,6 +569,7 @@ impl Thread {
let stream_completion = async {
let mut events = stream.await?;
let mut stop_reason = StopReason::EndTurn;
let mut current_token_usage = TokenUsage::default();
while let Some(event) = events.next().await {
let event = event?;
@@ -495,6 +582,12 @@ impl Thread {
LanguageModelCompletionEvent::Stop(reason) => {
stop_reason = reason;
}
LanguageModelCompletionEvent::UsageUpdate(token_usage) => {
thread.cumulative_token_usage =
thread.cumulative_token_usage.clone() + token_usage.clone()
- current_token_usage.clone();
current_token_usage = token_usage;
}
LanguageModelCompletionEvent::Text(chunk) => {
if let Some(last_message) = thread.messages.last_mut() {
if last_message.role == Role::Assistant {
@@ -788,6 +881,185 @@ impl Thread {
false
}
}
/// Reports feedback about the thread and stores it in our telemetry backend.
pub fn report_feedback(&self, is_positive: bool, cx: &mut Context<Self>) -> Task<Result<()>> {
let final_project_snapshot = Self::project_snapshot(self.project.clone(), cx);
let serialized_thread = self.serialize(cx);
let thread_id = self.id().clone();
let client = self.project.read(cx).client();
cx.background_spawn(async move {
let final_project_snapshot = final_project_snapshot.await;
let serialized_thread = serialized_thread.await?;
let thread_data =
serde_json::to_value(serialized_thread).unwrap_or_else(|_| serde_json::Value::Null);
let rating = if is_positive { "positive" } else { "negative" };
telemetry::event!(
"Assistant Thread Rated",
rating,
thread_id,
thread_data,
final_project_snapshot
);
client.telemetry().flush_events();
Ok(())
})
}
/// Create a snapshot of the current project state including git information and unsaved buffers.
fn project_snapshot(
project: Entity<Project>,
cx: &mut Context<Self>,
) -> Task<Arc<ProjectSnapshot>> {
let worktree_snapshots: Vec<_> = project
.read(cx)
.visible_worktrees(cx)
.map(|worktree| Self::worktree_snapshot(worktree, cx))
.collect();
cx.spawn(move |_, cx| async move {
let worktree_snapshots = futures::future::join_all(worktree_snapshots).await;
let mut unsaved_buffers = Vec::new();
cx.update(|app_cx| {
let buffer_store = project.read(app_cx).buffer_store();
for buffer_handle in buffer_store.read(app_cx).buffers() {
let buffer = buffer_handle.read(app_cx);
if buffer.is_dirty() {
if let Some(file) = buffer.file() {
let path = file.path().to_string_lossy().to_string();
unsaved_buffers.push(path);
}
}
}
})
.ok();
Arc::new(ProjectSnapshot {
worktree_snapshots,
unsaved_buffer_paths: unsaved_buffers,
timestamp: Utc::now(),
})
})
}
fn worktree_snapshot(worktree: Entity<project::Worktree>, cx: &App) -> Task<WorktreeSnapshot> {
cx.spawn(move |cx| async move {
// Get worktree path and snapshot
let worktree_info = cx.update(|app_cx| {
let worktree = worktree.read(app_cx);
let path = worktree.abs_path().to_string_lossy().to_string();
let snapshot = worktree.snapshot();
(path, snapshot)
});
let Ok((worktree_path, snapshot)) = worktree_info else {
return WorktreeSnapshot {
worktree_path: String::new(),
git_state: None,
};
};
// Extract git information
let git_state = match snapshot.repositories().first() {
None => None,
Some(repo_entry) => {
// Get branch information
let current_branch = repo_entry.branch().map(|branch| branch.name.to_string());
// Get repository info
let repo_result = worktree.read_with(&cx, |worktree, _cx| {
if let project::Worktree::Local(local_worktree) = &worktree {
local_worktree.get_local_repo(repo_entry).map(|local_repo| {
let repo = local_repo.repo();
(repo.remote_url("origin"), repo.head_sha(), repo.clone())
})
} else {
None
}
});
match repo_result {
Ok(Some((remote_url, head_sha, repository))) => {
// Get diff asynchronously
let diff = repository
.diff(git::repository::DiffType::HeadToWorktree, cx)
.await
.ok();
Some(GitState {
remote_url,
head_sha,
current_branch,
diff,
})
}
Err(_) | Ok(None) => None,
}
}
};
WorktreeSnapshot {
worktree_path,
git_state,
}
})
}
pub fn to_markdown(&self) -> Result<String> {
let mut markdown = Vec::new();
if let Some(summary) = self.summary() {
writeln!(markdown, "# {summary}\n")?;
};
for message in self.messages() {
writeln!(
markdown,
"## {role}\n",
role = match message.role {
Role::User => "User",
Role::Assistant => "Assistant",
Role::System => "System",
}
)?;
writeln!(markdown, "{}\n", message.text)?;
for tool_use in self.tool_uses_for_message(message.id) {
writeln!(
markdown,
"**Use Tool: {} ({})**",
tool_use.name, tool_use.id
)?;
writeln!(markdown, "```json")?;
writeln!(
markdown,
"{}",
serde_json::to_string_pretty(&tool_use.input)?
)?;
writeln!(markdown, "```")?;
}
for tool_result in self.tool_results_for_message(message.id) {
write!(markdown, "**Tool Results: {}", tool_result.tool_use_id)?;
if tool_result.is_error {
write!(markdown, " (Error)")?;
}
writeln!(markdown, "**\n")?;
writeln!(markdown, "{}", tool_result.content)?;
}
}
Ok(String::from_utf8_lossy(&markdown).to_string())
}
pub fn cumulative_token_usage(&self) -> TokenUsage {
self.cumulative_token_usage.clone()
}
}
#[derive(Debug, Clone)]

View File

@@ -7,7 +7,7 @@ use time::{OffsetDateTime, UtcOffset};
use ui::{prelude::*, IconButtonShape, ListItem, ListItemSpacing, Tooltip};
use crate::history_store::{HistoryEntry, HistoryStore};
use crate::thread_store::SavedThreadMetadata;
use crate::thread_store::SerializedThreadMetadata;
use crate::{AssistantPanel, RemoveSelectedThread};
pub struct ThreadHistory {
@@ -221,14 +221,14 @@ impl Render for ThreadHistory {
#[derive(IntoElement)]
pub struct PastThread {
thread: SavedThreadMetadata,
thread: SerializedThreadMetadata,
assistant_panel: WeakEntity<AssistantPanel>,
selected: bool,
}
impl PastThread {
pub fn new(
thread: SavedThreadMetadata,
thread: SerializedThreadMetadata,
assistant_panel: WeakEntity<AssistantPanel>,
selected: bool,
) -> Self {

View File

@@ -20,7 +20,7 @@ use prompt_store::PromptBuilder;
use serde::{Deserialize, Serialize};
use util::ResultExt as _;
use crate::thread::{MessageId, Thread, ThreadId};
use crate::thread::{MessageId, ProjectSnapshot, Thread, ThreadId};
pub fn init(cx: &mut App) {
ThreadsDatabase::init(cx);
@@ -32,7 +32,7 @@ pub struct ThreadStore {
prompt_builder: Arc<PromptBuilder>,
context_server_manager: Entity<ContextServerManager>,
context_server_tool_ids: HashMap<Arc<str>, Vec<ToolId>>,
threads: Vec<SavedThreadMetadata>,
threads: Vec<SerializedThreadMetadata>,
}
impl ThreadStore {
@@ -70,13 +70,13 @@ impl ThreadStore {
self.threads.len()
}
pub fn threads(&self) -> Vec<SavedThreadMetadata> {
pub fn threads(&self) -> Vec<SerializedThreadMetadata> {
let mut threads = self.threads.iter().cloned().collect::<Vec<_>>();
threads.sort_unstable_by_key(|thread| std::cmp::Reverse(thread.updated_at));
threads
}
pub fn recent_threads(&self, limit: usize) -> Vec<SavedThreadMetadata> {
pub fn recent_threads(&self, limit: usize) -> Vec<SerializedThreadMetadata> {
self.threads().into_iter().take(limit).collect()
}
@@ -107,7 +107,7 @@ impl ThreadStore {
this.update(&mut cx, |this, cx| {
cx.new(|cx| {
Thread::from_saved(
Thread::deserialize(
id.clone(),
thread,
this.project.clone(),
@@ -121,53 +121,14 @@ impl ThreadStore {
}
pub fn save_thread(&self, thread: &Entity<Thread>, cx: &mut Context<Self>) -> Task<Result<()>> {
let (metadata, thread) = thread.update(cx, |thread, _cx| {
let id = thread.id().clone();
let thread = SavedThread {
summary: thread.summary_or_default(),
updated_at: thread.updated_at(),
messages: thread
.messages()
.map(|message| {
let all_tool_uses = thread
.tool_uses_for_message(message.id)
.into_iter()
.chain(thread.scripting_tool_uses_for_message(message.id))
.map(|tool_use| SavedToolUse {
id: tool_use.id,
name: tool_use.name,
input: tool_use.input,
})
.collect();
let all_tool_results = thread
.tool_results_for_message(message.id)
.into_iter()
.chain(thread.scripting_tool_results_for_message(message.id))
.map(|tool_result| SavedToolResult {
tool_use_id: tool_result.tool_use_id.clone(),
is_error: tool_result.is_error,
content: tool_result.content.clone(),
})
.collect();
SavedMessage {
id: message.id,
role: message.role,
text: message.text.clone(),
tool_uses: all_tool_uses,
tool_results: all_tool_results,
}
})
.collect(),
};
(id, thread)
});
let (metadata, serialized_thread) =
thread.update(cx, |thread, cx| (thread.id().clone(), thread.serialize(cx)));
let database_future = ThreadsDatabase::global_future(cx);
cx.spawn(|this, mut cx| async move {
let serialized_thread = serialized_thread.await?;
let database = database_future.await.map_err(|err| anyhow!(err))?;
database.save_thread(metadata, thread).await?;
database.save_thread(metadata, serialized_thread).await?;
this.update(&mut cx, |this, cx| this.reload(cx))?.await
})
@@ -270,39 +231,41 @@ impl ThreadStore {
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SavedThreadMetadata {
pub struct SerializedThreadMetadata {
pub id: ThreadId,
pub summary: SharedString,
pub updated_at: DateTime<Utc>,
}
#[derive(Serialize, Deserialize)]
pub struct SavedThread {
pub struct SerializedThread {
pub summary: SharedString,
pub updated_at: DateTime<Utc>,
pub messages: Vec<SavedMessage>,
pub messages: Vec<SerializedMessage>,
#[serde(default)]
pub initial_project_snapshot: Option<Arc<ProjectSnapshot>>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SavedMessage {
pub struct SerializedMessage {
pub id: MessageId,
pub role: Role,
pub text: String,
#[serde(default)]
pub tool_uses: Vec<SavedToolUse>,
pub tool_uses: Vec<SerializedToolUse>,
#[serde(default)]
pub tool_results: Vec<SavedToolResult>,
pub tool_results: Vec<SerializedToolResult>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SavedToolUse {
pub struct SerializedToolUse {
pub id: LanguageModelToolUseId,
pub name: SharedString,
pub input: serde_json::Value,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SavedToolResult {
pub struct SerializedToolResult {
pub tool_use_id: LanguageModelToolUseId,
pub is_error: bool,
pub content: Arc<str>,
@@ -317,7 +280,7 @@ impl Global for GlobalThreadsDatabase {}
pub(crate) struct ThreadsDatabase {
executor: BackgroundExecutor,
env: heed::Env,
threads: Database<SerdeBincode<ThreadId>, SerdeJson<SavedThread>>,
threads: Database<SerdeBincode<ThreadId>, SerdeJson<SerializedThread>>,
}
impl ThreadsDatabase {
@@ -364,7 +327,7 @@ impl ThreadsDatabase {
})
}
pub fn list_threads(&self) -> Task<Result<Vec<SavedThreadMetadata>>> {
pub fn list_threads(&self) -> Task<Result<Vec<SerializedThreadMetadata>>> {
let env = self.env.clone();
let threads = self.threads;
@@ -373,7 +336,7 @@ impl ThreadsDatabase {
let mut iter = threads.iter(&txn)?;
let mut threads = Vec::new();
while let Some((key, value)) = iter.next().transpose()? {
threads.push(SavedThreadMetadata {
threads.push(SerializedThreadMetadata {
id: key,
summary: value.summary,
updated_at: value.updated_at,
@@ -384,7 +347,7 @@ impl ThreadsDatabase {
})
}
pub fn try_find_thread(&self, id: ThreadId) -> Task<Result<Option<SavedThread>>> {
pub fn try_find_thread(&self, id: ThreadId) -> Task<Result<Option<SerializedThread>>> {
let env = self.env.clone();
let threads = self.threads;
@@ -395,7 +358,7 @@ impl ThreadsDatabase {
})
}
pub fn save_thread(&self, id: ThreadId, thread: SavedThread) -> Task<Result<()>> {
pub fn save_thread(&self, id: ThreadId, thread: SerializedThread) -> Task<Result<()>> {
let env = self.env.clone();
let threads = self.threads;

View File

@@ -3,7 +3,7 @@ use std::sync::Arc;
use assistant_tool::{ToolSource, ToolWorkingSet};
use gpui::Entity;
use scripting_tool::ScriptingTool;
use ui::{prelude::*, ContextMenu, IconButtonShape, PopoverMenu, Tooltip};
use ui::{prelude::*, ContextMenu, PopoverMenu, Tooltip};
pub struct ToolSelector {
tools: Arc<ToolWorkingSet>,
@@ -24,22 +24,16 @@ impl ToolSelector {
let tools_by_source = self.tools.tools_by_source(cx);
let all_tools_enabled = self.tools.are_all_tools_enabled();
menu = menu.header("Tools").toggleable_entry(
"All Tools",
all_tools_enabled,
icon_position,
None,
{
let tools = self.tools.clone();
move |_window, cx| {
if all_tools_enabled {
tools.disable_all_tools(cx);
} else {
tools.enable_all_tools();
}
menu = menu.toggleable_entry("All Tools", all_tools_enabled, icon_position, None, {
let tools = self.tools.clone();
move |_window, cx| {
if all_tools_enabled {
tools.disable_all_tools(cx);
} else {
tools.enable_all_tools();
}
},
);
}
});
for (source, tools) in tools_by_source {
let mut tools = tools
@@ -63,7 +57,7 @@ impl ToolSelector {
}
menu = match &source {
ToolSource::Native => menu.header("Zed"),
ToolSource::Native => menu.separator().header("Zed Tools"),
ToolSource::ContextServer { id } => {
let all_tools_from_source_enabled =
self.tools.are_all_tools_from_source_enabled(&source);
@@ -124,7 +118,6 @@ impl Render for ToolSelector {
})
.trigger_with_tooltip(
IconButton::new("tool-selector-button", IconName::SettingsAlt)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.icon_color(Color::Muted),
Tooltip::text("Customize Tools"),

View File

@@ -11,7 +11,7 @@ use language_model::{
};
use crate::thread::MessageId;
use crate::thread_store::SavedMessage;
use crate::thread_store::SerializedMessage;
#[derive(Debug)]
pub struct ToolUse {
@@ -46,11 +46,11 @@ impl ToolUseState {
}
}
/// Constructs a [`ToolUseState`] from the given list of [`SavedMessage`]s.
/// Constructs a [`ToolUseState`] from the given list of [`SerializedMessage`]s.
///
/// Accepts a function to filter the tools that should be used to populate the state.
pub fn from_saved_messages(
messages: &[SavedMessage],
pub fn from_serialized_messages(
messages: &[SerializedMessage],
mut filter_by_tool_name: impl FnMut(&str) -> bool,
) -> Self {
let mut this = Self::new();

View File

@@ -126,7 +126,13 @@ impl RenderOnce for ContextPill {
h_flex()
.id("context-data")
.gap_1()
.child(Label::new(context.name.clone()).size(LabelSize::Small))
.child(
div().max_w_64().child(
Label::new(context.name.clone())
.size(LabelSize::Small)
.truncate(),
),
)
.when_some(context.parent.as_ref(), |element, parent_name| {
if *dupe_name {
element.child(
@@ -174,21 +180,22 @@ impl RenderOnce for ContextPill {
})
.hover(|style| style.bg(color.element_hover.opacity(0.5)))
.child(
Label::new(name.clone())
.size(LabelSize::Small)
.color(Color::Muted),
div().px_0p5().max_w_64().child(
Label::new(name.clone())
.size(LabelSize::Small)
.color(Color::Muted)
.truncate(),
),
)
.child(
div().px_0p5().child(
Label::new(match kind {
ContextKind::File => "Active Tab",
ContextKind::Thread
| ContextKind::Directory
| ContextKind::FetchedUrl => "Active",
})
.size(LabelSize::XSmall)
.color(Color::Muted),
),
Label::new(match kind {
ContextKind::File => "Active Tab",
ContextKind::Thread | ContextKind::Directory | ContextKind::FetchedUrl => {
"Active"
}
})
.size(LabelSize::XSmall)
.color(Color::Muted),
)
.child(
Icon::new(IconName::Plus)

View File

@@ -2254,6 +2254,7 @@ impl AssistantContext {
);
}
LanguageModelCompletionEvent::ToolUse(_) => {}
LanguageModelCompletionEvent::UsageUpdate(_) => {}
}
});

View File

@@ -48,7 +48,7 @@ impl SlashCommandCompletionProvider {
name_range: Range<Anchor>,
window: &mut Window,
cx: &mut App,
) -> Task<Result<Vec<project::Completion>>> {
) -> Task<Result<Option<Vec<project::Completion>>>> {
let slash_commands = self.slash_commands.clone();
let candidates = slash_commands
.command_names(cx)
@@ -71,65 +71,67 @@ impl SlashCommandCompletionProvider {
.await;
cx.update(|_, cx| {
matches
.into_iter()
.filter_map(|mat| {
let command = slash_commands.command(&mat.string, cx)?;
let mut new_text = mat.string.clone();
let requires_argument = command.requires_argument();
let accepts_arguments = command.accepts_arguments();
if requires_argument || accepts_arguments {
new_text.push(' ');
}
Some(
matches
.into_iter()
.filter_map(|mat| {
let command = slash_commands.command(&mat.string, cx)?;
let mut new_text = mat.string.clone();
let requires_argument = command.requires_argument();
let accepts_arguments = command.accepts_arguments();
if requires_argument || accepts_arguments {
new_text.push(' ');
}
let confirm =
editor
.clone()
.zip(workspace.clone())
.map(|(editor, workspace)| {
let command_name = mat.string.clone();
let command_range = command_range.clone();
let editor = editor.clone();
let workspace = workspace.clone();
Arc::new(
move |intent: CompletionIntent,
window: &mut Window,
cx: &mut App| {
if !requires_argument
&& (!accepts_arguments || intent.is_complete())
{
editor
.update(cx, |editor, cx| {
editor.run_command(
command_range.clone(),
&command_name,
&[],
true,
workspace.clone(),
window,
cx,
);
})
.ok();
false
} else {
requires_argument || accepts_arguments
}
},
) as Arc<_>
});
Some(project::Completion {
old_range: name_range.clone(),
documentation: Some(CompletionDocumentation::SingleLine(
command.description().into(),
)),
new_text,
label: command.label(cx),
confirm,
source: CompletionSource::Custom,
let confirm =
editor
.clone()
.zip(workspace.clone())
.map(|(editor, workspace)| {
let command_name = mat.string.clone();
let command_range = command_range.clone();
let editor = editor.clone();
let workspace = workspace.clone();
Arc::new(
move |intent: CompletionIntent,
window: &mut Window,
cx: &mut App| {
if !requires_argument
&& (!accepts_arguments || intent.is_complete())
{
editor
.update(cx, |editor, cx| {
editor.run_command(
command_range.clone(),
&command_name,
&[],
true,
workspace.clone(),
window,
cx,
);
})
.ok();
false
} else {
requires_argument || accepts_arguments
}
},
) as Arc<_>
});
Some(project::Completion {
old_range: name_range.clone(),
documentation: Some(CompletionDocumentation::SingleLine(
command.description().into(),
)),
new_text,
label: command.label(cx),
confirm,
source: CompletionSource::Custom,
})
})
})
.collect()
.collect(),
)
})
})
}
@@ -143,7 +145,7 @@ impl SlashCommandCompletionProvider {
last_argument_range: Range<Anchor>,
window: &mut Window,
cx: &mut App,
) -> Task<Result<Vec<project::Completion>>> {
) -> Task<Result<Option<Vec<project::Completion>>>> {
let new_cancel_flag = Arc::new(AtomicBool::new(false));
let mut flag = self.cancel_flag.lock();
flag.store(true, SeqCst);
@@ -161,27 +163,28 @@ impl SlashCommandCompletionProvider {
let workspace = self.workspace.clone();
let arguments = arguments.to_vec();
cx.background_spawn(async move {
Ok(completions
.await?
.into_iter()
.map(|new_argument| {
let confirm =
editor
.clone()
.zip(workspace.clone())
.map(|(editor, workspace)| {
Arc::new({
let mut completed_arguments = arguments.clone();
if new_argument.replace_previous_arguments {
completed_arguments.clear();
} else {
completed_arguments.pop();
}
completed_arguments.push(new_argument.new_text.clone());
Ok(Some(
completions
.await?
.into_iter()
.map(|new_argument| {
let confirm =
editor
.clone()
.zip(workspace.clone())
.map(|(editor, workspace)| {
Arc::new({
let mut completed_arguments = arguments.clone();
if new_argument.replace_previous_arguments {
completed_arguments.clear();
} else {
completed_arguments.pop();
}
completed_arguments.push(new_argument.new_text.clone());
let command_range = command_range.clone();
let command_name = command_name.clone();
move |intent: CompletionIntent,
let command_range = command_range.clone();
let command_name = command_name.clone();
move |intent: CompletionIntent,
window: &mut Window,
cx: &mut App| {
if new_argument.after_completion.run()
@@ -205,31 +208,32 @@ impl SlashCommandCompletionProvider {
!new_argument.after_completion.run()
}
}
}) as Arc<_>
});
}) as Arc<_>
});
let mut new_text = new_argument.new_text.clone();
if new_argument.after_completion == AfterCompletion::Continue {
new_text.push(' ');
}
let mut new_text = new_argument.new_text.clone();
if new_argument.after_completion == AfterCompletion::Continue {
new_text.push(' ');
}
project::Completion {
old_range: if new_argument.replace_previous_arguments {
argument_range.clone()
} else {
last_argument_range.clone()
},
label: new_argument.label,
new_text,
documentation: None,
confirm,
source: CompletionSource::Custom,
}
})
.collect())
project::Completion {
old_range: if new_argument.replace_previous_arguments {
argument_range.clone()
} else {
last_argument_range.clone()
},
label: new_argument.label,
new_text,
documentation: None,
confirm,
source: CompletionSource::Custom,
}
})
.collect(),
))
})
} else {
Task::ready(Ok(Vec::new()))
Task::ready(Ok(Some(Vec::new())))
}
}
}
@@ -242,7 +246,7 @@ impl CompletionProvider for SlashCommandCompletionProvider {
_: editor::CompletionContext,
window: &mut Window,
cx: &mut Context<Editor>,
) -> Task<Result<Vec<project::Completion>>> {
) -> Task<Result<Option<Vec<project::Completion>>>> {
let Some((name, arguments, command_range, last_argument_range)) =
buffer.update(cx, |buffer, _cx| {
let position = buffer_position.to_point(buffer);
@@ -286,7 +290,7 @@ impl CompletionProvider for SlashCommandCompletionProvider {
Some((name, arguments, command_range, last_argument_range))
})
else {
return Task::ready(Ok(Vec::new()));
return Task::ready(Ok(Some(Vec::new())));
};
if let Some((arguments, argument_range)) = arguments {

View File

@@ -16,15 +16,21 @@ anyhow.workspace = true
assistant_tool.workspace = true
chrono.workspace = true
collections.workspace = true
feature_flags.workspace = true
futures.workspace = true
gpui.workspace = true
language.workspace = true
language_model.workspace = true
project.workspace = true
release_channel.workspace = true
schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
theme.workspace = true
ui.workspace = true
util.workspace = true
workspace.workspace = true
settings.workspace = true
[dev-dependencies]
rand.workspace = true

View File

@@ -1,5 +1,6 @@
mod bash_tool;
mod delete_path_tool;
mod diagnostics_tool;
mod edit_files_tool;
mod list_directory_tool;
mod now_tool;
@@ -12,6 +13,7 @@ use gpui::App;
use crate::bash_tool::BashTool;
use crate::delete_path_tool::DeletePathTool;
use crate::diagnostics_tool::DiagnosticsTool;
use crate::edit_files_tool::EditFilesTool;
use crate::list_directory_tool::ListDirectoryTool;
use crate::now_tool::NowTool;
@@ -21,14 +23,16 @@ use crate::regex_search::RegexSearchTool;
pub fn init(cx: &mut App) {
assistant_tool::init(cx);
crate::edit_files_tool::log::init(cx);
let registry = ToolRegistry::global(cx);
registry.register_tool(NowTool);
registry.register_tool(ReadFileTool);
registry.register_tool(ListDirectoryTool);
registry.register_tool(EditFilesTool);
registry.register_tool(PathSearchTool);
registry.register_tool(RegexSearchTool);
registry.register_tool(DeletePathTool);
registry.register_tool(BashTool);
registry.register_tool(DeletePathTool);
registry.register_tool(DiagnosticsTool);
registry.register_tool(EditFilesTool);
registry.register_tool(ListDirectoryTool);
registry.register_tool(NowTool);
registry.register_tool(PathSearchTool);
registry.register_tool(ReadFileTool);
registry.register_tool(RegexSearchTool);
}

View File

@@ -1,4 +1,4 @@
use anyhow::{anyhow, Result};
use anyhow::{anyhow, Context as _, Result};
use assistant_tool::Tool;
use gpui::{App, Entity, Task};
use language_model::LanguageModelRequestMessage;
@@ -6,11 +6,14 @@ use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use util::command::new_smol_command;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct BashToolInput {
/// The bash command to execute as a one-liner.
command: String,
/// Working directory for the command. This must be one of the root directories of the project.
cd: String,
}
pub struct BashTool;
@@ -33,7 +36,7 @@ impl Tool for BashTool {
self: Arc<Self>,
input: serde_json::Value,
_messages: &[LanguageModelRequestMessage],
_project: Entity<Project>,
project: Entity<Project>,
cx: &mut App,
) -> Task<Result<String>> {
let input: BashToolInput = match serde_json::from_value(input) {
@@ -41,23 +44,31 @@ impl Tool for BashTool {
Err(err) => return Task::ready(Err(anyhow!(err))),
};
cx.spawn(|_| async move {
// Add 2>&1 to merge stderr into stdout for proper interleaving
let command = format!("{} 2>&1", input.command);
let Some(worktree) = project.read(cx).worktree_for_root_name(&input.cd, cx) else {
return Task::ready(Err(anyhow!("Working directory not found in the project")));
};
let working_directory = worktree.read(cx).abs_path();
// Spawn a blocking task to execute the command
let output = futures::executor::block_on(async {
std::process::Command::new("bash")
.arg("-c")
.arg(&command)
.output()
.map_err(|err| anyhow!("Failed to execute bash command: {}", err))
})?;
cx.spawn(|_| async move {
// Add 2>&1 to merge stderr into stdout for proper interleaving.
let command = format!("({}) 2>&1", input.command);
let output = new_smol_command("bash")
.arg("-c")
.arg(&command)
.current_dir(working_directory)
.output()
.await
.context("Failed to execute bash command")?;
let output_string = String::from_utf8_lossy(&output.stdout).to_string();
if output.status.success() {
Ok(output_string)
if output_string.is_empty() {
Ok("Command executed successfully.".to_string())
} else {
Ok(output_string)
}
} else {
Ok(format!(
"Command failed with exit code {}\n{}",

View File

@@ -1 +1,7 @@
Executes a bash one-liner and returns the combined output. This tool spawns a bash process, combines stdout and stderr into one interleaved stream as they are produced (preserving the order of writes), and captures that stream into a string which is returned. Use this tool when you need to run shell commands to get information about the system or process files.
Executes a bash one-liner and returns the combined output.
This tool spawns a bash process, combines stdout and stderr into one interleaved stream as they are produced (preserving the order of writes), and captures that stream into a string which is returned.
Make sure you use the `cd` parameter to navigate to one of the root directories of the project. NEVER do it as part of the `command` itself, otherwise it will error.
Remember that each invocation of this tool will spawn a new bash process, so you can't rely on any state from previous invocations.

View File

@@ -0,0 +1,127 @@
use anyhow::{anyhow, Result};
use assistant_tool::Tool;
use gpui::{App, Entity, Task};
use language::{DiagnosticSeverity, OffsetRangeExt};
use language_model::LanguageModelRequestMessage;
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{
fmt::Write,
path::{Path, PathBuf},
sync::Arc,
};
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct DiagnosticsToolInput {
/// The path to get diagnostics for. If not provided, returns a project-wide summary.
///
/// This path should never be absolute, and the first component
/// of the path should always be a root directory in a project.
///
/// <example>
/// If the project has the following root directories:
///
/// - lorem
/// - ipsum
///
/// If you wanna access diagnostics for `dolor.txt` in `ipsum`, you should use the path `ipsum/dolor.txt`.
/// </example>
pub path: Option<PathBuf>,
}
pub struct DiagnosticsTool;
impl Tool for DiagnosticsTool {
fn name(&self) -> String {
"diagnostics".into()
}
fn description(&self) -> String {
include_str!("./diagnostics_tool/description.md").into()
}
fn input_schema(&self) -> serde_json::Value {
let schema = schemars::schema_for!(DiagnosticsToolInput);
serde_json::to_value(&schema).unwrap()
}
fn run(
self: Arc<Self>,
input: serde_json::Value,
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
cx: &mut App,
) -> Task<Result<String>> {
let input = match serde_json::from_value::<DiagnosticsToolInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
};
if let Some(path) = input.path {
let Some(project_path) = project.read(cx).find_project_path(&path, cx) else {
return Task::ready(Err(anyhow!("Could not find path in project")));
};
let buffer = project.update(cx, |project, cx| project.open_buffer(project_path, cx));
cx.spawn(|cx| async move {
let mut output = String::new();
let buffer = buffer.await?;
let snapshot = buffer.read_with(&cx, |buffer, _cx| buffer.snapshot())?;
for (_, group) in snapshot.diagnostic_groups(None) {
let entry = &group.entries[group.primary_ix];
let range = entry.range.to_point(&snapshot);
let severity = match entry.diagnostic.severity {
DiagnosticSeverity::ERROR => "error",
DiagnosticSeverity::WARNING => "warning",
_ => continue,
};
writeln!(
output,
"{} at line {}: {}",
severity,
range.start.row + 1,
entry.diagnostic.message
)?;
}
if output.is_empty() {
Ok("File doesn't have errors or warnings!".to_string())
} else {
Ok(output)
}
})
} else {
let project = project.read(cx);
let mut output = String::new();
let mut has_diagnostics = false;
for (project_path, _, summary) in project.diagnostic_summaries(true, cx) {
if summary.error_count > 0 || summary.warning_count > 0 {
let Some(worktree) = project.worktree_for_id(project_path.worktree_id, cx)
else {
continue;
};
has_diagnostics = true;
output.push_str(&format!(
"{}: {} error(s), {} warning(s)\n",
Path::new(worktree.read(cx).root_name())
.join(project_path.path)
.display(),
summary.error_count,
summary.warning_count
));
}
}
if has_diagnostics {
Task::ready(Ok(output))
} else {
Task::ready(Ok("No errors or warnings found in the project.".to_string()))
}
}
}
}

View File

@@ -0,0 +1,16 @@
Get errors and warnings for the project or a specific file.
This tool can be invoked after a series of edits to determine if further edits are necessary, or if the user asks to fix errors or warnings in their codebase.
When a path is provided, shows all diagnostics for that specific file.
When no path is provided, shows a summary of error and warning counts for all files in the project.
<example>
To get diagnostics for a specific file:
{
"path": "src/main.rs"
}
To get a project-wide diagnostic summary:
{}
</example>

View File

@@ -1,33 +1,60 @@
mod edit_action;
pub mod log;
use anyhow::{anyhow, Context, Result};
use assistant_tool::Tool;
use collections::HashSet;
use edit_action::{EditAction, EditActionParser};
use futures::StreamExt;
use gpui::{App, Entity, Task};
use gpui::{App, AsyncApp, Entity, Task};
use language_model::{
LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role,
};
use project::{Project, ProjectPath};
use log::{EditToolLog, EditToolRequestId};
use project::{search::SearchQuery, Project};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::fmt::Write;
use std::sync::Arc;
use util::paths::PathMatcher;
use util::ResultExt;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct EditFilesToolInput {
/// High-level edit instructions. These will be interpreted by a smaller model,
/// so explain the edits you want that model to make and to which files need changing.
/// The description should be concise and clear. We will show this description to the user
/// as well.
/// High-level edit instructions. These will be interpreted by a smaller
/// model, so explain the changes you want that model to make and which
/// file paths need changing.
///
/// The description should be concise and clear. We will show this
/// description to the user as well.
///
/// WARNING: When specifying which file paths need changing, you MUST
/// start each path with one of the project's root directories.
///
/// WARNING: NEVER include code blocks or snippets in edit instructions.
/// Only provide natural language descriptions of the changes needed! The tool will
/// reject any instructions that contain code blocks or snippets.
///
/// The following examples assume we have two root directories in the project:
/// - root-1
/// - root-2
///
/// <example>
/// If you want to rename a function you can say "Rename the function 'foo' to 'bar'".
/// If you want to introduce a new quit function to kill the process, your
/// instructions should be: "Add a new `quit` function to
/// `root-1/src/main.rs` to kill the process".
///
/// Notice how the file path starts with root-1. Without that, the path
/// would be ambiguous and the call would fail!
/// </example>
///
/// <example>
/// If you want to add a new function you can say "Add a new method to the `User` struct that prints the age".
/// If you want to change documentation to always start with a capital
/// letter, your instructions should be: "In `root-2/db.js`,
/// `root-2/inMemory.js` and `root-2/sql.js`, change all the documentation
/// to start with a capital letter".
///
/// Notice how we never specify code snippets in the instructions!
/// </example>
pub edit_instructions: String,
}
@@ -60,6 +87,65 @@ impl Tool for EditFilesTool {
Err(err) => return Task::ready(Err(anyhow!(err))),
};
match EditToolLog::try_global(cx) {
Some(log) => {
let req_id = log.update(cx, |log, cx| {
log.new_request(input.edit_instructions.clone(), cx)
});
let task =
EditToolRequest::new(input, messages, project, Some((log.clone(), req_id)), cx);
cx.spawn(|mut cx| async move {
let result = task.await;
let str_result = match &result {
Ok(out) => Ok(out.clone()),
Err(err) => Err(err.to_string()),
};
log.update(&mut cx, |log, cx| {
log.set_tool_output(req_id, str_result, cx)
})
.log_err();
result
})
}
None => EditToolRequest::new(input, messages, project, None, cx),
}
}
}
struct EditToolRequest {
parser: EditActionParser,
changed_buffers: HashSet<Entity<language::Buffer>>,
bad_searches: Vec<BadSearch>,
project: Entity<Project>,
log: Option<(Entity<EditToolLog>, EditToolRequestId)>,
}
#[derive(Debug)]
enum DiffResult {
BadSearch(BadSearch),
Diff(language::Diff),
}
#[derive(Debug)]
struct BadSearch {
file_path: String,
search: String,
}
impl EditToolRequest {
fn new(
input: EditFilesToolInput,
messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
log: Option<(Entity<EditToolLog>, EditToolRequestId)>,
cx: &mut App,
) -> Task<Result<String>> {
let model_registry = LanguageModelRegistry::read_global(cx);
let Some(model) = model_registry.editor_model() else {
return Task::ready(Err(anyhow!("No editor model configured")));
@@ -82,110 +168,208 @@ impl Tool for EditFilesTool {
});
cx.spawn(|mut cx| async move {
let request = LanguageModelRequest {
let llm_request = LanguageModelRequest {
messages,
tools: vec![],
stop: vec![],
temperature: None,
temperature: Some(0.0),
};
let mut parser = EditActionParser::new();
let stream = model.stream_completion_text(request, &cx);
let stream = model.stream_completion_text(llm_request, &cx);
let mut chunks = stream.await?;
let mut changed_buffers = HashSet::default();
let mut applied_edits = 0;
while let Some(chunk) = chunks.stream.next().await {
for action in parser.parse_chunk(&chunk?) {
let project_path = project.read_with(&cx, |project, cx| {
let worktree_root_name = action
.file_path()
.components()
.next()
.context("Invalid path")?;
let worktree = project
.worktree_for_root_name(
&worktree_root_name.as_os_str().to_string_lossy(),
cx,
)
.context("Directory not found in project")?;
anyhow::Ok(ProjectPath {
worktree_id: worktree.read(cx).id(),
path: Arc::from(
action.file_path().strip_prefix(worktree_root_name).unwrap(),
),
})
})??;
let buffer = project
.update(&mut cx, |project, cx| project.open_buffer(project_path, cx))?
.await?;
let diff = buffer
.read_with(&cx, |buffer, cx| {
let new_text = match action {
EditAction::Replace { old, new, .. } => {
// TODO: Replace in background?
buffer.text().replace(&old, &new)
}
EditAction::Write { content, .. } => content,
};
buffer.diff(new_text, cx)
})?
.await;
let _clock =
buffer.update(&mut cx, |buffer, cx| buffer.apply_diff(diff, cx))?;
changed_buffers.insert(buffer);
applied_edits += 1;
}
}
let mut answer = match changed_buffers.len() {
0 => "No files were edited.".to_string(),
1 => "Successfully edited ".to_string(),
_ => "Successfully edited these files:\n\n".to_string(),
let mut request = Self {
parser: EditActionParser::new(),
changed_buffers: HashSet::default(),
bad_searches: Vec::new(),
project,
log,
};
// Save each buffer once at the end
for buffer in changed_buffers {
project
.update(&mut cx, |project, cx| {
if let Some(file) = buffer.read(&cx).file() {
let _ = write!(&mut answer, "{}\n\n", &file.path().display());
}
project.save_buffer(buffer, cx)
})?
.await?;
while let Some(chunk) = chunks.stream.next().await {
request.process_response_chunk(&chunk?, &mut cx).await?;
}
let errors = parser.errors();
if errors.is_empty() {
Ok(answer.trim_end().to_string())
} else {
let error_message = errors
.iter()
.map(|e| e.to_string())
.collect::<Vec<_>>()
.join("\n");
if applied_edits > 0 {
Err(anyhow!(
"Applied {} edit(s), but some blocks failed to parse:\n{}",
applied_edits,
error_message
))
} else {
Err(anyhow!(error_message))
}
}
request.finalize(&mut cx).await
})
}
async fn process_response_chunk(&mut self, chunk: &str, cx: &mut AsyncApp) -> Result<()> {
let new_actions = self.parser.parse_chunk(chunk);
if let Some((ref log, req_id)) = self.log {
log.update(cx, |log, cx| {
log.push_editor_response_chunk(req_id, chunk, &new_actions, cx)
})
.log_err();
}
for action in new_actions {
self.apply_action(action, cx).await?;
}
Ok(())
}
async fn apply_action(&mut self, action: EditAction, cx: &mut AsyncApp) -> Result<()> {
let project_path = self.project.read_with(cx, |project, cx| {
project
.find_project_path(action.file_path(), cx)
.context("Path not found in project")
})??;
let buffer = self
.project
.update(cx, |project, cx| project.open_buffer(project_path, cx))?
.await?;
let result = match action {
EditAction::Replace {
old,
new,
file_path,
} => {
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
cx.background_executor()
.spawn(Self::replace_diff(old, new, file_path, snapshot))
.await
}
EditAction::Write { content, .. } => Ok(DiffResult::Diff(
buffer
.read_with(cx, |buffer, cx| buffer.diff(content, cx))?
.await,
)),
}?;
match result {
DiffResult::BadSearch(invalid_replace) => {
self.bad_searches.push(invalid_replace);
}
DiffResult::Diff(diff) => {
let _clock = buffer.update(cx, |buffer, cx| buffer.apply_diff(diff, cx))?;
self.changed_buffers.insert(buffer);
}
}
Ok(())
}
async fn replace_diff(
old: String,
new: String,
file_path: std::path::PathBuf,
snapshot: language::BufferSnapshot,
) -> Result<DiffResult> {
let query = SearchQuery::text(
old.clone(),
false,
true,
true,
PathMatcher::new(&[])?,
PathMatcher::new(&[])?,
None,
)?;
let matches = query.search(&snapshot, None).await;
if matches.is_empty() {
return Ok(DiffResult::BadSearch(BadSearch {
search: new.clone(),
file_path: file_path.display().to_string(),
}));
}
let edit_range = matches[0].clone();
let diff = language::text_diff(&old, &new);
let edits = diff
.into_iter()
.map(|(old_range, text)| {
let start = edit_range.start + old_range.start;
let end = edit_range.start + old_range.end;
(start..end, text)
})
.collect::<Vec<_>>();
let diff = language::Diff {
base_version: snapshot.version().clone(),
line_ending: snapshot.line_ending(),
edits,
};
anyhow::Ok(DiffResult::Diff(diff))
}
async fn finalize(self, cx: &mut AsyncApp) -> Result<String> {
let mut answer = match self.changed_buffers.len() {
0 => "No files were edited.".to_string(),
1 => "Successfully edited ".to_string(),
_ => "Successfully edited these files:\n\n".to_string(),
};
// Save each buffer once at the end
for buffer in self.changed_buffers {
let (path, save_task) = self.project.update(cx, |project, cx| {
let path = buffer
.read(cx)
.file()
.map(|file| file.path().display().to_string());
let task = project.save_buffer(buffer.clone(), cx);
(path, task)
})?;
save_task.await?;
if let Some(path) = path {
writeln!(&mut answer, "{}", path)?;
}
}
let errors = self.parser.errors();
if errors.is_empty() && self.bad_searches.is_empty() {
Ok(answer.trim_end().to_string())
} else {
if !self.bad_searches.is_empty() {
writeln!(
&mut answer,
"\nThese searches failed because they didn't match any strings:"
)?;
for replace in self.bad_searches {
writeln!(
&mut answer,
"- '{}' does not appear in `{}`",
replace.search.replace("\r", "\\r").replace("\n", "\\n"),
replace.file_path
)?;
}
writeln!(&mut answer, "Make sure to use exact searches.")?;
}
if !errors.is_empty() {
writeln!(
&mut answer,
"\nThese SEARCH/REPLACE blocks failed to parse:"
)?;
for error in errors {
writeln!(&mut answer, "- {}", error)?;
}
}
writeln!(
&mut answer,
"\nYou can fix errors by running the tool again. You can include instructions,\
but errors are part of the conversation so you don't need to repeat them."
)?;
Err(anyhow!(answer))
}
}
}

View File

@@ -1,4 +1,4 @@
Edit files in the current project.
Edit files in the current project by specifying instructions in natural language.
When using this tool, you should suggest one coherent edit that can be made to the codebase.

View File

@@ -106,7 +106,7 @@ Every *SEARCH/REPLACE block* must use this format:
7. The end of the replace block: >>>>>>> REPLACE
8. The closing fence: ```
Use the *FULL* file path, as shown to you by the user.
Use the *FULL* file path, as shown to you by the user. Make sure to include the project's root directory name at the start of the path. *NEVER* specify the absolute path of the file!
Every *SEARCH* section must *EXACTLY MATCH* the existing file content, character for character, including all comments, docstrings, etc.
If the file contains code or other data wrapped/escaped in json/xml/quotes or other containers, you need to propose edits to the literal contents of the file, including the container markup.

View File

@@ -0,0 +1,415 @@
use std::path::Path;
use collections::HashSet;
use feature_flags::FeatureFlagAppExt;
use gpui::{
actions, list, prelude::*, App, Empty, Entity, EventEmitter, FocusHandle, Focusable, Global,
ListAlignment, ListState, SharedString, Subscription, Window,
};
use release_channel::ReleaseChannel;
use settings::Settings;
use ui::prelude::*;
use workspace::{item::ItemEvent, Item, Workspace, WorkspaceId};
use super::edit_action::EditAction;
actions!(debug, [EditTool]);
pub fn init(cx: &mut App) {
if cx.is_staff() || ReleaseChannel::global(cx) == ReleaseChannel::Dev {
// Track events even before opening the log
EditToolLog::global(cx);
}
cx.observe_new(|workspace: &mut Workspace, _, _| {
workspace.register_action(|workspace, _: &EditTool, window, cx| {
let viewer = cx.new(EditToolLogViewer::new);
workspace.add_item_to_active_pane(Box::new(viewer), None, true, window, cx)
});
})
.detach();
}
pub struct GlobalEditToolLog(Entity<EditToolLog>);
impl Global for GlobalEditToolLog {}
#[derive(Default)]
pub struct EditToolLog {
requests: Vec<EditToolRequest>,
}
#[derive(Clone, Copy, Hash, Eq, PartialEq)]
pub struct EditToolRequestId(u32);
impl EditToolLog {
pub fn global(cx: &mut App) -> Entity<Self> {
match Self::try_global(cx) {
Some(entity) => entity,
None => {
let entity = cx.new(|_cx| Self::default());
cx.set_global(GlobalEditToolLog(entity.clone()));
entity
}
}
}
pub fn try_global(cx: &App) -> Option<Entity<Self>> {
cx.try_global::<GlobalEditToolLog>()
.map(|log| log.0.clone())
}
pub fn new_request(
&mut self,
instructions: String,
cx: &mut Context<Self>,
) -> EditToolRequestId {
let id = EditToolRequestId(self.requests.len() as u32);
self.requests.push(EditToolRequest {
id,
instructions,
editor_response: None,
tool_output: None,
parsed_edits: Vec::new(),
});
cx.emit(EditToolLogEvent::Inserted);
id
}
pub fn push_editor_response_chunk(
&mut self,
id: EditToolRequestId,
chunk: &str,
new_actions: &[EditAction],
cx: &mut Context<Self>,
) {
if let Some(request) = self.requests.get_mut(id.0 as usize) {
match &mut request.editor_response {
None => {
request.editor_response = Some(chunk.to_string());
}
Some(response) => {
response.push_str(chunk);
}
}
request.parsed_edits.extend(new_actions.iter().cloned());
cx.emit(EditToolLogEvent::Updated);
}
}
pub fn set_tool_output(
&mut self,
id: EditToolRequestId,
tool_output: Result<String, String>,
cx: &mut Context<Self>,
) {
if let Some(request) = self.requests.get_mut(id.0 as usize) {
request.tool_output = Some(tool_output);
cx.emit(EditToolLogEvent::Updated);
}
}
}
enum EditToolLogEvent {
Inserted,
Updated,
}
impl EventEmitter<EditToolLogEvent> for EditToolLog {}
pub struct EditToolRequest {
id: EditToolRequestId,
instructions: String,
// we don't use a result here because the error might have occurred after we got a response
editor_response: Option<String>,
parsed_edits: Vec<EditAction>,
tool_output: Option<Result<String, String>>,
}
pub struct EditToolLogViewer {
focus_handle: FocusHandle,
log: Entity<EditToolLog>,
list_state: ListState,
expanded_edits: HashSet<(EditToolRequestId, usize)>,
_subscription: Subscription,
}
impl EditToolLogViewer {
pub fn new(cx: &mut Context<Self>) -> Self {
let log = EditToolLog::global(cx);
let subscription = cx.subscribe(&log, Self::handle_log_event);
Self {
focus_handle: cx.focus_handle(),
log: log.clone(),
list_state: ListState::new(
log.read(cx).requests.len(),
ListAlignment::Bottom,
px(1024.),
{
let this = cx.entity().downgrade();
move |ix, window: &mut Window, cx: &mut App| {
this.update(cx, |this, cx| this.render_request(ix, window, cx))
.unwrap()
}
},
),
expanded_edits: HashSet::default(),
_subscription: subscription,
}
}
fn handle_log_event(
&mut self,
_: Entity<EditToolLog>,
event: &EditToolLogEvent,
cx: &mut Context<Self>,
) {
match event {
EditToolLogEvent::Inserted => {
let count = self.list_state.item_count();
self.list_state.splice(count..count, 1);
}
EditToolLogEvent::Updated => {}
}
cx.notify();
}
fn render_request(
&self,
index: usize,
_window: &mut Window,
cx: &mut Context<Self>,
) -> AnyElement {
let requests = &self.log.read(cx).requests;
let request = &requests[index];
v_flex()
.gap_3()
.child(Self::render_section(IconName::ArrowRight, "Tool Input"))
.child(request.instructions.clone())
.py_5()
.when(index + 1 < requests.len(), |element| {
element
.border_b_1()
.border_color(cx.theme().colors().border)
})
.map(|parent| match &request.editor_response {
None => {
if request.tool_output.is_none() {
parent.child("...")
} else {
parent
}
}
Some(response) => parent
.child(Self::render_section(
IconName::ZedAssistant,
"Editor Response",
))
.child(Label::new(response.clone()).buffer_font(cx)),
})
.when(!request.parsed_edits.is_empty(), |parent| {
parent
.child(Self::render_section(IconName::Microscope, "Parsed Edits"))
.child(
v_flex()
.gap_2()
.children(request.parsed_edits.iter().enumerate().map(
|(index, edit)| {
self.render_edit_action(edit, request.id, index, cx)
},
)),
)
})
.when_some(request.tool_output.as_ref(), |parent, output| {
parent
.child(Self::render_section(IconName::ArrowLeft, "Tool Output"))
.child(match output {
Ok(output) => Label::new(output.clone()).color(Color::Success),
Err(error) => Label::new(error.clone()).color(Color::Error),
})
})
.into_any()
}
fn render_section(icon: IconName, title: &'static str) -> AnyElement {
h_flex()
.gap_1()
.child(Icon::new(icon).color(Color::Muted))
.child(Label::new(title).size(LabelSize::Small).color(Color::Muted))
.into_any()
}
fn render_edit_action(
&self,
edit_action: &EditAction,
request_id: EditToolRequestId,
index: usize,
cx: &Context<Self>,
) -> AnyElement {
let expanded_id = (request_id, index);
match edit_action {
EditAction::Replace {
file_path,
old,
new,
} => self
.render_edit_action_container(
expanded_id,
&file_path,
[
Self::render_block(IconName::MagnifyingGlass, "Search", old.clone(), cx)
.border_r_1()
.border_color(cx.theme().colors().border)
.into_any(),
Self::render_block(IconName::Replace, "Replace", new.clone(), cx)
.into_any(),
],
cx,
)
.into_any(),
EditAction::Write { file_path, content } => self
.render_edit_action_container(
expanded_id,
&file_path,
[
Self::render_block(IconName::Pencil, "Write", content.clone(), cx)
.into_any(),
],
cx,
)
.into_any(),
}
}
fn render_edit_action_container(
&self,
expanded_id: (EditToolRequestId, usize),
file_path: &Path,
content: impl IntoIterator<Item = AnyElement>,
cx: &Context<Self>,
) -> AnyElement {
let is_expanded = self.expanded_edits.contains(&expanded_id);
v_flex()
.child(
h_flex()
.bg(cx.theme().colors().element_background)
.border_1()
.border_color(cx.theme().colors().border)
.rounded_t_md()
.when(!is_expanded, |el| el.rounded_b_md())
.py_1()
.px_2()
.gap_1()
.child(
ui::Disclosure::new(ElementId::Integer(expanded_id.1), is_expanded)
.on_click(cx.listener(move |this, _ev, _window, cx| {
if is_expanded {
this.expanded_edits.remove(&expanded_id);
} else {
this.expanded_edits.insert(expanded_id);
}
cx.notify();
})),
)
.child(Label::new(file_path.display().to_string()).size(LabelSize::Small)),
)
.child(if is_expanded {
h_flex()
.border_1()
.border_t_0()
.border_color(cx.theme().colors().border)
.rounded_b_md()
.children(content)
.into_any()
} else {
Empty.into_any()
})
.into_any()
}
fn render_block(icon: IconName, title: &'static str, content: String, cx: &App) -> Div {
v_flex()
.p_1()
.gap_1()
.flex_1()
.h_full()
.child(
h_flex()
.gap_1()
.child(Icon::new(icon).color(Color::Muted))
.child(Label::new(title).size(LabelSize::Small).color(Color::Muted)),
)
.font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
.text_sm()
.child(content)
.child(div().flex_1())
}
}
impl EventEmitter<()> for EditToolLogViewer {}
impl Focusable for EditToolLogViewer {
fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
self.focus_handle.clone()
}
}
impl Item for EditToolLogViewer {
type Event = ();
fn to_item_events(_: &Self::Event, _: impl FnMut(ItemEvent)) {}
fn tab_content_text(&self, _window: &Window, _cx: &App) -> Option<SharedString> {
Some("Edit Tool Log".into())
}
fn telemetry_event_text(&self) -> Option<&'static str> {
None
}
fn clone_on_split(
&self,
_workspace_id: Option<WorkspaceId>,
_window: &mut Window,
cx: &mut Context<Self>,
) -> Option<Entity<Self>>
where
Self: Sized,
{
Some(cx.new(Self::new))
}
}
impl Render for EditToolLogViewer {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
if self.list_state.item_count() == 0 {
return v_flex()
.justify_center()
.size_full()
.gap_1()
.bg(cx.theme().colors().editor_background)
.text_center()
.text_lg()
.child("No requests yet")
.child(
div()
.text_ui(cx)
.child("Go ask the assistant to perform some edits"),
);
}
v_flex()
.p_4()
.bg(cx.theme().colors().editor_background)
.size_full()
.child(list(self.list_state.clone()).flex_grow())
}
}

View File

@@ -12,10 +12,10 @@ pub struct ListDirectoryToolInput {
/// The relative path of the directory to list.
///
/// This path should never be absolute, and the first component
/// of the path should always be a top-level directory in a project.
/// of the path should always be a root directory in a project.
///
/// <example>
/// If the project has the following top-level directories:
/// If the project has the following root directories:
///
/// - directory1
/// - directory2
@@ -24,7 +24,7 @@ pub struct ListDirectoryToolInput {
/// </example>
///
/// <example>
/// If the project has the following top-level directories:
/// If the project has the following root directories:
///
/// - foo
/// - bar
@@ -62,27 +62,37 @@ impl Tool for ListDirectoryTool {
Err(err) => return Task::ready(Err(anyhow!(err))),
};
let Some(worktree_root_name) = input.path.components().next() else {
return Task::ready(Err(anyhow!("Invalid path")));
let Some(project_path) = project.read(cx).find_project_path(&input.path, cx) else {
return Task::ready(Err(anyhow!("Path not found in project")));
};
let Some(worktree) = project
.read(cx)
.worktree_for_root_name(&worktree_root_name.as_os_str().to_string_lossy(), cx)
.worktree_for_id(project_path.worktree_id, cx)
else {
return Task::ready(Err(anyhow!("Directory not found in the project")));
return Task::ready(Err(anyhow!("Worktree not found")));
};
let path = input.path.strip_prefix(worktree_root_name).unwrap();
let worktree = worktree.read(cx);
let Some(entry) = worktree.entry_for_path(&project_path.path) else {
return Task::ready(Err(anyhow!("Path not found: {}", input.path.display())));
};
if !entry.is_dir() {
return Task::ready(Err(anyhow!("{} is a file.", input.path.display())));
}
let mut output = String::new();
for entry in worktree.read(cx).child_entries(path) {
for entry in worktree.child_entries(&project_path.path) {
writeln!(
output,
"{}",
Path::new(worktree_root_name.as_os_str())
.join(&entry.path)
.display(),
Path::new(worktree.root_name()).join(&entry.path).display(),
)
.unwrap();
}
if output.is_empty() {
return Task::ready(Ok(format!("{} is empty.", input.path.display())));
}
Task::ready(Ok(output))
}
}

View File

@@ -13,7 +13,7 @@ pub struct PathSearchToolInput {
/// The glob to search all project paths for.
///
/// <example>
/// If the project has the following top-level directories:
/// If the project has the following root directories:
///
/// - directory1/a/something.txt
/// - directory2/a/things.txt

View File

@@ -5,7 +5,7 @@ use anyhow::{anyhow, Result};
use assistant_tool::Tool;
use gpui::{App, Entity, Task};
use language_model::LanguageModelRequestMessage;
use project::{Project, ProjectPath};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -14,10 +14,10 @@ pub struct ReadFileToolInput {
/// The relative path of the file to read.
///
/// This path should never be absolute, and the first component
/// of the path should always be a top-level directory in a project.
/// of the path should always be a root directory in a project.
///
/// <example>
/// If the project has the following top-level directories:
/// If the project has the following root directories:
///
/// - directory1
/// - directory2
@@ -56,18 +56,8 @@ impl Tool for ReadFileTool {
Err(err) => return Task::ready(Err(anyhow!(err))),
};
let Some(worktree_root_name) = input.path.components().next() else {
return Task::ready(Err(anyhow!("Invalid path")));
};
let Some(worktree) = project
.read(cx)
.worktree_for_root_name(&worktree_root_name.as_os_str().to_string_lossy(), cx)
else {
return Task::ready(Err(anyhow!("Directory not found in the project")));
};
let project_path = ProjectPath {
worktree_id: worktree.read(cx).id(),
path: Arc::from(input.path.strip_prefix(worktree_root_name).unwrap()),
let Some(project_path) = project.read(cx).find_project_path(&input.path, cx) else {
return Task::ready(Err(anyhow!("Path not found in project")));
};
cx.spawn(|cx| async move {
let buffer = cx

View File

@@ -93,7 +93,7 @@ fn view_release_notes_locally(
let tab_description = SharedString::from(body.title.to_string());
let editor = cx.new(|cx| {
Editor::for_multibuffer(buffer, Some(project), true, window, cx)
Editor::for_multibuffer(buffer, Some(project), window, cx)
});
let workspace_handle = workspace.weak_handle();
let markdown_preview: Entity<MarkdownPreviewView> =

View File

@@ -22,6 +22,7 @@ git2.workspace = true
gpui.workspace = true
language.workspace = true
log.workspace = true
pretty_assertions.workspace = true
rope.workspace = true
sum_tree.workspace = true
text.workspace = true
@@ -31,7 +32,6 @@ util.workspace = true
ctor.workspace = true
env_logger.workspace = true
gpui = { workspace = true, features = ["test-support"] }
pretty_assertions.workspace = true
rand.workspace = true
serde_json.workspace = true
text = { workspace = true, features = ["test-support"] }

View File

@@ -6,9 +6,9 @@ use rope::Rope;
use std::cmp::Ordering;
use std::mem;
use std::{future::Future, iter, ops::Range, sync::Arc};
use sum_tree::{SumTree, TreeMap};
use text::ToOffset as _;
use sum_tree::SumTree;
use text::{Anchor, Bias, BufferId, OffsetRangeExt, Point};
use text::{AnchorRangeExt, ToOffset as _};
use util::ResultExt;
pub struct BufferDiff {
@@ -26,7 +26,7 @@ pub struct BufferDiffSnapshot {
#[derive(Clone)]
struct BufferDiffInner {
hunks: SumTree<InternalDiffHunk>,
pending_hunks: TreeMap<usize, PendingHunk>,
pending_hunks: SumTree<PendingHunk>,
base_text: language::BufferSnapshot,
base_text_exists: bool,
}
@@ -48,7 +48,7 @@ pub enum DiffHunkStatusKind {
pub enum DiffHunkSecondaryStatus {
HasSecondaryHunk,
OverlapsWithSecondaryHunk,
None,
NoSecondaryHunk,
SecondaryHunkAdditionPending,
SecondaryHunkRemovalPending,
}
@@ -74,6 +74,8 @@ struct InternalDiffHunk {
#[derive(Debug, Clone, PartialEq, Eq)]
struct PendingHunk {
buffer_range: Range<Anchor>,
diff_base_byte_range: Range<usize>,
buffer_version: clock::Global,
new_status: DiffHunkSecondaryStatus,
}
@@ -93,6 +95,16 @@ impl sum_tree::Item for InternalDiffHunk {
}
}
impl sum_tree::Item for PendingHunk {
type Summary = DiffHunkSummary;
fn summary(&self, _cx: &text::BufferSnapshot) -> Self::Summary {
DiffHunkSummary {
buffer_range: self.buffer_range.clone(),
}
}
}
impl sum_tree::Summary for DiffHunkSummary {
type Context = text::BufferSnapshot;
@@ -176,6 +188,7 @@ impl BufferDiffSnapshot {
}
impl BufferDiffInner {
/// Returns the new index text and new pending hunks.
fn stage_or_unstage_hunks(
&mut self,
unstaged_diff: &Self,
@@ -183,7 +196,7 @@ impl BufferDiffInner {
hunks: &[DiffHunk],
buffer: &text::BufferSnapshot,
file_exists: bool,
) -> (Option<Rope>, Vec<(usize, PendingHunk)>) {
) -> (Option<Rope>, SumTree<PendingHunk>) {
let head_text = self
.base_text_exists
.then(|| self.base_text.as_rope().clone());
@@ -195,41 +208,41 @@ impl BufferDiffInner {
// entire file must be either created or deleted in the index.
let (index_text, head_text) = match (index_text, head_text) {
(Some(index_text), Some(head_text)) if file_exists || !stage => (index_text, head_text),
(_, head_text @ _) => {
if stage {
(index_text, head_text) => {
let (rope, new_status) = if stage {
log::debug!("stage all");
return (
(
file_exists.then(|| buffer.as_rope().clone()),
vec![(
0,
PendingHunk {
buffer_version: buffer.version().clone(),
new_status: DiffHunkSecondaryStatus::SecondaryHunkRemovalPending,
},
)],
);
DiffHunkSecondaryStatus::SecondaryHunkRemovalPending,
)
} else {
log::debug!("unstage all");
return (
(
head_text,
vec![(
0,
PendingHunk {
buffer_version: buffer.version().clone(),
new_status: DiffHunkSecondaryStatus::SecondaryHunkAdditionPending,
},
)],
);
}
DiffHunkSecondaryStatus::SecondaryHunkAdditionPending,
)
};
let hunk = PendingHunk {
buffer_range: Anchor::MIN..Anchor::MAX,
diff_base_byte_range: 0..index_text.map_or(0, |rope| rope.len()),
buffer_version: buffer.version().clone(),
new_status,
};
let tree = SumTree::from_item(hunk, buffer);
return (rope, tree);
}
};
let mut unstaged_hunk_cursor = unstaged_diff.hunks.cursor::<DiffHunkSummary>(buffer);
unstaged_hunk_cursor.next(buffer);
let mut edits = Vec::new();
let mut pending_hunks = Vec::new();
let mut prev_unstaged_hunk_buffer_offset = 0;
let mut prev_unstaged_hunk_base_text_offset = 0;
let mut pending_hunks = SumTree::new(buffer);
let mut old_pending_hunks = unstaged_diff
.pending_hunks
.cursor::<DiffHunkSummary>(buffer);
// first, merge new hunks into pending_hunks
for DiffHunk {
buffer_range,
diff_base_byte_range,
@@ -237,12 +250,58 @@ impl BufferDiffInner {
..
} in hunks.iter().cloned()
{
if (stage && secondary_status == DiffHunkSecondaryStatus::None)
let preceding_pending_hunks =
old_pending_hunks.slice(&buffer_range.start, Bias::Left, buffer);
pending_hunks.append(preceding_pending_hunks, buffer);
// skip all overlapping old pending hunks
while old_pending_hunks
.item()
.is_some_and(|preceding_pending_hunk_item| {
preceding_pending_hunk_item
.buffer_range
.overlaps(&buffer_range, buffer)
})
{
old_pending_hunks.next(buffer);
}
// merge into pending hunks
if (stage && secondary_status == DiffHunkSecondaryStatus::NoSecondaryHunk)
|| (!stage && secondary_status == DiffHunkSecondaryStatus::HasSecondaryHunk)
{
continue;
}
pending_hunks.push(
PendingHunk {
buffer_range,
diff_base_byte_range,
buffer_version: buffer.version().clone(),
new_status: if stage {
DiffHunkSecondaryStatus::SecondaryHunkRemovalPending
} else {
DiffHunkSecondaryStatus::SecondaryHunkAdditionPending
},
},
buffer,
);
}
// append the remainder
pending_hunks.append(old_pending_hunks.suffix(buffer), buffer);
let mut prev_unstaged_hunk_buffer_offset = 0;
let mut prev_unstaged_hunk_base_text_offset = 0;
let mut edits = Vec::<(Range<usize>, String)>::new();
// then, iterate over all pending hunks (both new ones and the existing ones) and compute the edits
for PendingHunk {
buffer_range,
diff_base_byte_range,
..
} in pending_hunks.iter().cloned()
{
let skipped_hunks = unstaged_hunk_cursor.slice(&buffer_range.start, Bias::Left, buffer);
if let Some(secondary_hunk) = skipped_hunks.last() {
@@ -294,22 +353,15 @@ impl BufferDiffInner {
.chunks_in_range(diff_base_byte_range.clone())
.collect::<String>()
};
pending_hunks.push((
diff_base_byte_range.start,
PendingHunk {
buffer_version: buffer.version().clone(),
new_status: if stage {
DiffHunkSecondaryStatus::SecondaryHunkRemovalPending
} else {
DiffHunkSecondaryStatus::SecondaryHunkAdditionPending
},
},
));
edits.push((index_range, replacement_text));
}
debug_assert!(edits.iter().is_sorted_by_key(|(range, _)| range.start));
let mut new_index_text = Rope::new();
let mut index_cursor = index_text.cursor(0);
for (old_range, replacement_text) in edits {
new_index_text.append(index_cursor.slice(old_range.start));
index_cursor.seek_forward(old_range.end);
@@ -354,12 +406,14 @@ impl BufferDiffInner {
});
let mut secondary_cursor = None;
let mut pending_hunks = TreeMap::default();
let mut pending_hunks_cursor = None;
if let Some(secondary) = secondary.as_ref() {
let mut cursor = secondary.hunks.cursor::<DiffHunkSummary>(buffer);
cursor.next(buffer);
secondary_cursor = Some(cursor);
pending_hunks = secondary.pending_hunks.clone();
let mut cursor = secondary.pending_hunks.cursor::<DiffHunkSummary>(buffer);
cursor.next(buffer);
pending_hunks_cursor = Some(cursor);
}
let max_point = buffer.max_point();
@@ -378,16 +432,33 @@ impl BufferDiffInner {
end_anchor = buffer.anchor_before(end_point);
}
let mut secondary_status = DiffHunkSecondaryStatus::None;
let mut secondary_status = DiffHunkSecondaryStatus::NoSecondaryHunk;
let mut has_pending = false;
if let Some(pending_hunk) = pending_hunks.get(&start_base) {
if !buffer.has_edits_since_in_range(
&pending_hunk.buffer_version,
start_anchor..end_anchor,
) {
has_pending = true;
secondary_status = pending_hunk.new_status;
if let Some(pending_cursor) = pending_hunks_cursor.as_mut() {
if start_anchor
.cmp(&pending_cursor.start().buffer_range.start, buffer)
.is_gt()
{
pending_cursor.seek_forward(&start_anchor, Bias::Left, buffer);
}
if let Some(pending_hunk) = pending_cursor.item() {
let mut pending_range = pending_hunk.buffer_range.to_point(buffer);
if pending_range.end.column > 0 {
pending_range.end.row += 1;
pending_range.end.column = 0;
}
if pending_range == (start_point..end_point) {
if !buffer.has_edits_since_in_range(
&pending_hunk.buffer_version,
start_anchor..end_anchor,
) {
has_pending = true;
secondary_status = pending_hunk.new_status;
}
}
}
}
@@ -449,7 +520,7 @@ impl BufferDiffInner {
diff_base_byte_range: hunk.diff_base_byte_range.clone(),
buffer_range: hunk.buffer_range.clone(),
// The secondary status is not used by callers of this method.
secondary_status: DiffHunkSecondaryStatus::None,
secondary_status: DiffHunkSecondaryStatus::NoSecondaryHunk,
})
})
}
@@ -724,7 +795,7 @@ impl BufferDiff {
base_text,
hunks,
base_text_exists,
pending_hunks: TreeMap::default(),
pending_hunks: SumTree::new(&buffer),
}
}
}
@@ -740,8 +811,8 @@ impl BufferDiff {
cx.background_spawn(async move {
BufferDiffInner {
base_text: base_text_snapshot,
pending_hunks: SumTree::new(&buffer),
hunks: compute_hunks(base_text_pair, buffer),
pending_hunks: TreeMap::default(),
base_text_exists,
}
})
@@ -751,7 +822,7 @@ impl BufferDiff {
BufferDiffInner {
base_text: language::Buffer::build_empty_snapshot(cx),
hunks: SumTree::new(buffer),
pending_hunks: TreeMap::default(),
pending_hunks: SumTree::new(buffer),
base_text_exists: false,
}
}
@@ -767,7 +838,7 @@ impl BufferDiff {
pub fn clear_pending_hunks(&mut self, cx: &mut Context<Self>) {
if let Some(secondary_diff) = &self.secondary_diff {
secondary_diff.update(cx, |diff, _| {
diff.inner.pending_hunks.clear();
diff.inner.pending_hunks = SumTree::from_summary(DiffHunkSummary::default());
});
cx.emit(BufferDiffEvent::DiffChanged {
changed_range: Some(Anchor::MIN..Anchor::MAX),
@@ -783,18 +854,17 @@ impl BufferDiff {
file_exists: bool,
cx: &mut Context<Self>,
) -> Option<Rope> {
let (new_index_text, pending_hunks) = self.inner.stage_or_unstage_hunks(
let (new_index_text, new_pending_hunks) = self.inner.stage_or_unstage_hunks(
&self.secondary_diff.as_ref()?.read(cx).inner,
stage,
&hunks,
buffer,
file_exists,
);
if let Some(unstaged_diff) = &self.secondary_diff {
unstaged_diff.update(cx, |diff, _| {
for (offset, pending_hunk) in pending_hunks {
diff.inner.pending_hunks.insert(offset, pending_hunk);
}
diff.inner.pending_hunks = new_pending_hunks;
});
}
cx.emit(BufferDiffEvent::HunksStagedOrUnstaged(
@@ -916,7 +986,9 @@ impl BufferDiff {
}
_ => (true, Some(text::Anchor::MIN..text::Anchor::MAX)),
};
let pending_hunks = mem::take(&mut self.inner.pending_hunks);
let pending_hunks = mem::replace(&mut self.inner.pending_hunks, SumTree::new(buffer));
self.inner = new_state;
if !base_text_changed {
self.inner.pending_hunks = pending_hunks;
@@ -1149,21 +1221,21 @@ impl DiffHunkStatus {
pub fn deleted_none() -> Self {
Self {
kind: DiffHunkStatusKind::Deleted,
secondary: DiffHunkSecondaryStatus::None,
secondary: DiffHunkSecondaryStatus::NoSecondaryHunk,
}
}
pub fn added_none() -> Self {
Self {
kind: DiffHunkStatusKind::Added,
secondary: DiffHunkSecondaryStatus::None,
secondary: DiffHunkSecondaryStatus::NoSecondaryHunk,
}
}
pub fn modified_none() -> Self {
Self {
kind: DiffHunkStatusKind::Modified,
secondary: DiffHunkSecondaryStatus::None,
secondary: DiffHunkSecondaryStatus::NoSecondaryHunk,
}
}
}
@@ -1171,13 +1243,14 @@ impl DiffHunkStatus {
/// Range (crossing new lines), old, new
#[cfg(any(test, feature = "test-support"))]
#[track_caller]
pub fn assert_hunks<Iter>(
diff_hunks: Iter,
pub fn assert_hunks<ExpectedText, HunkIter>(
diff_hunks: HunkIter,
buffer: &text::BufferSnapshot,
diff_base: &str,
expected_hunks: &[(Range<u32>, &str, &str, DiffHunkStatus)],
expected_hunks: &[(Range<u32>, ExpectedText, ExpectedText, DiffHunkStatus)],
) where
Iter: Iterator<Item = DiffHunk>,
HunkIter: Iterator<Item = DiffHunk>,
ExpectedText: AsRef<str>,
{
let actual_hunks = diff_hunks
.map(|hunk| {
@@ -1197,14 +1270,14 @@ pub fn assert_hunks<Iter>(
.map(|(r, old_text, new_text, status)| {
(
Point::new(r.start, 0)..Point::new(r.end, 0),
*old_text,
new_text.to_string(),
old_text.as_ref(),
new_text.as_ref().to_string(),
*status,
)
})
.collect();
assert_eq!(actual_hunks, expected_hunks);
pretty_assertions::assert_eq!(actual_hunks, expected_hunks);
}
#[cfg(test)]
@@ -1263,7 +1336,7 @@ mod tests {
);
diff = cx.update(|cx| BufferDiff::build_empty(&buffer, cx));
assert_hunks(
assert_hunks::<&str, _>(
diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer, None),
&buffer,
&diff_base,
@@ -1601,7 +1674,10 @@ mod tests {
.hunks_intersecting_range(hunk_range.clone(), &buffer, &cx)
.collect::<Vec<_>>();
for hunk in &hunks {
assert_ne!(hunk.secondary_status, DiffHunkSecondaryStatus::None)
assert_ne!(
hunk.secondary_status,
DiffHunkSecondaryStatus::NoSecondaryHunk
)
}
let new_index_text = diff
@@ -1880,10 +1956,10 @@ mod tests {
let hunk_to_change = hunk.clone();
let stage = match hunk.secondary_status {
DiffHunkSecondaryStatus::HasSecondaryHunk => {
hunk.secondary_status = DiffHunkSecondaryStatus::None;
hunk.secondary_status = DiffHunkSecondaryStatus::NoSecondaryHunk;
true
}
DiffHunkSecondaryStatus::None => {
DiffHunkSecondaryStatus::NoSecondaryHunk => {
hunk.secondary_status = DiffHunkSecondaryStatus::HasSecondaryHunk;
false
}

View File

@@ -198,6 +198,8 @@ fn main() -> Result<()> {
let mut paths = vec![];
let mut urls = vec![];
let mut stdin_tmp_file: Option<fs::File> = None;
let mut anonymous_fd_tmp_files = vec![];
for path in args.paths_with_position.iter() {
if path.starts_with("zed://")
|| path.starts_with("http://")
@@ -211,6 +213,11 @@ fn main() -> Result<()> {
paths.push(file.path().to_string_lossy().to_string());
let (file, _) = file.keep()?;
stdin_tmp_file = Some(file);
} else if let Some(file) = anonymous_fd(path) {
let tmp_file = NamedTempFile::new()?;
paths.push(tmp_file.path().to_string_lossy().to_string());
let (tmp_file, _) = tmp_file.keep()?;
anonymous_fd_tmp_files.push((file, tmp_file));
} else {
paths.push(parse_path_with_position(path)?)
}
@@ -252,31 +259,33 @@ fn main() -> Result<()> {
}
});
let pipe_handle: JoinHandle<anyhow::Result<()>> = thread::spawn(move || {
if let Some(mut tmp_file) = stdin_tmp_file {
let mut stdin = std::io::stdin().lock();
if io::IsTerminal::is_terminal(&stdin) {
return Ok(());
}
let mut buffer = [0; 8 * 1024];
loop {
let bytes_read = io::Read::read(&mut stdin, &mut buffer)?;
if bytes_read == 0 {
break;
let stdin_pipe_handle: Option<JoinHandle<anyhow::Result<()>>> =
stdin_tmp_file.map(|tmp_file| {
thread::spawn(move || {
let stdin = std::io::stdin().lock();
if io::IsTerminal::is_terminal(&stdin) {
return Ok(());
}
io::Write::write(&mut tmp_file, &buffer[..bytes_read])?;
}
io::Write::flush(&mut tmp_file)?;
}
Ok(())
});
return pipe_to_tmp(stdin, tmp_file);
})
});
let anonymous_fd_pipe_handles: Vec<JoinHandle<anyhow::Result<()>>> = anonymous_fd_tmp_files
.into_iter()
.map(|(file, tmp_file)| thread::spawn(move || pipe_to_tmp(file, tmp_file)))
.collect();
if args.foreground {
app.run_foreground(url)?;
} else {
app.launch(url)?;
sender.join().unwrap()?;
pipe_handle.join().unwrap()?;
if let Some(handle) = stdin_pipe_handle {
handle.join().unwrap()?;
}
for handle in anonymous_fd_pipe_handles {
handle.join().unwrap()?;
}
}
if let Some(exit_status) = exit_status.lock().take() {
@@ -285,6 +294,64 @@ fn main() -> Result<()> {
Ok(())
}
fn pipe_to_tmp(mut src: impl io::Read, mut dest: fs::File) -> Result<()> {
let mut buffer = [0; 8 * 1024];
loop {
let bytes_read = match src.read(&mut buffer) {
Err(err) if err.kind() == io::ErrorKind::Interrupted => continue,
res => res?,
};
if bytes_read == 0 {
break;
}
io::Write::write_all(&mut dest, &buffer[..bytes_read])?;
}
io::Write::flush(&mut dest)?;
Ok(())
}
fn anonymous_fd(path: &str) -> Option<fs::File> {
#[cfg(target_os = "linux")]
{
use std::os::fd::{self, FromRawFd};
let fd_str = path.strip_prefix("/proc/self/fd/")?;
let link = fs::read_link(path).ok()?;
if !link.starts_with("memfd:") {
return None;
}
let fd: fd::RawFd = fd_str.parse().ok()?;
let file = unsafe { fs::File::from_raw_fd(fd) };
return Some(file);
}
#[cfg(target_os = "macos")]
{
use std::os::{
fd::{self, FromRawFd},
unix::fs::FileTypeExt,
};
let fd_str = path.strip_prefix("/dev/fd/")?;
let metadata = fs::metadata(path).ok()?;
let file_type = metadata.file_type();
if !file_type.is_fifo() && !file_type.is_socket() {
return None;
}
let fd: fd::RawFd = fd_str.parse().ok()?;
let file = unsafe { fs::File::from_raw_fd(fd) };
return Some(file);
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
{
_ = path;
// not implemented for bsd, windows. Could be, but isn't yet
return None;
}
}
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
mod linux {
use std::{

View File

@@ -660,6 +660,10 @@ fn for_snowflake(
e.event_type.clone(),
serde_json::to_value(&e.event_properties).unwrap(),
),
Event::AssistantThreadFeedback(e) => (
"Assistant Feedback".to_string(),
serde_json::to_value(&e).unwrap(),
),
};
if let serde_json::Value::Object(ref mut map) = event_properties {

View File

@@ -313,9 +313,8 @@ async fn test_basic_following(
result
});
let multibuffer_editor_a = workspace_a.update_in(cx_a, |workspace, window, cx| {
let editor = cx.new(|cx| {
Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), true, window, cx)
});
let editor = cx
.new(|cx| Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), window, cx));
workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
editor
});

View File

@@ -61,9 +61,9 @@ impl CompletionProvider for MessageEditorCompletionProvider {
_: editor::CompletionContext,
_window: &mut Window,
cx: &mut Context<Editor>,
) -> Task<anyhow::Result<Vec<Completion>>> {
) -> Task<Result<Option<Vec<Completion>>>> {
let Some(handle) = self.0.upgrade() else {
return Task::ready(Ok(Vec::new()));
return Task::ready(Ok(None));
};
handle.update(cx, |message_editor, cx| {
message_editor.completions(buffer, buffer_position, cx)
@@ -246,20 +246,22 @@ impl MessageEditor {
buffer: &Entity<Buffer>,
end_anchor: Anchor,
cx: &mut Context<Self>,
) -> Task<Result<Vec<Completion>>> {
) -> Task<Result<Option<Vec<Completion>>>> {
if let Some((start_anchor, query, candidates)) =
self.collect_mention_candidates(buffer, end_anchor, cx)
{
if !candidates.is_empty() {
return cx.spawn(|_, cx| async move {
Ok(Self::resolve_completions_for_candidates(
&cx,
query.as_str(),
&candidates,
start_anchor..end_anchor,
Self::completion_for_mention,
)
.await)
Ok(Some(
Self::resolve_completions_for_candidates(
&cx,
query.as_str(),
&candidates,
start_anchor..end_anchor,
Self::completion_for_mention,
)
.await,
))
});
}
}
@@ -269,19 +271,21 @@ impl MessageEditor {
{
if !candidates.is_empty() {
return cx.spawn(|_, cx| async move {
Ok(Self::resolve_completions_for_candidates(
&cx,
query.as_str(),
candidates,
start_anchor..end_anchor,
Self::completion_for_emoji,
)
.await)
Ok(Some(
Self::resolve_completions_for_candidates(
&cx,
query.as_str(),
candidates,
start_anchor..end_anchor,
Self::completion_for_emoji,
)
.await,
))
});
}
}
Task::ready(Ok(vec![]))
Task::ready(Ok(Some(Vec::new())))
}
async fn resolve_completions_for_candidates(

View File

@@ -170,7 +170,9 @@ enum SignInStatus {
prompt: Option<request::PromptUserDeviceFlow>,
task: Shared<Task<Result<(), Arc<anyhow::Error>>>>,
},
SignedOut,
SignedOut {
awaiting_signing_in: bool,
},
}
#[derive(Debug, Clone)]
@@ -180,7 +182,9 @@ pub enum Status {
},
Error(Arc<str>),
Disabled,
SignedOut,
SignedOut {
awaiting_signing_in: bool,
},
SigningIn {
prompt: Option<request::PromptUserDeviceFlow>,
},
@@ -345,8 +349,8 @@ impl Copilot {
buffers: Default::default(),
_subscription: cx.on_app_quit(Self::shutdown_language_server),
};
this.enable_or_disable_copilot(cx);
cx.observe_global::<SettingsStore>(move |this, cx| this.enable_or_disable_copilot(cx))
this.start_copilot(true, false, cx);
cx.observe_global::<SettingsStore>(move |this, cx| this.start_copilot(true, false, cx))
.detach();
this
}
@@ -364,26 +368,40 @@ impl Copilot {
}
}
fn enable_or_disable_copilot(&mut self, cx: &mut Context<Self>) {
fn start_copilot(
&mut self,
check_edit_prediction_provider: bool,
awaiting_sign_in_after_start: bool,
cx: &mut Context<Self>,
) {
if !matches!(self.server, CopilotServer::Disabled) {
return;
}
let language_settings = all_language_settings(None, cx);
if check_edit_prediction_provider
&& language_settings.edit_predictions.provider != EditPredictionProvider::Copilot
{
return;
}
let server_id = self.server_id;
let http = self.http.clone();
let node_runtime = self.node_runtime.clone();
let language_settings = all_language_settings(None, cx);
if language_settings.edit_predictions.provider == EditPredictionProvider::Copilot {
if matches!(self.server, CopilotServer::Disabled) {
let env = self.build_env(&language_settings.edit_predictions.copilot);
let start_task = cx
.spawn(move |this, cx| {
Self::start_language_server(server_id, http, node_runtime, env, this, cx)
})
.shared();
self.server = CopilotServer::Starting { task: start_task };
cx.notify();
}
} else {
self.server = CopilotServer::Disabled;
cx.notify();
}
let env = self.build_env(&language_settings.edit_predictions.copilot);
let start_task = cx
.spawn(move |this, cx| {
Self::start_language_server(
server_id,
http,
node_runtime,
env,
this,
awaiting_sign_in_after_start,
cx,
)
})
.shared();
self.server = CopilotServer::Starting { task: start_task };
cx.notify();
}
fn build_env(&self, copilot_settings: &CopilotSettings) -> Option<HashMap<String, String>> {
@@ -449,6 +467,7 @@ impl Copilot {
node_runtime: NodeRuntime,
env: Option<HashMap<String, String>>,
this: WeakEntity<Self>,
awaiting_sign_in_after_start: bool,
mut cx: AsyncApp,
) {
let start_language_server = async {
@@ -522,7 +541,9 @@ impl Copilot {
Ok((server, status)) => {
this.server = CopilotServer::Running(RunningCopilotServer {
lsp: server,
sign_in_status: SignInStatus::SignedOut,
sign_in_status: SignInStatus::SignedOut {
awaiting_signing_in: awaiting_sign_in_after_start,
},
registered_buffers: Default::default(),
});
cx.emit(Event::CopilotLanguageServerStarted);
@@ -545,7 +566,7 @@ impl Copilot {
cx.notify();
task.clone()
}
SignInStatus::SignedOut | SignInStatus::Unauthorized { .. } => {
SignInStatus::SignedOut { .. } | SignInStatus::Unauthorized { .. } => {
let lsp = server.lsp.clone();
let task = cx
.spawn(|this, mut cx| async move {
@@ -633,7 +654,7 @@ impl Copilot {
anyhow::Ok(())
})
}
CopilotServer::Disabled => cx.background_spawn(async move {
CopilotServer::Disabled => cx.background_spawn(async {
clear_copilot_config_dir().await;
anyhow::Ok(())
}),
@@ -651,7 +672,8 @@ impl Copilot {
let server_id = self.server_id;
move |this, cx| async move {
clear_copilot_dir().await;
Self::start_language_server(server_id, http, node_runtime, env, this, cx).await
Self::start_language_server(server_id, http, node_runtime, env, this, false, cx)
.await
}
})
.shared();
@@ -961,7 +983,11 @@ impl Copilot {
SignInStatus::SigningIn { prompt, .. } => Status::SigningIn {
prompt: prompt.clone(),
},
SignInStatus::SignedOut => Status::SignedOut,
SignInStatus::SignedOut {
awaiting_signing_in,
} => Status::SignedOut {
awaiting_signing_in: *awaiting_signing_in,
},
}
}
}
@@ -990,7 +1016,11 @@ impl Copilot {
}
}
request::SignInStatus::Ok { user: None } | request::SignInStatus::NotSignedIn => {
server.sign_in_status = SignInStatus::SignedOut;
if !matches!(server.sign_in_status, SignInStatus::SignedOut { .. }) {
server.sign_in_status = SignInStatus::SignedOut {
awaiting_signing_in: false,
};
}
cx.emit(Event::CopilotAuthSignedOut);
for buffer in self.buffers.iter().cloned().collect::<Vec<_>>() {
self.unregister_buffer(&buffer);

View File

@@ -745,8 +745,8 @@ mod tests {
);
multibuffer
});
let editor = cx
.add_window(|window, cx| Editor::for_multibuffer(multibuffer, None, true, window, cx));
let editor =
cx.add_window(|window, cx| Editor::for_multibuffer(multibuffer, None, window, cx));
editor
.update(cx, |editor, window, cx| {
use gpui::Focusable;
@@ -781,7 +781,7 @@ mod tests {
assert!(editor.has_active_inline_completion());
assert_eq!(
editor.display_text(cx),
"\n\n\na = 1\nb = 2 + a\n\n\n\n\n\nc = 3\nd = 4\n\n"
"\n\na = 1\nb = 2 + a\n\n\n\nc = 3\nd = 4\n"
);
assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4\n");
});
@@ -803,7 +803,7 @@ mod tests {
assert!(!editor.has_active_inline_completion());
assert_eq!(
editor.display_text(cx),
"\n\n\na = 1\nb = 2\n\n\n\n\n\nc = 3\nd = 4\n\n"
"\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4\n"
);
assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4\n");
@@ -812,7 +812,7 @@ mod tests {
assert!(!editor.has_active_inline_completion());
assert_eq!(
editor.display_text(cx),
"\n\n\na = 1\nb = 2\n\n\n\n\n\nc = 3\nd = 4 \n\n"
"\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4 \n"
);
assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4 \n");
});
@@ -823,7 +823,7 @@ mod tests {
assert!(editor.has_active_inline_completion());
assert_eq!(
editor.display_text(cx),
"\n\n\na = 1\nb = 2\n\n\n\n\n\nc = 3\nd = 4 + c\n\n"
"\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4 + c\n"
);
assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4 \n");
});
@@ -997,8 +997,8 @@ mod tests {
);
multibuffer
});
let editor = cx
.add_window(|window, cx| Editor::for_multibuffer(multibuffer, None, true, window, cx));
let editor =
cx.add_window(|window, cx| Editor::for_multibuffer(multibuffer, None, window, cx));
editor
.update(cx, |editor, window, cx| {
use gpui::Focusable;

View File

@@ -1,9 +1,10 @@
use crate::{request::PromptUserDeviceFlow, Copilot, Status};
use gpui::{
div, App, ClipboardItem, Context, DismissEvent, Element, Entity, EventEmitter, FocusHandle,
Focusable, InteractiveElement, IntoElement, MouseDownEvent, ParentElement, Render, Styled,
Subscription, Window,
div, percentage, svg, Animation, AnimationExt, App, ClipboardItem, Context, DismissEvent,
Element, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement,
MouseDownEvent, ParentElement, Render, Styled, Subscription, Transformation, Window,
};
use std::time::Duration;
use ui::{prelude::*, Button, Label, Vector, VectorName};
use util::ResultExt as _;
use workspace::notifications::NotificationId;
@@ -17,11 +18,13 @@ pub fn initiate_sign_in(window: &mut Window, cx: &mut App) {
let Some(copilot) = Copilot::global(cx) else {
return;
};
let status = copilot.read(cx).status();
let Some(workspace) = window.root::<Workspace>().flatten() else {
return;
};
match status {
if matches!(copilot.read(cx).status(), Status::Disabled) {
copilot.update(cx, |this, cx| this.start_copilot(false, true, cx));
}
match copilot.read(cx).status() {
Status::Starting { task } => {
workspace.update(cx, |workspace, cx| {
workspace.show_toast(
@@ -54,6 +57,15 @@ pub fn initiate_sign_in(window: &mut Window, cx: &mut App) {
copilot
.update(cx, |copilot, cx| copilot.sign_in(cx))
.detach_and_log_err(cx);
if let Some(window_handle) = cx.active_window() {
window_handle
.update(cx, |_, window, cx| {
workspace.toggle_modal(window, cx, |_, cx| {
CopilotCodeVerification::new(&copilot, cx)
});
})
.log_err();
}
}
})
.log_err();
@@ -76,6 +88,7 @@ pub struct CopilotCodeVerification {
status: Status,
connect_clicked: bool,
focus_handle: FocusHandle,
copilot: Entity<Copilot>,
_subscription: Subscription,
}
@@ -86,7 +99,20 @@ impl Focusable for CopilotCodeVerification {
}
impl EventEmitter<DismissEvent> for CopilotCodeVerification {}
impl ModalView for CopilotCodeVerification {}
impl ModalView for CopilotCodeVerification {
fn on_before_dismiss(
&mut self,
_: &mut Window,
cx: &mut Context<Self>,
) -> workspace::DismissDecision {
self.copilot.update(cx, |copilot, cx| {
if matches!(copilot.status(), Status::SigningIn { .. }) {
copilot.sign_out(cx).detach_and_log_err(cx);
}
});
workspace::DismissDecision::Dismiss(true)
}
}
impl CopilotCodeVerification {
pub fn new(copilot: &Entity<Copilot>, cx: &mut Context<Self>) -> Self {
@@ -95,6 +121,7 @@ impl CopilotCodeVerification {
status,
connect_clicked: false,
focus_handle: cx.focus_handle(),
copilot: copilot.clone(),
_subscription: cx.observe(copilot, |this, copilot, cx| {
let status = copilot.read(cx).status();
match status {
@@ -180,9 +207,12 @@ impl CopilotCodeVerification {
.child(
Button::new("copilot-enable-cancel-button", "Cancel")
.full_width()
.on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))),
.on_click(cx.listener(|_, _, _, cx| {
cx.emit(DismissEvent);
})),
)
}
fn render_enabled_modal(cx: &mut Context<Self>) -> impl Element {
v_flex()
.gap_2()
@@ -216,16 +246,27 @@ impl CopilotCodeVerification {
)
}
fn render_disabled_modal() -> impl Element {
v_flex()
.child(Headline::new("Copilot is disabled").size(HeadlineSize::Large))
.child(Label::new("You can enable Copilot in your settings."))
fn render_loading(window: &mut Window, _: &mut Context<Self>) -> impl Element {
let loading_icon = svg()
.size_8()
.path(IconName::ArrowCircle.path())
.text_color(window.text_style().color)
.with_animation(
"icon_circle_arrow",
Animation::new(Duration::from_secs(2)).repeat(),
|svg, delta| svg.with_transformation(Transformation::rotate(percentage(delta))),
);
h_flex().justify_center().child(loading_icon)
}
}
impl Render for CopilotCodeVerification {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let prompt = match &self.status {
Status::SigningIn { prompt: None } => {
Self::render_loading(window, cx).into_any_element()
}
Status::SigningIn {
prompt: Some(prompt),
} => Self::render_prompting_modal(self.connect_clicked, prompt, cx).into_any_element(),
@@ -237,10 +278,6 @@ impl Render for CopilotCodeVerification {
self.connect_clicked = false;
Self::render_enabled_modal(cx).into_any_element()
}
Status::Disabled => {
self.connect_clicked = false;
Self::render_disabled_modal().into_any_element()
}
_ => div().into_any_element(),
};

View File

@@ -198,13 +198,8 @@ impl ProjectDiagnosticsEditor {
let excerpts = cx.new(|cx| MultiBuffer::new(project_handle.read(cx).capability()));
let editor = cx.new(|cx| {
let mut editor = Editor::for_multibuffer(
excerpts.clone(),
Some(project_handle.clone()),
true,
window,
cx,
);
let mut editor =
Editor::for_multibuffer(excerpts.clone(), Some(project_handle.clone()), window, cx);
editor.set_vertical_scroll_margin(5, cx);
editor.disable_inline_diagnostics();
editor

View File

@@ -169,10 +169,10 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
editor_blocks(&editor, cx),
[
(DisplayRow(0), FILE_HEADER.into()),
(DisplayRow(3), DIAGNOSTIC_HEADER.into()),
(DisplayRow(16), EXCERPT_HEADER.into()),
(DisplayRow(18), DIAGNOSTIC_HEADER.into()),
(DisplayRow(27), EXCERPT_HEADER.into()),
(DisplayRow(2), DIAGNOSTIC_HEADER.into()),
(DisplayRow(15), EXCERPT_HEADER.into()),
(DisplayRow(16), DIAGNOSTIC_HEADER.into()),
(DisplayRow(25), EXCERPT_HEADER.into()),
]
);
assert_eq!(
@@ -186,7 +186,6 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
// diagnostic group 1
"\n", // primary message
"\n", // padding
"\n", // expand
" let x = vec![];\n",
" let y = vec![];\n",
"\n", // supporting diagnostic
@@ -198,7 +197,6 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
" c(y);\n",
"\n", // supporting diagnostic
" d(x);\n",
"\n", // expand
"\n", // context ellipsis
// diagnostic group 2
"\n", // primary message
@@ -210,13 +208,11 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
" a(x);\n",
"\n", // supporting diagnostic
" b(y);\n",
"\n", // expand
"\n", // context ellipsis
" c(y);\n",
" d(x);\n",
"\n", // supporting diagnostic
"}",
"\n", // expand
)
);
@@ -224,7 +220,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
editor.update(cx, |editor, cx| {
assert_eq!(
editor.selections.display_ranges(cx),
[DisplayPoint::new(DisplayRow(13), 6)..DisplayPoint::new(DisplayRow(13), 6)]
[DisplayPoint::new(DisplayRow(12), 6)..DisplayPoint::new(DisplayRow(12), 6)]
);
});
@@ -260,12 +256,12 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
editor_blocks(&editor, cx),
[
(DisplayRow(0), FILE_HEADER.into()),
(DisplayRow(3), DIAGNOSTIC_HEADER.into()),
(DisplayRow(8), FILE_HEADER.into()),
(DisplayRow(12), DIAGNOSTIC_HEADER.into()),
(DisplayRow(25), EXCERPT_HEADER.into()),
(DisplayRow(27), DIAGNOSTIC_HEADER.into()),
(DisplayRow(36), EXCERPT_HEADER.into()),
(DisplayRow(2), DIAGNOSTIC_HEADER.into()),
(DisplayRow(7), FILE_HEADER.into()),
(DisplayRow(9), DIAGNOSTIC_HEADER.into()),
(DisplayRow(22), EXCERPT_HEADER.into()),
(DisplayRow(23), DIAGNOSTIC_HEADER.into()),
(DisplayRow(32), EXCERPT_HEADER.into()),
]
);
@@ -280,7 +276,6 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
// diagnostic group 1
"\n", // primary message
"\n", // padding
"\n", // expand
"const a: i32 = 'a';\n",
"\n", // supporting diagnostic
"const b: i32 = c;\n",
@@ -292,8 +287,6 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
// diagnostic group 1
"\n", // primary message
"\n", // padding
"\n", // expand
"\n", // expand
" let x = vec![];\n",
" let y = vec![];\n",
"\n", // supporting diagnostic
@@ -309,7 +302,6 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
// diagnostic group 2
"\n", // primary message
"\n", // filename
"\n", // expand
"fn main() {\n",
" let x = vec![];\n",
"\n", // supporting diagnostic
@@ -317,13 +309,11 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
" a(x);\n",
"\n", // supporting diagnostic
" b(y);\n",
"\n", // expand
"\n", // context ellipsis
" c(y);\n",
" d(x);\n",
"\n", // supporting diagnostic
"}",
"\n", // expand
)
);
@@ -331,7 +321,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
editor.update(cx, |editor, cx| {
assert_eq!(
editor.selections.display_ranges(cx),
[DisplayPoint::new(DisplayRow(22), 6)..DisplayPoint::new(DisplayRow(22), 6)]
[DisplayPoint::new(DisplayRow(19), 6)..DisplayPoint::new(DisplayRow(19), 6)]
);
});
@@ -380,14 +370,14 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
editor_blocks(&editor, cx),
[
(DisplayRow(0), FILE_HEADER.into()),
(DisplayRow(3), DIAGNOSTIC_HEADER.into()),
(DisplayRow(8), EXCERPT_HEADER.into()),
(DisplayRow(10), DIAGNOSTIC_HEADER.into()),
(DisplayRow(15), FILE_HEADER.into()),
(DisplayRow(19), DIAGNOSTIC_HEADER.into()),
(DisplayRow(32), EXCERPT_HEADER.into()),
(DisplayRow(34), DIAGNOSTIC_HEADER.into()),
(DisplayRow(43), EXCERPT_HEADER.into()),
(DisplayRow(2), DIAGNOSTIC_HEADER.into()),
(DisplayRow(7), EXCERPT_HEADER.into()),
(DisplayRow(8), DIAGNOSTIC_HEADER.into()),
(DisplayRow(13), FILE_HEADER.into()),
(DisplayRow(15), DIAGNOSTIC_HEADER.into()),
(DisplayRow(28), EXCERPT_HEADER.into()),
(DisplayRow(29), DIAGNOSTIC_HEADER.into()),
(DisplayRow(38), EXCERPT_HEADER.into()),
]
);
@@ -402,7 +392,6 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
// diagnostic group 1
"\n", // primary message
"\n", // padding
"\n", // expand
"const a: i32 = 'a';\n",
"\n", // supporting diagnostic
"const b: i32 = c;\n",
@@ -410,7 +399,6 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
// diagnostic group 2
"\n", // primary message
"\n", // padding
"\n", // expand
"const a: i32 = 'a';\n",
"const b: i32 = c;\n",
"\n", // supporting diagnostic
@@ -422,8 +410,6 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
// diagnostic group 1
"\n", // primary message
"\n", // padding
"\n", // expand
"\n", // expand
" let x = vec![];\n",
" let y = vec![];\n",
"\n", // supporting diagnostic
@@ -439,7 +425,6 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
// diagnostic group 2
"\n", // primary message
"\n", // filename
"\n", // expand
"fn main() {\n",
" let x = vec![];\n",
"\n", // supporting diagnostic
@@ -447,13 +432,11 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
" a(x);\n",
"\n", // supporting diagnostic
" b(y);\n",
"\n", // expand
"\n", // context ellipsis
" c(y);\n",
" d(x);\n",
"\n", // supporting diagnostic
"}",
"\n", // expand
)
);
}
@@ -535,7 +518,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
editor_blocks(&editor, cx),
[
(DisplayRow(0), FILE_HEADER.into()),
(DisplayRow(3), DIAGNOSTIC_HEADER.into()),
(DisplayRow(2), DIAGNOSTIC_HEADER.into()),
]
);
assert_eq!(
@@ -546,9 +529,8 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
// diagnostic group 1
"\n", // primary message
"\n", // padding
"\n", // expand
"a();\n", //
"b();", "\n", // expand
"b();",
)
);
@@ -584,9 +566,9 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
editor_blocks(&editor, cx),
[
(DisplayRow(0), FILE_HEADER.into()),
(DisplayRow(3), DIAGNOSTIC_HEADER.into()),
(DisplayRow(7), EXCERPT_HEADER.into()),
(DisplayRow(9), DIAGNOSTIC_HEADER.into()),
(DisplayRow(2), DIAGNOSTIC_HEADER.into()),
(DisplayRow(6), EXCERPT_HEADER.into()),
(DisplayRow(7), DIAGNOSTIC_HEADER.into()),
]
);
assert_eq!(
@@ -597,10 +579,8 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
// diagnostic group 1
"\n", // primary message
"\n", // padding
"\n", // expand
"a();\n", // location
"b();\n", //
"\n", // expand
"\n", // collapsed context
// diagnostic group 2
"\n", // primary message
@@ -608,7 +588,6 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
"a();\n", // context
"b();\n", //
"c();", // context
"\n", // expand
)
);
@@ -655,9 +634,9 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
editor_blocks(&editor, cx),
[
(DisplayRow(0), FILE_HEADER.into()),
(DisplayRow(3), DIAGNOSTIC_HEADER.into()),
(DisplayRow(8), EXCERPT_HEADER.into()),
(DisplayRow(10), DIAGNOSTIC_HEADER.into()),
(DisplayRow(2), DIAGNOSTIC_HEADER.into()),
(DisplayRow(7), EXCERPT_HEADER.into()),
(DisplayRow(8), DIAGNOSTIC_HEADER.into()),
]
);
assert_eq!(
@@ -668,11 +647,9 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
// diagnostic group 1
"\n", // primary message
"\n", // padding
"\n", // expand
"a();\n", // location
"b();\n", //
"c();\n", // context
"\n", // expand
"\n", // collapsed context
// diagnostic group 2
"\n", // primary message
@@ -680,7 +657,6 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
"b();\n", // context
"c();\n", //
"d();", // context
"\n", // expand
)
);
@@ -716,9 +692,9 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
editor_blocks(&editor, cx),
[
(DisplayRow(0), FILE_HEADER.into()),
(DisplayRow(3), DIAGNOSTIC_HEADER.into()),
(DisplayRow(8), EXCERPT_HEADER.into()),
(DisplayRow(10), DIAGNOSTIC_HEADER.into()),
(DisplayRow(2), DIAGNOSTIC_HEADER.into()),
(DisplayRow(7), EXCERPT_HEADER.into()),
(DisplayRow(8), DIAGNOSTIC_HEADER.into()),
]
);
assert_eq!(
@@ -729,11 +705,9 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
// diagnostic group 1
"\n", // primary message
"\n", // padding
"\n", // expand
"b();\n", // location
"c();\n", //
"d();\n", // context
"\n", // expand
"\n", // collapsed context
// diagnostic group 2
"\n", // primary message
@@ -741,7 +715,6 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
"c();\n", // context
"d();\n", //
"e();", // context
"\n", // expand
)
);
}

View File

@@ -1,6 +1,6 @@
//! This module contains all actions supported by [`Editor`].
use super::*;
use gpui::{action_as, action_with_deprecated_aliases};
use gpui::{action_as, action_with_deprecated_aliases, actions};
use schemars::JsonSchema;
use util::serde::default_true;
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)]
@@ -248,7 +248,7 @@ impl_actions!(
]
);
gpui::actions!(
actions!(
editor,
[
AcceptEditPrediction,
@@ -404,6 +404,7 @@ gpui::actions!(
ShowCharacterPalette,
ShowEditPrediction,
ShowSignatureHelp,
ShowWordCompletions,
ShuffleLines,
SortLinesCaseInsensitive,
SortLinesCaseSensitive,

View File

@@ -118,10 +118,8 @@ impl DisplayMap {
font: Font,
font_size: Pixels,
wrap_width: Option<Pixels>,
show_excerpt_controls: bool,
buffer_header_height: u32,
excerpt_header_height: u32,
excerpt_footer_height: u32,
fold_placeholder: FoldPlaceholder,
cx: &mut Context<Self>,
) -> Self {
@@ -134,13 +132,7 @@ impl DisplayMap {
let (fold_map, snapshot) = FoldMap::new(snapshot);
let (tab_map, snapshot) = TabMap::new(snapshot, tab_size);
let (wrap_map, snapshot) = WrapMap::new(snapshot, font, font_size, wrap_width, cx);
let block_map = BlockMap::new(
snapshot,
show_excerpt_controls,
buffer_header_height,
excerpt_header_height,
excerpt_footer_height,
);
let block_map = BlockMap::new(snapshot, buffer_header_height, excerpt_header_height);
cx.observe(&wrap_map, |_, _, cx| cx.notify()).detach();
@@ -555,10 +547,6 @@ impl DisplayMap {
pub fn is_rewrapping(&self, cx: &gpui::App) -> bool {
self.wrap_map.read(cx).is_rewrapping()
}
pub fn show_excerpt_controls(&self) -> bool {
self.block_map.show_excerpt_controls()
}
}
#[derive(Debug, Default)]
@@ -1102,8 +1090,8 @@ impl DisplaySnapshot {
.map(|(row, block)| (DisplayRow(row), block))
}
pub fn sticky_header_excerpt(&self, row: DisplayRow) -> Option<StickyHeaderExcerpt<'_>> {
self.block_snapshot.sticky_header_excerpt(row.0)
pub fn sticky_header_excerpt(&self, row: f32) -> Option<StickyHeaderExcerpt<'_>> {
self.block_snapshot.sticky_header_excerpt(row)
}
pub fn block_for_id(&self, id: BlockId) -> Option<Block> {
@@ -1301,10 +1289,6 @@ impl DisplaySnapshot {
self.block_snapshot.buffer_header_height
}
pub fn excerpt_footer_height(&self) -> u32 {
self.block_snapshot.excerpt_footer_height
}
pub fn excerpt_header_height(&self) -> u32 {
self.block_snapshot.excerpt_header_height
}
@@ -1514,10 +1498,8 @@ pub mod tests {
font,
font_size,
wrap_width,
true,
buffer_start_excerpt_header_height,
excerpt_header_height,
0,
FoldPlaceholder::test(),
cx,
)
@@ -1764,10 +1746,8 @@ pub mod tests {
font("Helvetica"),
font_size,
wrap_width,
true,
1,
1,
0,
FoldPlaceholder::test(),
cx,
)
@@ -1875,10 +1855,8 @@ pub mod tests {
font("Helvetica"),
font_size,
None,
true,
1,
1,
0,
FoldPlaceholder::test(),
cx,
)
@@ -1938,8 +1916,6 @@ pub mod tests {
font("Helvetica"),
font_size,
None,
true,
1,
1,
1,
FoldPlaceholder::test(),
@@ -2032,8 +2008,6 @@ pub mod tests {
font("Helvetica"),
font_size,
None,
true,
1,
1,
1,
FoldPlaceholder::test(),
@@ -2134,10 +2108,8 @@ pub mod tests {
font("Courier"),
px(16.0),
None,
true,
1,
1,
0,
FoldPlaceholder::test(),
cx,
)
@@ -2239,10 +2211,8 @@ pub mod tests {
font("Courier"),
px(16.0),
None,
true,
1,
1,
0,
FoldPlaceholder::test(),
cx,
)
@@ -2328,10 +2298,8 @@ pub mod tests {
font("Courier"),
px(16.0),
None,
true,
1,
1,
0,
FoldPlaceholder::test(),
cx,
)
@@ -2472,10 +2440,8 @@ pub mod tests {
font("Courier"),
font_size,
Some(px(40.0)),
true,
1,
1,
0,
FoldPlaceholder::test(),
cx,
)
@@ -2556,8 +2522,6 @@ pub mod tests {
font("Courier"),
font_size,
None,
true,
1,
1,
1,
FoldPlaceholder::test(),
@@ -2682,10 +2646,8 @@ pub mod tests {
font("Helvetica"),
font_size,
None,
true,
1,
1,
0,
FoldPlaceholder::test(),
cx,
);
@@ -2721,10 +2683,8 @@ pub mod tests {
font("Helvetica"),
font_size,
None,
true,
1,
1,
0,
FoldPlaceholder::test(),
cx,
)
@@ -2798,10 +2758,8 @@ pub mod tests {
font("Helvetica"),
font_size,
None,
true,
1,
1,
0,
FoldPlaceholder::test(),
cx,
)

View File

@@ -37,10 +37,8 @@ pub struct BlockMap {
custom_blocks: Vec<Arc<CustomBlock>>,
custom_blocks_by_id: TreeMap<CustomBlockId, Arc<CustomBlock>>,
transforms: RefCell<SumTree<Transform>>,
show_excerpt_controls: bool,
buffer_header_height: u32,
excerpt_header_height: u32,
excerpt_footer_height: u32,
pub(super) folded_buffers: HashSet<BufferId>,
}
@@ -58,7 +56,6 @@ pub struct BlockSnapshot {
custom_blocks_by_id: TreeMap<CustomBlockId, Arc<CustomBlock>>,
pub(super) buffer_header_height: u32,
pub(super) excerpt_header_height: u32,
pub(super) excerpt_footer_height: u32,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
@@ -285,14 +282,12 @@ pub enum Block {
first_excerpt: ExcerptInfo,
prev_excerpt: Option<ExcerptInfo>,
height: u32,
show_excerpt_controls: bool,
},
ExcerptBoundary {
prev_excerpt: Option<ExcerptInfo>,
next_excerpt: Option<ExcerptInfo>,
height: u32,
starts_new_buffer: bool,
show_excerpt_controls: bool,
},
}
@@ -362,13 +357,11 @@ impl Debug for Block {
first_excerpt,
prev_excerpt,
height,
show_excerpt_controls,
} => f
.debug_struct("FoldedBuffer")
.field("first_excerpt", &first_excerpt)
.field("prev_excerpt", prev_excerpt)
.field("height", height)
.field("show_excerpt_controls", show_excerpt_controls)
.finish(),
Self::ExcerptBoundary {
starts_new_buffer,
@@ -413,10 +406,8 @@ pub struct BlockRows<'a> {
impl BlockMap {
pub fn new(
wrap_snapshot: WrapSnapshot,
show_excerpt_controls: bool,
buffer_header_height: u32,
excerpt_header_height: u32,
excerpt_footer_height: u32,
) -> Self {
let row_count = wrap_snapshot.max_point().row() + 1;
let mut transforms = SumTree::default();
@@ -428,10 +419,8 @@ impl BlockMap {
folded_buffers: HashSet::default(),
transforms: RefCell::new(transforms),
wrap_snapshot: RefCell::new(wrap_snapshot.clone()),
show_excerpt_controls,
buffer_header_height,
excerpt_header_height,
excerpt_footer_height,
};
map.sync(
&wrap_snapshot,
@@ -454,7 +443,6 @@ impl BlockMap {
custom_blocks_by_id: self.custom_blocks_by_id.clone(),
buffer_header_height: self.buffer_header_height,
excerpt_header_height: self.excerpt_header_height,
excerpt_footer_height: self.excerpt_footer_height,
},
}
}
@@ -650,8 +638,6 @@ impl BlockMap {
if buffer.show_headers() {
blocks_in_edit.extend(BlockMap::header_and_footer_blocks(
self.show_excerpt_controls,
self.excerpt_footer_height,
self.buffer_header_height,
self.excerpt_header_height,
buffer,
@@ -722,13 +708,7 @@ impl BlockMap {
}
}
pub fn show_excerpt_controls(&self) -> bool {
self.show_excerpt_controls
}
fn header_and_footer_blocks<'a, R, T>(
show_excerpt_controls: bool,
excerpt_footer_height: u32,
buffer_header_height: u32,
excerpt_header_height: u32,
buffer: &'a multi_buffer::MultiBufferSnapshot,
@@ -774,11 +754,6 @@ impl BlockMap {
.filter(|prev| !folded_buffers.contains(&prev.buffer_id));
let mut height = 0;
if prev_excerpt.is_some() {
if show_excerpt_controls {
height += excerpt_footer_height;
}
}
if let Some(new_buffer_id) = new_buffer_id {
let first_excerpt = excerpt_boundary.next.clone().unwrap();
@@ -812,7 +787,6 @@ impl BlockMap {
Block::FoldedBuffer {
prev_excerpt,
height: height + buffer_header_height,
show_excerpt_controls,
first_excerpt,
},
));
@@ -822,9 +796,6 @@ impl BlockMap {
if excerpt_boundary.next.is_some() {
if new_buffer_id.is_some() {
height += buffer_header_height;
if show_excerpt_controls {
height += excerpt_header_height;
}
} else {
height += excerpt_header_height;
}
@@ -845,7 +816,6 @@ impl BlockMap {
next_excerpt: excerpt_boundary.next,
height,
starts_new_buffer: new_buffer_id.is_some(),
show_excerpt_controls,
},
))
})
@@ -1432,7 +1402,8 @@ impl BlockSnapshot {
})
}
pub fn sticky_header_excerpt(&self, top_row: u32) -> Option<StickyHeaderExcerpt<'_>> {
pub fn sticky_header_excerpt(&self, position: f32) -> Option<StickyHeaderExcerpt<'_>> {
let top_row = position as u32;
let mut cursor = self.transforms.cursor::<BlockRow>(&());
cursor.seek(&BlockRow(top_row), Bias::Left, &());
@@ -1445,19 +1416,13 @@ impl BlockSnapshot {
prev_excerpt,
next_excerpt,
starts_new_buffer,
show_excerpt_controls,
..
}) => {
let matches_start = if *show_excerpt_controls && prev_excerpt.is_some() {
start < top_row
} else {
start <= top_row
};
let matches_start = (start as f32) < position;
if matches_start && top_row <= end {
return next_excerpt.as_ref().map(|excerpt| StickyHeaderExcerpt {
next_buffer_row: None,
next_excerpt_controls_present: *show_excerpt_controls,
excerpt,
});
}
@@ -1467,7 +1432,6 @@ impl BlockSnapshot {
return prev_excerpt.as_ref().map(|excerpt| StickyHeaderExcerpt {
excerpt,
next_buffer_row,
next_excerpt_controls_present: *show_excerpt_controls,
});
}
Some(Block::FoldedBuffer {
@@ -1476,7 +1440,6 @@ impl BlockSnapshot {
}) if top_row <= start => {
return Some(StickyHeaderExcerpt {
next_buffer_row: Some(end),
next_excerpt_controls_present: false,
excerpt,
});
}
@@ -1785,7 +1748,6 @@ impl BlockChunks<'_> {
pub struct StickyHeaderExcerpt<'a> {
pub excerpt: &'a ExcerptInfo,
pub next_excerpt_controls_present: bool,
pub next_buffer_row: Option<u32>,
}
@@ -2066,7 +2028,7 @@ mod tests {
let (mut tab_map, tab_snapshot) = TabMap::new(fold_snapshot, 1.try_into().unwrap());
let (wrap_map, wraps_snapshot) =
cx.update(|cx| WrapMap::new(tab_snapshot, font("Helvetica"), px(14.0), None, cx));
let mut block_map = BlockMap::new(wraps_snapshot.clone(), true, 1, 1, 1);
let mut block_map = BlockMap::new(wraps_snapshot.clone(), 1, 1);
let mut writer = block_map.write(wraps_snapshot.clone(), Default::default());
let block_ids = writer.insert(vec![
@@ -2279,14 +2241,11 @@ mod tests {
let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
let (_, wraps_snapshot) = WrapMap::new(tab_snapshot, font, font_size, Some(wrap_width), cx);
let block_map = BlockMap::new(wraps_snapshot.clone(), true, 1, 1, 1);
let block_map = BlockMap::new(wraps_snapshot.clone(), 1, 1);
let snapshot = block_map.read(wraps_snapshot, Default::default());
// Each excerpt has a header above and footer below. Excerpts are also *separated* by a newline.
assert_eq!(
snapshot.text(),
"\n\nBuff\ner 1\n\n\n\nBuff\ner 2\n\n\n\nBuff\ner 3\n"
);
assert_eq!(snapshot.text(), "\nBuff\ner 1\n\nBuff\ner 2\n\nBuff\ner 3");
let blocks: Vec<_> = snapshot
.blocks_in_range(0..u32::MAX)
@@ -2295,10 +2254,9 @@ mod tests {
assert_eq!(
blocks,
vec![
(0..2, BlockId::ExcerptBoundary(Some(excerpt_ids[0]))), // path, header
(4..7, BlockId::ExcerptBoundary(Some(excerpt_ids[1]))), // footer, path, header
(9..12, BlockId::ExcerptBoundary(Some(excerpt_ids[2]))), // footer, path, header
(14..15, BlockId::ExcerptBoundary(None)), // footer
(0..1, BlockId::ExcerptBoundary(Some(excerpt_ids[0]))), // path, header
(3..4, BlockId::ExcerptBoundary(Some(excerpt_ids[1]))), // path, header
(6..7, BlockId::ExcerptBoundary(Some(excerpt_ids[2]))), // path, header
]
);
}
@@ -2317,7 +2275,7 @@ mod tests {
let (_tab_map, tab_snapshot) = TabMap::new(fold_snapshot, 1.try_into().unwrap());
let (_wrap_map, wraps_snapshot) =
cx.update(|cx| WrapMap::new(tab_snapshot, font("Helvetica"), px(14.0), None, cx));
let mut block_map = BlockMap::new(wraps_snapshot.clone(), false, 1, 1, 0);
let mut block_map = BlockMap::new(wraps_snapshot.clone(), 1, 1);
let mut writer = block_map.write(wraps_snapshot.clone(), Default::default());
let block_ids = writer.insert(vec![
@@ -2420,7 +2378,7 @@ mod tests {
let (_, wraps_snapshot) = cx.update(|cx| {
WrapMap::new(tab_snapshot, font("Helvetica"), px(14.0), Some(px(60.)), cx)
});
let mut block_map = BlockMap::new(wraps_snapshot.clone(), true, 1, 1, 0);
let mut block_map = BlockMap::new(wraps_snapshot.clone(), 1, 1);
let mut writer = block_map.write(wraps_snapshot.clone(), Default::default());
writer.insert(vec![
@@ -2464,7 +2422,7 @@ mod tests {
let (mut tab_map, tab_snapshot) = TabMap::new(fold_snapshot, tab_size);
let (wrap_map, wraps_snapshot) =
cx.update(|cx| WrapMap::new(tab_snapshot, font("Helvetica"), px(14.0), None, cx));
let mut block_map = BlockMap::new(wraps_snapshot.clone(), false, 1, 1, 0);
let mut block_map = BlockMap::new(wraps_snapshot.clone(), 1, 1);
let mut writer = block_map.write(wraps_snapshot.clone(), Default::default());
let replace_block_id = writer.insert(vec![BlockProperties {
@@ -2631,12 +2589,12 @@ mod tests {
let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
let (_, wrap_snapshot) =
cx.update(|cx| WrapMap::new(tab_snapshot, font("Helvetica"), px(14.0), None, cx));
let mut block_map = BlockMap::new(wrap_snapshot.clone(), true, 2, 1, 1);
let mut block_map = BlockMap::new(wrap_snapshot.clone(), 2, 1);
let blocks_snapshot = block_map.read(wrap_snapshot.clone(), Patch::default());
assert_eq!(
blocks_snapshot.text(),
"\n\n\n111\n\n\n\n\n222\n\n\n333\n\n\n444\n\n\n\n\n555\n\n\n666\n"
"\n\n111\n\n\n222\n\n333\n\n444\n\n\n555\n\n666"
);
assert_eq!(
blocks_snapshot
@@ -2644,30 +2602,21 @@ mod tests {
.map(|i| i.buffer_row)
.collect::<Vec<_>>(),
vec![
None,
None,
None,
Some(0),
None,
None,
None,
None,
Some(1),
None,
None,
Some(2),
None,
None,
Some(3),
None,
None,
None,
None,
Some(4),
None,
None,
Some(5),
None,
]
);
@@ -2715,7 +2664,7 @@ mod tests {
let blocks_snapshot = block_map.read(wrap_snapshot.clone(), Patch::default());
assert_eq!(
blocks_snapshot.text(),
"\n\n\n111\n\n\n\n\n\n222\n\n\n\n333\n\n\n444\n\n\n\n\n\n\n555\n\n\n666\n\n"
"\n\n111\n\n\n\n222\n\n\n333\n\n444\n\n\n\n\n555\n\n666\n"
);
assert_eq!(
blocks_snapshot
@@ -2723,35 +2672,26 @@ mod tests {
.map(|i| i.buffer_row)
.collect::<Vec<_>>(),
vec![
None,
None,
None,
Some(0),
None,
None,
None,
None,
None,
Some(1),
None,
None,
None,
Some(2),
None,
None,
Some(3),
None,
None,
None,
None,
None,
None,
Some(4),
None,
None,
Some(5),
None,
None,
]
);
@@ -2793,7 +2733,7 @@ mod tests {
);
assert_eq!(
blocks_snapshot.text(),
"\n\n\n\n\n\n222\n\n\n\n333\n\n\n444\n\n\n\n\n\n\n555\n\n\n666\n\n"
"\n\n\n\n\n222\n\n\n333\n\n444\n\n\n\n\n555\n\n666\n"
);
assert_eq!(
blocks_snapshot
@@ -2806,27 +2746,20 @@ mod tests {
None,
None,
None,
None,
Some(1),
None,
None,
None,
Some(2),
None,
None,
Some(3),
None,
None,
None,
None,
None,
None,
Some(4),
None,
None,
Some(5),
None,
None,
]
);
@@ -2862,7 +2795,7 @@ mod tests {
.count(),
"Should have two folded blocks, producing headers"
);
assert_eq!(blocks_snapshot.text(), "\n\n\n\n\n\n\n\n555\n\n\n666\n\n");
assert_eq!(blocks_snapshot.text(), "\n\n\n\n\n\n\n555\n\n666\n");
assert_eq!(
blocks_snapshot
.row_infos(BlockRow(0))
@@ -2876,13 +2809,10 @@ mod tests {
None,
None,
None,
None,
Some(4),
None,
None,
Some(5),
None,
None,
]
);
@@ -2917,7 +2847,7 @@ mod tests {
);
assert_eq!(
blocks_snapshot.text(),
"\n\n\n\n111\n\n\n\n\n\n\n\n555\n\n\n666\n\n",
"\n\n\n111\n\n\n\n\n\n555\n\n666\n",
"Should have extra newline for 111 buffer, due to a new block added when it was folded"
);
assert_eq!(
@@ -2929,21 +2859,16 @@ mod tests {
None,
None,
None,
None,
Some(0),
None,
None,
None,
None,
None,
None,
None,
Some(4),
None,
None,
Some(5),
None,
None,
]
);
@@ -2974,7 +2899,7 @@ mod tests {
assert_eq!(
blocks_snapshot.text(),
"\n\n\n\n111\n\n\n\n\n",
"\n\n\n111\n\n\n\n",
"Should have a single, first buffer left after folding"
);
assert_eq!(
@@ -2982,18 +2907,7 @@ mod tests {
.row_infos(BlockRow(0))
.map(|i| i.buffer_row)
.collect::<Vec<_>>(),
vec![
None,
None,
None,
None,
Some(0),
None,
None,
None,
None,
None,
]
vec![None, None, None, Some(0), None, None, None, None,]
);
}
@@ -3020,10 +2934,10 @@ mod tests {
let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
let (_, wrap_snapshot) =
cx.update(|cx| WrapMap::new(tab_snapshot, font("Helvetica"), px(14.0), None, cx));
let mut block_map = BlockMap::new(wrap_snapshot.clone(), true, 2, 1, 1);
let mut block_map = BlockMap::new(wrap_snapshot.clone(), 2, 1);
let blocks_snapshot = block_map.read(wrap_snapshot.clone(), Patch::default());
assert_eq!(blocks_snapshot.text(), "\n\n\n111\n");
assert_eq!(blocks_snapshot.text(), "\n\n111");
let mut writer = block_map.write(wrap_snapshot.clone(), Patch::default());
buffer.read_with(cx, |buffer, cx| {
@@ -3077,11 +2991,9 @@ mod tests {
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 excerpt_footer_height = rng.gen_range(1..=5);
log::info!("Wrap width: {:?}", wrap_width);
log::info!("Excerpt Header Height: {:?}", excerpt_header_height);
log::info!("Excerpt Footer Height: {:?}", excerpt_footer_height);
let is_singleton = rng.gen();
let buffer = if is_singleton {
let len = rng.gen_range(0..10);
@@ -3108,10 +3020,8 @@ mod tests {
cx.update(|cx| WrapMap::new(tab_snapshot, font, font_size, wrap_width, cx));
let mut block_map = BlockMap::new(
wraps_snapshot,
true,
buffer_start_header_height,
excerpt_header_height,
excerpt_footer_height,
);
for _ in 0..operations {
@@ -3329,8 +3239,6 @@ mod tests {
// Note that this needs to be synced with the related section in BlockMap::sync
expected_blocks.extend(BlockMap::header_and_footer_blocks(
true,
excerpt_footer_height,
buffer_start_header_height,
excerpt_header_height,
&buffer_snapshot,

View File

@@ -983,6 +983,7 @@ impl Iterator for WrapRows<'_> {
buffer_row: None,
multibuffer_row: None,
diff_status,
expand_info: None,
}
} else {
buffer_row

View File

@@ -106,7 +106,7 @@ use language::{
point_from_lsp, text_diff_with_options, AutoindentMode, BracketMatch, BracketPair, Buffer,
Capability, CharKind, CodeLabel, CursorShape, Diagnostic, DiffOptions, EditPredictionsMode,
EditPreview, HighlightedText, IndentKind, IndentSize, Language, OffsetRangeExt, Point,
Selection, SelectionGoal, TextObject, TransactionId, TreeSitterOptions,
Selection, SelectionGoal, TextObject, TransactionId, TreeSitterOptions, WordsQuery,
};
use language::{point_to_lsp, BufferRow, CharClassifier, Runnable, RunnableRange};
use linked_editing_ranges::refresh_linked_ranges;
@@ -196,7 +196,6 @@ use crate::signature_help::{SignatureHelpHiddenBy, SignatureHelpState};
pub const FILE_HEADER_HEIGHT: u32 = 2;
pub const MULTI_BUFFER_EXCERPT_HEADER_HEIGHT: u32 = 1;
pub const MULTI_BUFFER_EXCERPT_FOOTER_HEIGHT: u32 = 1;
pub const DEFAULT_MULTIBUFFER_CONTEXT: u32 = 2;
const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
const MAX_LINE_LEN: usize = 1024;
@@ -1092,7 +1091,6 @@ impl Editor {
EditorMode::SingleLine { auto_width: false },
buffer,
None,
false,
window,
cx,
)
@@ -1101,7 +1099,7 @@ impl Editor {
pub fn multi_line(window: &mut Window, cx: &mut Context<Self>) -> Self {
let buffer = cx.new(|cx| Buffer::local("", cx));
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
Self::new(EditorMode::Full, buffer, None, false, window, cx)
Self::new(EditorMode::Full, buffer, None, window, cx)
}
pub fn auto_width(window: &mut Window, cx: &mut Context<Self>) -> Self {
@@ -1111,7 +1109,6 @@ impl Editor {
EditorMode::SingleLine { auto_width: true },
buffer,
None,
false,
window,
cx,
)
@@ -1124,7 +1121,6 @@ impl Editor {
EditorMode::AutoHeight { max_lines },
buffer,
None,
false,
window,
cx,
)
@@ -1137,33 +1133,23 @@ impl Editor {
cx: &mut Context<Self>,
) -> Self {
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
Self::new(EditorMode::Full, buffer, project, false, window, cx)
Self::new(EditorMode::Full, buffer, project, window, cx)
}
pub fn for_multibuffer(
buffer: Entity<MultiBuffer>,
project: Option<Entity<Project>>,
show_excerpt_controls: bool,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
Self::new(
EditorMode::Full,
buffer,
project,
show_excerpt_controls,
window,
cx,
)
Self::new(EditorMode::Full, buffer, project, window, cx)
}
pub fn clone(&self, window: &mut Window, cx: &mut Context<Self>) -> Self {
let show_excerpt_controls = self.display_map.read(cx).show_excerpt_controls();
let mut clone = Self::new(
self.mode,
self.buffer.clone(),
self.project.clone(),
show_excerpt_controls,
window,
cx,
);
@@ -1183,7 +1169,6 @@ impl Editor {
mode: EditorMode,
buffer: Entity<MultiBuffer>,
project: Option<Entity<Project>>,
show_excerpt_controls: bool,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
@@ -1228,10 +1213,8 @@ impl Editor {
style.font(),
font_size,
None,
show_excerpt_controls,
FILE_HEADER_HEIGHT,
MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
MULTI_BUFFER_EXCERPT_FOOTER_HEIGHT,
fold_placeholder,
cx,
)
@@ -1358,10 +1341,7 @@ impl Editor {
project,
blink_manager: blink_manager.clone(),
show_local_selections: true,
show_scrollbars: match mode {
EditorMode::AutoHeight { .. } | EditorMode::SingleLine { .. } => false,
EditorMode::Full => true,
},
show_scrollbars: true,
mode,
show_breadcrumbs: EditorSettings::get_global(cx).toolbar.breadcrumbs,
show_gutter: mode == EditorMode::Full,
@@ -3997,20 +3977,34 @@ impl Editor {
}))
}
pub fn show_word_completions(
&mut self,
_: &ShowWordCompletions,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.open_completions_menu(true, None, window, cx);
}
pub fn show_completions(
&mut self,
options: &ShowCompletions,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.open_completions_menu(false, options.trigger.as_deref(), window, cx);
}
fn open_completions_menu(
&mut self,
ignore_completion_provider: bool,
trigger: Option<&str>,
window: &mut Window,
cx: &mut Context<Self>,
) {
if self.pending_rename.is_some() {
return;
}
let Some(provider) = self.completion_provider.as_ref() else {
return;
};
if !self.snippet_stack.is_empty() && self.context_menu.borrow().as_ref().is_some() {
return;
}
@@ -4032,14 +4026,14 @@ impl Editor {
let query = Self::completion_query(&self.buffer.read(cx).read(cx), position);
let trigger_kind = match &options.trigger {
let trigger_kind = match trigger {
Some(trigger) if buffer.read(cx).completion_triggers().contains(trigger) => {
CompletionTriggerKind::TRIGGER_CHARACTER
}
_ => CompletionTriggerKind::INVOKED,
};
let completion_context = CompletionContext {
trigger_character: options.trigger.as_ref().and_then(|trigger| {
trigger_character: trigger.and_then(|trigger| {
if trigger_kind == CompletionTriggerKind::TRIGGER_CHARACTER {
Some(String::from(trigger))
} else {
@@ -4048,8 +4042,7 @@ impl Editor {
}),
trigger_kind,
};
let completions =
provider.completions(&buffer, buffer_position, completion_context, window, cx);
let (old_range, word_kind) = buffer_snapshot.surrounding_word(buffer_position);
let (old_range, word_to_exclude) = if word_kind == Some(CharKind::Word) {
let word_to_exclude = buffer_snapshot
@@ -4087,15 +4080,49 @@ impl Editor {
);
let word_search_range = buffer_snapshot.point_to_offset(min_word_search)
..buffer_snapshot.point_to_offset(max_word_search);
let words = match completion_settings.words {
WordsCompletionMode::Disabled => Task::ready(HashMap::default()),
WordsCompletionMode::Enabled | WordsCompletionMode::Fallback => {
cx.background_spawn(async move {
buffer_snapshot.words_in_range(None, word_search_range)
})
let provider = self
.completion_provider
.as_ref()
.filter(|_| !ignore_completion_provider);
let skip_digits = query
.as_ref()
.map_or(true, |query| !query.chars().any(|c| c.is_digit(10)));
let (mut words, provided_completions) = match provider {
Some(provider) => {
let completions =
provider.completions(&buffer, buffer_position, completion_context, window, cx);
let words = match completion_settings.words {
WordsCompletionMode::Disabled => Task::ready(HashMap::default()),
WordsCompletionMode::Enabled | WordsCompletionMode::Fallback => cx
.background_spawn(async move {
buffer_snapshot.words_in_range(WordsQuery {
fuzzy_contents: None,
range: word_search_range,
skip_digits,
})
}),
};
(words, completions)
}
None => (
cx.background_spawn(async move {
buffer_snapshot.words_in_range(WordsQuery {
fuzzy_contents: None,
range: word_search_range,
skip_digits,
})
}),
Task::ready(Ok(None)),
),
};
let sort_completions = provider.sort_completions();
let sort_completions = provider
.as_ref()
.map_or(true, |provider| provider.sort_completions());
let id = post_inc(&mut self.next_completion_id);
let task = cx.spawn_in(window, |editor, mut cx| {
@@ -4103,52 +4130,34 @@ impl Editor {
editor.update(&mut cx, |this, _| {
this.completion_tasks.retain(|(task_id, _)| *task_id >= id);
})?;
let mut completions = completions.await.log_err().unwrap_or_default();
match completion_settings.words {
WordsCompletionMode::Enabled => {
completions.extend(
words
.await
.into_iter()
.filter(|(word, _)| word_to_exclude.as_ref() != Some(word))
.map(|(word, word_range)| Completion {
old_range: old_range.clone(),
new_text: word.clone(),
label: CodeLabel::plain(word, None),
documentation: None,
source: CompletionSource::BufferWord {
word_range,
resolved: false,
},
confirm: None,
}),
);
let mut completions = Vec::new();
if let Some(provided_completions) = provided_completions.await.log_err().flatten() {
completions.extend(provided_completions);
if completion_settings.words == WordsCompletionMode::Fallback {
words = Task::ready(HashMap::default());
}
WordsCompletionMode::Fallback => {
if completions.is_empty() {
completions.extend(
words
.await
.into_iter()
.filter(|(word, _)| word_to_exclude.as_ref() != Some(word))
.map(|(word, word_range)| Completion {
old_range: old_range.clone(),
new_text: word.clone(),
label: CodeLabel::plain(word, None),
documentation: None,
source: CompletionSource::BufferWord {
word_range,
resolved: false,
},
confirm: None,
}),
);
}
}
WordsCompletionMode::Disabled => {}
}
let mut words = words.await;
if let Some(word_to_exclude) = &word_to_exclude {
words.remove(word_to_exclude);
}
for lsp_completion in &completions {
words.remove(&lsp_completion.new_text);
}
completions.extend(words.into_iter().map(|(word, word_range)| Completion {
old_range: old_range.clone(),
new_text: word.clone(),
label: CodeLabel::plain(word, None),
documentation: None,
source: CompletionSource::BufferWord {
word_range,
resolved: false,
},
confirm: None,
}));
let menu = if completions.is_empty() {
None
} else {
@@ -4205,7 +4214,7 @@ impl Editor {
}
})?;
Ok::<_, anyhow::Error>(())
anyhow::Ok(())
}
.log_err()
});
@@ -4693,8 +4702,8 @@ impl Editor {
workspace.update_in(&mut cx, |workspace, window, cx| {
let project = workspace.project().clone();
let editor = cx
.new(|cx| Editor::for_multibuffer(excerpt_buffer, Some(project), true, window, cx));
let editor =
cx.new(|cx| Editor::for_multibuffer(excerpt_buffer, Some(project), window, cx));
workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
editor.update(cx, |editor, cx| {
editor.highlight_background::<Self>(
@@ -12381,7 +12390,6 @@ impl Editor {
Editor::for_multibuffer(
excerpt_buffer,
Some(workspace.project().clone()),
true,
window,
cx,
)
@@ -16779,8 +16787,7 @@ fn wrap_with_prefix(
is_whitespace,
} in tokenizer
{
if (current_line_len + grapheme_len) > wrap_column && (current_line_len != line_prefix_len)
{
if current_line_len + grapheme_len > wrap_column && current_line_len != line_prefix_len {
wrapped_text.push_str(current_line.trim_end());
wrapped_text.push('\n');
current_line.truncate(line_prefix.len());
@@ -16932,7 +16939,7 @@ pub trait CompletionProvider {
trigger: CompletionContext,
window: &mut Window,
cx: &mut Context<Editor>,
) -> Task<Result<Vec<Completion>>>;
) -> Task<Result<Option<Vec<Completion>>>>;
fn resolve_completions(
&self,
@@ -17172,15 +17179,25 @@ impl CompletionProvider for Entity<Project> {
options: CompletionContext,
_window: &mut Window,
cx: &mut Context<Editor>,
) -> Task<Result<Vec<Completion>>> {
) -> Task<Result<Option<Vec<Completion>>>> {
self.update(cx, |project, cx| {
let snippets = snippet_completions(project, buffer, buffer_position, cx);
let project_completions = project.completions(buffer, buffer_position, options, cx);
cx.background_spawn(async move {
let mut completions = project_completions.await?;
let snippets_completions = snippets.await?;
completions.extend(snippets_completions);
Ok(completions)
match project_completions.await? {
Some(mut completions) => {
completions.extend(snippets_completions);
Ok(Some(completions))
}
None => {
if snippets_completions.is_empty() {
Ok(None)
} else {
Ok(Some(snippets_completions))
}
}
}
})
})
}

View File

@@ -7676,7 +7676,6 @@ async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
EditorMode::Full,
multi_buffer,
Some(project.clone()),
true,
window,
cx,
)
@@ -9236,11 +9235,11 @@ async fn test_words_completion(cx: &mut TestAppContext) {
Ok(Some(lsp::CompletionResponse::Array(vec![
lsp::CompletionItem {
label: "first".into(),
..Default::default()
..lsp::CompletionItem::default()
},
lsp::CompletionItem {
label: "last".into(),
..Default::default()
..lsp::CompletionItem::default()
},
])))
}
@@ -9290,6 +9289,130 @@ async fn test_words_completion(cx: &mut TestAppContext) {
});
}
#[gpui::test]
async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
init_test(cx, |language_settings| {
language_settings.defaults.completions = Some(CompletionSettings {
words: WordsCompletionMode::Enabled,
lsp: true,
lsp_fetch_timeout_ms: 0,
});
});
let mut cx = EditorLspTestContext::new_rust(
lsp::ServerCapabilities {
completion_provider: Some(lsp::CompletionOptions {
trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
..lsp::CompletionOptions::default()
}),
signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
..lsp::ServerCapabilities::default()
},
cx,
)
.await;
let _completion_requests_handler =
cx.lsp
.server
.on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
Ok(Some(lsp::CompletionResponse::Array(vec![
lsp::CompletionItem {
label: "first".into(),
..lsp::CompletionItem::default()
},
lsp::CompletionItem {
label: "last".into(),
..lsp::CompletionItem::default()
},
])))
});
cx.set_state(indoc! {"ˇ
first
last
second
"});
cx.simulate_keystroke(".");
cx.executor().run_until_parked();
cx.condition(|editor, _| editor.context_menu_visible())
.await;
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),
&["first", "last", "second"],
"Word completions that has the same edit as the any of the LSP ones, should not be proposed"
);
} else {
panic!("expected completion menu to be open");
}
editor.cancel(&Cancel, window, cx);
});
}
#[gpui::test]
async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
init_test(cx, |language_settings| {
language_settings.defaults.completions = Some(CompletionSettings {
words: WordsCompletionMode::Fallback,
lsp: false,
lsp_fetch_timeout_ms: 0,
});
});
let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
cx.set_state(indoc! {"ˇ
0_usize
let
33
4.5f32
"});
cx.update_editor(|editor, window, cx| {
editor.show_completions(&ShowCompletions::default(), window, cx);
});
cx.executor().run_until_parked();
cx.condition(|editor, _| editor.context_menu_visible())
.await;
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),
&["let"],
"With no digits in the completion query, no digits should be in the word completions"
);
} else {
panic!("expected completion menu to be open");
}
editor.cancel(&Cancel, window, cx);
});
cx.set_state(indoc! {"
0_usize
let
3
33.35f32
"});
cx.update_editor(|editor, window, cx| {
editor.show_completions(&ShowCompletions::default(), window, cx);
});
cx.executor().run_until_parked();
cx.condition(|editor, _| editor.context_menu_visible())
.await;
cx.update_editor(|editor, _, _| {
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
{
assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
} else {
panic!("expected completion menu to be open");
}
});
}
#[gpui::test]
async fn test_multiline_completion(cx: &mut TestAppContext) {
init_test(cx, |_| {});
@@ -13438,7 +13561,6 @@ async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) {
EditorMode::Full,
multi_buffer,
Some(project.clone()),
true,
window,
cx,
)
@@ -13921,9 +14043,8 @@ async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
multibuffer
});
let editor = cx.add_window(|window, cx| {
Editor::new(EditorMode::Full, multi_buffer, None, true, window, cx)
});
let editor =
cx.add_window(|window, cx| Editor::new(EditorMode::Full, multi_buffer, None, window, cx));
editor
.update(cx, |editor, _window, cx| {
for (buffer, diff_base) in [
@@ -14042,9 +14163,8 @@ async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
multibuffer
});
let editor = cx.add_window(|window, cx| {
Editor::new(EditorMode::Full, multi_buffer, None, true, window, cx)
});
let editor =
cx.add_window(|window, cx| Editor::new(EditorMode::Full, multi_buffer, None, window, cx));
editor
.update(cx, |editor, _window, cx| {
let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
@@ -15600,14 +15720,7 @@ async fn test_display_diff_hunks(cx: &mut TestAppContext) {
});
let editor = cx.add_window(|window, cx| {
Editor::new(
EditorMode::Full,
multibuffer,
Some(project),
true,
window,
cx,
)
Editor::new(EditorMode::Full, multibuffer, Some(project), window, cx)
});
cx.run_until_parked();
@@ -15626,9 +15739,9 @@ async fn test_display_diff_hunks(cx: &mut TestAppContext) {
assert_eq!(
hunks,
[
DisplayRow(3)..DisplayRow(5),
DisplayRow(10)..DisplayRow(12),
DisplayRow(17)..DisplayRow(19),
DisplayRow(2)..DisplayRow(4),
DisplayRow(7)..DisplayRow(9),
DisplayRow(12)..DisplayRow(14),
]
);
}
@@ -16059,7 +16172,6 @@ async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
EditorMode::Full,
multi_buffer,
Some(project.clone()),
true,
window,
cx,
)
@@ -16214,7 +16326,6 @@ async fn test_folding_buffers(cx: &mut TestAppContext) {
EditorMode::Full,
multi_buffer.clone(),
Some(project.clone()),
true,
window,
cx,
)
@@ -16222,7 +16333,7 @@ async fn test_folding_buffers(cx: &mut TestAppContext) {
assert_eq!(
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
"\n\n\naaaa\nbbbb\ncccc\n\n\n\nffff\ngggg\n\n\n\njjjj\n\n\n\n\nllll\nmmmm\nnnnn\n\n\n\nqqqq\nrrrr\n\n\n\nuuuu\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n\n1111\n2222\n\n\n\n5555\n",
"\n\naaaa\nbbbb\ncccc\n\n\nffff\ngggg\n\n\njjjj\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
);
multi_buffer_editor.update(cx, |editor, cx| {
@@ -16230,7 +16341,7 @@ async fn test_folding_buffers(cx: &mut TestAppContext) {
});
assert_eq!(
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
"\n\n\n\n\nllll\nmmmm\nnnnn\n\n\n\nqqqq\nrrrr\n\n\n\nuuuu\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n\n1111\n2222\n\n\n\n5555\n",
"\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
"After folding the first buffer, its text should not be displayed"
);
@@ -16239,7 +16350,7 @@ async fn test_folding_buffers(cx: &mut TestAppContext) {
});
assert_eq!(
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
"\n\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n\n1111\n2222\n\n\n\n5555\n",
"\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
"After folding the second buffer, its text should not be displayed"
);
@@ -16264,7 +16375,7 @@ async fn test_folding_buffers(cx: &mut TestAppContext) {
});
assert_eq!(
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
"\n\n\n\n\nllll\nmmmm\nnnnn\n\n\n\nqqqq\nrrrr\n\n\n\nuuuu\n\n\n",
"\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
"After unfolding the second buffer, its text should be displayed"
);
@@ -16286,7 +16397,7 @@ async fn test_folding_buffers(cx: &mut TestAppContext) {
assert_eq!(
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
"\n\n\nB\n\n\n\n\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\n\nqqqq\nrrrr\n\n\n\nuuuu\n\n\n",
"\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
"After unfolding the first buffer, its and 2nd buffer's text should be displayed"
);
@@ -16295,7 +16406,7 @@ async fn test_folding_buffers(cx: &mut TestAppContext) {
});
assert_eq!(
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
"\n\n\nB\n\n\n\n\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\n\nqqqq\nrrrr\n\n\n\nuuuu\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n\n1111\n2222\n\n\n\n5555\n",
"\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
"After unfolding the all buffers, all original text should be displayed"
);
}
@@ -16381,13 +16492,12 @@ async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
EditorMode::Full,
multi_buffer,
Some(project.clone()),
true,
window,
cx,
)
});
let full_text = "\n\n\n1111\n2222\n3333\n\n\n\n\n4444\n5555\n6666\n\n\n\n\n7777\n8888\n9999\n";
let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
assert_eq!(
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
full_text,
@@ -16398,7 +16508,7 @@ async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
});
assert_eq!(
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
"\n\n\n\n\n4444\n5555\n6666\n\n\n\n\n7777\n8888\n9999\n",
"\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
"After folding the first buffer, its text should not be displayed"
);
@@ -16408,7 +16518,7 @@ async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
assert_eq!(
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
"\n\n\n\n\n\n\n7777\n8888\n9999\n",
"\n\n\n\n\n\n7777\n8888\n9999",
"After folding the second buffer, its text should not be displayed"
);
@@ -16426,7 +16536,7 @@ async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
});
assert_eq!(
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
"\n\n\n\n\n4444\n5555\n6666\n\n\n",
"\n\n\n\n4444\n5555\n6666\n\n",
"After unfolding the second buffer, its text should be displayed"
);
@@ -16435,7 +16545,7 @@ async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
});
assert_eq!(
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
"\n\n\n1111\n2222\n3333\n\n\n\n\n4444\n5555\n6666\n\n\n",
"\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
"After unfolding the first buffer, its text should be displayed"
);
@@ -16501,7 +16611,6 @@ async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut Test
EditorMode::Full,
multi_buffer,
Some(project.clone()),
true,
window,
cx,
)
@@ -16520,7 +16629,7 @@ async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut Test
editor.change_selections(None, window, cx, |s| s.select_ranges(Some(highlight_range)));
});
let full_text = format!("\n\n\n{sample_text}\n");
let full_text = format!("\n\n{sample_text}");
assert_eq!(
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
full_text,
@@ -16549,14 +16658,7 @@ async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContex
],
cx,
);
let mut editor = Editor::new(
EditorMode::Full,
multi_buffer.clone(),
None,
true,
window,
cx,
);
let mut editor = Editor::new(EditorMode::Full, multi_buffer.clone(), None, window, cx);
let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
// fold all but the second buffer, so that we test navigating between two
@@ -16868,7 +16970,7 @@ async fn assert_highlighted_edits(
) {
let window = cx.add_window(|window, cx| {
let buffer = MultiBuffer::build_simple(text, cx);
Editor::new(EditorMode::Full, buffer, None, true, window, cx)
Editor::new(EditorMode::Full, buffer, None, window, cx)
});
let cx = &mut VisualTestContext::from_window(*window, cx);

View File

@@ -15,15 +15,15 @@ use crate::{
inlay_hint_settings,
items::BufferSearchHighlights,
mouse_context_menu::{self, MenuPosition, MouseContextMenu},
scroll::{axis_pair, scroll_amount::ScrollAmount, AxisPair},
scroll::{axis_pair, scroll_amount::ScrollAmount, AxisPair, HorizontalLayoutDetails},
BlockId, ChunkReplacement, CursorShape, CustomBlockId, DisplayDiffHunk, DisplayPoint,
DisplayRow, DocumentHighlightRead, DocumentHighlightWrite, EditDisplayMode, Editor, EditorMode,
EditorSettings, EditorSnapshot, EditorStyle, ExpandExcerpts, FocusedBlock, GoToHunk,
GoToPreviousHunk, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput, HoveredCursor,
InlayHintRefreshReason, InlineCompletion, JumpData, LineDown, LineHighlight, LineUp,
OpenExcerpts, PageDown, PageUp, Point, RowExt, RowRangeExt, SelectPhase, SelectedTextHighlight,
Selection, SoftWrap, StickyHeaderExcerpt, ToPoint, ToggleFold, COLUMNAR_SELECTION_MODIFIERS,
CURSORS_VISIBLE_FOR, FILE_HEADER_HEIGHT, GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, MAX_LINE_LEN,
EditorSettings, EditorSnapshot, EditorStyle, FocusedBlock, GoToHunk, GoToPreviousHunk,
GutterDimensions, HalfPageDown, HalfPageUp, HandleInput, HoveredCursor, InlayHintRefreshReason,
InlineCompletion, JumpData, LineDown, LineHighlight, LineUp, OpenExcerpts, PageDown, PageUp,
Point, RowExt, RowRangeExt, SelectPhase, SelectedTextHighlight, Selection, SoftWrap,
StickyHeaderExcerpt, ToPoint, ToggleFold, COLUMNAR_SELECTION_MODIFIERS, CURSORS_VISIBLE_FOR,
FILE_HEADER_HEIGHT, GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, MAX_LINE_LEN,
MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
};
use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind};
@@ -33,10 +33,10 @@ use file_icons::FileIcons;
use git::{blame::BlameEntry, status::FileStatus, Oid};
use gpui::{
anchored, deferred, div, fill, linear_color_stop, linear_gradient, outline, point, px, quad,
relative, size, solid_background, svg, transparent_black, Action, AnyElement, App,
AvailableSpace, Axis, Bounds, ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners,
CursorStyle, DispatchPhase, Edges, Element, ElementInputHandler, Entity, Focusable as _,
FontId, GlobalElementId, Hitbox, Hsla, InteractiveElement, IntoElement, Keystroke, Length,
relative, size, solid_background, transparent_black, Action, AnyElement, App, AvailableSpace,
Axis, Bounds, ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners, CursorStyle,
DispatchPhase, Edges, Element, ElementInputHandler, Entity, Focusable as _, FontId,
GlobalElementId, Hitbox, Hsla, InteractiveElement, IntoElement, Keystroke, Length,
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad,
ParentElement, Pixels, ScrollDelta, ScrollWheelEvent, ShapedLine, SharedString, Size,
StatefulInteractiveElement, Style, Styled, Subscription, TextRun, TextStyleRefinement, Window,
@@ -45,15 +45,15 @@ use inline_completion::Direction;
use itertools::Itertools;
use language::{
language_settings::{
self, IndentGuideBackgroundColoring, IndentGuideColoring, IndentGuideSettings,
IndentGuideBackgroundColoring, IndentGuideColoring, IndentGuideSettings,
ShowWhitespaceSetting,
},
ChunkRendererContext,
};
use lsp::DiagnosticSeverity;
use multi_buffer::{
Anchor, ExcerptId, ExcerptInfo, ExpandExcerptDirection, MultiBufferPoint, MultiBufferRow,
RowInfo,
Anchor, ExcerptId, ExcerptInfo, ExpandExcerptDirection, ExpandInfo, MultiBufferPoint,
MultiBufferRow, RowInfo,
};
use project::project_settings::{self, GitGutterSetting, ProjectSettings};
use settings::Settings;
@@ -72,7 +72,7 @@ use sum_tree::Bias;
use text::BufferId;
use theme::{ActiveTheme, Appearance, BufferLineHeight, PlayerColor};
use ui::{
h_flex, prelude::*, ButtonLike, ButtonStyle, ContextMenu, IconButtonShape, KeyBinding, Tooltip,
h_flex, prelude::*, ButtonLike, ContextMenu, IconButtonShape, KeyBinding, Tooltip,
POPOVER_Y_PADDING,
};
use unicode_segmentation::UnicodeSegmentation;
@@ -390,6 +390,7 @@ impl EditorElement {
register_action(editor, window, Editor::set_mark);
register_action(editor, window, Editor::swap_selection_ends);
register_action(editor, window, Editor::show_completions);
register_action(editor, window, Editor::show_word_completions);
register_action(editor, window, Editor::toggle_code_actions);
register_action(editor, window, Editor::open_excerpts);
register_action(editor, window, Editor::open_excerpts_in_split);
@@ -1308,8 +1309,7 @@ impl EditorElement {
);
let scrollbar_settings = EditorSettings::get_global(cx).scrollbar;
let editor_show_scrollbars = self.editor.read(cx).show_scrollbars;
let show_scrollbars = editor_show_scrollbars
let show_scrollbars = self.editor.read(cx).show_scrollbars
&& match scrollbar_settings.show {
ShowScrollbar::Auto => {
let editor = self.editor.read(cx);
@@ -1339,11 +1339,10 @@ impl EditorElement {
ShowScrollbar::Always => true,
ShowScrollbar::Never => false,
};
let axes: AxisPair<bool> = scrollbar_settings.axes.into();
if snapshot.mode == EditorMode::Full
|| matches!(snapshot.mode, EditorMode::AutoHeight { .. }) && editor_show_scrollbars
{
if snapshot.mode != EditorMode::Full {
return axis_pair(None, None);
}
@@ -1517,6 +1516,19 @@ impl EditorElement {
}
}
fn prepaint_expand_toggles(
&self,
expand_toggles: &mut [Option<(AnyElement, gpui::Point<Pixels>)>],
window: &mut Window,
cx: &mut App,
) {
for (expand_toggle, origin) in expand_toggles.iter_mut().flatten() {
let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent);
expand_toggle.layout_as_root(available_space, window, cx);
expand_toggle.prepaint_as_root(*origin, available_space, window, cx);
}
}
fn prepaint_crease_trailers(
&self,
trailers: Vec<Option<AnyElement>>,
@@ -2011,6 +2023,7 @@ impl EditorElement {
&self,
line_height: Pixels,
range: Range<DisplayRow>,
row_infos: &[RowInfo],
scroll_pixel_position: gpui::Point<Pixels>,
gutter_dimensions: &GutterDimensions,
gutter_hitbox: &Hitbox,
@@ -2076,6 +2089,12 @@ impl EditorElement {
}
}
let display_row = multibuffer_point.to_display_point(snapshot).row();
if row_infos
.get((display_row - range.start).0 as usize)
.is_some_and(|row_info| row_info.expand_info.is_some())
{
return None;
}
let button = editor.render_run_indicator(
&self.style,
Some(display_row) == active_task_indicator_row,
@@ -2100,6 +2119,82 @@ impl EditorElement {
})
}
fn layout_excerpt_gutter(
&self,
gutter_hitbox: &Hitbox,
line_height: Pixels,
scroll_position: gpui::Point<f32>,
buffer_rows: &[RowInfo],
window: &mut Window,
cx: &mut App,
) -> Vec<Option<(AnyElement, gpui::Point<Pixels>)>> {
let editor_font_size = self.style.text.font_size.to_pixels(window.rem_size()) * 1.2;
let icon_size = editor_font_size.round();
let button_h_padding = ((icon_size - px(1.0)) / 2.0).round() - px(2.0);
let scroll_top = scroll_position.y * line_height;
let elements = buffer_rows
.into_iter()
.enumerate()
.map(|(ix, row_info)| {
let ExpandInfo {
excerpt_id,
direction,
} = row_info.expand_info?;
let icon_name = match direction {
ExpandExcerptDirection::Up => IconName::ExpandUp,
ExpandExcerptDirection::Down => IconName::ExpandDown,
ExpandExcerptDirection::UpAndDown => IconName::ExpandVertical,
};
let editor = self.editor.clone();
let max_row = self
.editor
.read(cx)
.buffer()
.read(cx)
.snapshot(cx)
.widest_line_number();
let is_wide = max_row > 999
&& row_info
.buffer_row
.is_some_and(|row| row.ilog10() == max_row.ilog10());
let toggle = IconButton::new(("expand", ix), icon_name)
.icon_color(Color::Custom(cx.theme().colors().editor_line_number))
.selected_icon_color(Color::Custom(cx.theme().colors().editor_foreground))
.icon_size(IconSize::Custom(rems(editor_font_size / window.rem_size())))
.width((icon_size + button_h_padding * 2).into())
.when(is_wide, |el| {
el.width((icon_size + button_h_padding).into())
})
.on_click(move |_, _, cx| {
editor.update(cx, |editor, cx| {
editor.expand_excerpt(excerpt_id, direction, cx);
});
})
.tooltip(Tooltip::for_action_title(
"Expand excerpt",
&crate::actions::ExpandExcerpts::default(),
))
.into_any_element();
let position = point(
px(1.),
ix as f32 * line_height - (scroll_top % line_height) + px(1.),
);
let origin = gutter_hitbox.origin + position;
Some((toggle, origin))
})
.collect();
elements
}
fn layout_code_actions_indicator(
&self,
line_height: Pixels,
@@ -2317,6 +2412,9 @@ impl EditorElement {
.into_iter()
.enumerate()
.map(|(ix, info)| {
if info.expand_info.is_some() {
return None;
}
let row = info.multibuffer_row?;
let display_row = DisplayRow(rows.start.0 + ix as u32);
let active = active_rows.contains_key(&display_row);
@@ -2339,6 +2437,9 @@ impl EditorElement {
buffer_rows
.into_iter()
.map(|row_info| {
if row_info.expand_info.is_some() {
return None;
}
if let Some(row) = row_info.multibuffer_row {
snapshot.render_crease_trailer(row, window, cx)
} else {
@@ -2516,25 +2617,11 @@ impl EditorElement {
Block::FoldedBuffer {
first_excerpt,
prev_excerpt,
show_excerpt_controls,
height,
..
} => {
let selected = selected_buffer_ids.contains(&first_excerpt.buffer_id);
let mut result = v_flex().id(block_id).w_full();
if let Some(prev_excerpt) = prev_excerpt {
if *show_excerpt_controls {
result = result.child(self.render_expand_excerpt_control(
block_id,
ExpandExcerptDirection::Down,
prev_excerpt.id,
gutter_dimensions,
window,
cx,
));
}
}
let result = v_flex().id(block_id).w_full();
let jump_data = header_jump_data(snapshot, block_row_start, *height, first_excerpt);
result
@@ -2542,6 +2629,7 @@ impl EditorElement {
first_excerpt,
true,
selected,
false,
jump_data,
window,
cx,
@@ -2550,28 +2638,14 @@ impl EditorElement {
}
Block::ExcerptBoundary {
prev_excerpt,
next_excerpt,
show_excerpt_controls,
height,
starts_new_buffer,
..
} => {
let color = cx.theme().colors().clone();
let mut result = v_flex().id(block_id).w_full();
if let Some(prev_excerpt) = prev_excerpt {
if *show_excerpt_controls {
result = result.child(self.render_expand_excerpt_control(
block_id,
ExpandExcerptDirection::Down,
prev_excerpt.id,
gutter_dimensions,
window,
cx,
));
}
}
if let Some(next_excerpt) = next_excerpt {
let jump_data =
header_jump_data(snapshot, block_row_start, *height, next_excerpt);
@@ -2584,6 +2658,7 @@ impl EditorElement {
next_excerpt,
false,
selected,
false,
jump_data,
window,
cx,
@@ -2592,40 +2667,17 @@ impl EditorElement {
result = result
.child(div().h(FILE_HEADER_HEIGHT as f32 * window.line_height()));
}
if *show_excerpt_controls {
result = result.child(self.render_expand_excerpt_control(
block_id,
ExpandExcerptDirection::Up,
next_excerpt.id,
gutter_dimensions,
window,
cx,
));
}
} else {
if *show_excerpt_controls {
result = result.child(
h_flex()
.relative()
.child(
div()
.top(px(0.))
.absolute()
.w_full()
.h_px()
.bg(color.border_variant),
)
.child(self.render_expand_excerpt_control(
block_id,
ExpandExcerptDirection::Up,
next_excerpt.id,
gutter_dimensions,
window,
cx,
)),
);
}
result = result.child(
h_flex().relative().child(
div()
.top(line_height / 2.)
.absolute()
.w_full()
.h_px()
.bg(color.border_variant),
),
);
};
}
@@ -2664,6 +2716,7 @@ impl EditorElement {
for_excerpt: &ExcerptInfo,
is_folded: bool,
is_selected: bool,
_is_sticky: bool,
jump_data: JumpData,
window: &mut Window,
cx: &mut App,
@@ -2710,7 +2763,7 @@ impl EditorElement {
.pl_0p5()
.pr_5()
.rounded_sm()
.shadow_md()
// .when(is_sticky, |el| el.shadow_md())
.border_1()
.map(|div| {
let border_color = if is_selected
@@ -2849,95 +2902,6 @@ impl EditorElement {
)
}
fn render_expand_excerpt_control(
&self,
block_id: BlockId,
direction: ExpandExcerptDirection,
excerpt_id: ExcerptId,
gutter_dimensions: &GutterDimensions,
window: &Window,
cx: &mut App,
) -> impl IntoElement {
let color = cx.theme().colors().clone();
let hover_color = color.border_variant.opacity(0.5);
let focus_handle = self.editor.focus_handle(cx).clone();
let icon_offset =
gutter_dimensions.width - (gutter_dimensions.left_padding + gutter_dimensions.margin);
let header_height = MULTI_BUFFER_EXCERPT_HEADER_HEIGHT as f32 * window.line_height();
let group_name = if direction == ExpandExcerptDirection::Down {
"expand-down"
} else {
"expand-up"
};
let expand_area = |id: SharedString| {
h_flex()
.id(id)
.w_full()
.cursor_pointer()
.block_mouse_down()
.on_mouse_move(|_, _, cx| cx.stop_propagation())
.hover(|style| style.bg(hover_color))
.tooltip({
let focus_handle = focus_handle.clone();
move |window, cx| {
Tooltip::for_action_in(
"Expand Excerpt",
&ExpandExcerpts { lines: 0 },
&focus_handle,
window,
cx,
)
}
})
};
expand_area(
format!(
"block-{}-{}",
block_id,
if direction == ExpandExcerptDirection::Down {
"down"
} else {
"up"
}
)
.into(),
)
.group(group_name)
.child(
h_flex()
.w(icon_offset)
.h(header_height)
.flex_none()
.justify_end()
.child(
ButtonLike::new("expand-icon")
.style(ButtonStyle::Transparent)
.child(
svg()
.path(if direction == ExpandExcerptDirection::Down {
IconName::ArrowDownFromLine.path()
} else {
IconName::ArrowUpFromLine.path()
})
.size(IconSize::XSmall.rems())
.text_color(cx.theme().colors().editor_line_number)
.group_hover(group_name, |style| {
style.text_color(cx.theme().colors().editor_active_line_number)
}),
),
),
)
.on_click(window.listener_for(&self.editor, {
move |editor, _, _, cx| {
editor.expand_excerpt(excerpt_id, direction, cx);
cx.stop_propagation();
}
}))
}
fn render_blocks(
&self,
rows: Range<DisplayRow>,
@@ -3169,7 +3133,6 @@ impl EditorElement {
&self,
StickyHeaderExcerpt {
excerpt,
next_excerpt_controls_present,
next_buffer_row,
}: StickyHeaderExcerpt<'_>,
scroll_position: f32,
@@ -3206,7 +3169,7 @@ impl EditorElement {
.top_0(),
)
.child(
self.render_buffer_header(excerpt, false, selected, jump_data, window, cx)
self.render_buffer_header(excerpt, false, selected, true, jump_data, window, cx)
.into_any_element(),
)
.into_any_element();
@@ -3216,11 +3179,7 @@ impl EditorElement {
if let Some(next_buffer_row) = next_buffer_row {
// Push up the sticky header when the excerpt is getting close to the top of the viewport
let mut max_row = next_buffer_row - FILE_HEADER_HEIGHT * 2;
if next_excerpt_controls_present {
max_row -= MULTI_BUFFER_EXCERPT_HEADER_HEIGHT;
}
let max_row = next_buffer_row - FILE_HEADER_HEIGHT * 2;
let offset = scroll_position - max_row as f32;
@@ -4516,6 +4475,12 @@ impl EditorElement {
}
});
window.with_element_namespace("expand_toggles", |window| {
for (expand_toggle, _) in layout.expand_toggles.iter_mut().flatten() {
expand_toggle.paint(window, cx);
}
});
for test_indicator in layout.test_indicators.iter_mut() {
test_indicator.paint(window, cx);
}
@@ -6893,6 +6858,18 @@ impl Element for EditorElement {
cx,
);
let mut expand_toggles =
window.with_element_namespace("expand_toggles", |window| {
self.layout_excerpt_gutter(
&gutter_hitbox,
line_height,
scroll_position,
&row_infos,
window,
cx,
)
});
let mut crease_toggles =
window.with_element_namespace("crease_toggles", |window| {
self.layout_crease_toggles(
@@ -6990,7 +6967,7 @@ impl Element for EditorElement {
let mut scroll_width = scroll_range_bounds.size.width;
let sticky_header_excerpt = if snapshot.buffer_snapshot.show_headers() {
snapshot.sticky_header_excerpt(start_row)
snapshot.sticky_header_excerpt(scroll_position.y)
} else {
None
};
@@ -7054,6 +7031,11 @@ impl Element for EditorElement {
);
self.editor.update(cx, |editor, cx| {
editor.scroll_manager.latest_horizontal_details = HorizontalLayoutDetails {
letter_width: letter_size.width.0,
editor_width: editor_width.0,
scroll_max: scroll_max.x,
};
let clamped = editor.scroll_manager.clamp_scroll_left(scroll_max.x);
let autoscrolled = if autoscroll_horizontally {
@@ -7307,7 +7289,14 @@ impl Element for EditorElement {
.tasks
.contains_key(&(buffer_id, row));
if !has_test_indicator {
let has_expand_indicator = row_infos
.get(
(newest_selection_head.row() - start_row).0
as usize,
)
.is_some_and(|row_info| row_info.expand_info.is_some());
if !has_test_indicator && !has_expand_indicator {
code_actions_indicator = self
.layout_code_actions_indicator(
line_height,
@@ -7340,6 +7329,7 @@ impl Element for EditorElement {
self.layout_run_indicators(
line_height,
start_row..end_row,
&row_infos,
scroll_pixel_position,
&gutter_dimensions,
&gutter_hitbox,
@@ -7402,6 +7392,10 @@ impl Element for EditorElement {
)
});
window.with_element_namespace("expand_toggles", |window| {
self.prepaint_expand_toggles(&mut expand_toggles, window, cx)
});
let invisible_symbol_font_size = font_size / 2.;
let tab_invisible = window
.text_system()
@@ -7503,6 +7497,7 @@ impl Element for EditorElement {
tab_invisible,
space_invisible,
sticky_buffer_header,
expand_toggles,
}
})
})
@@ -7676,6 +7671,7 @@ pub struct EditorLayout {
code_actions_indicator: Option<AnyElement>,
test_indicators: Vec<AnyElement>,
crease_toggles: Vec<Option<AnyElement>>,
expand_toggles: Vec<Option<(AnyElement, gpui::Point<Pixels>)>>,
diff_hunk_controls: Vec<AnyElement>,
crease_trailers: Vec<Option<CreaseTrailerLayout>>,
inline_completion_popover: Option<AnyElement>,
@@ -7703,7 +7699,7 @@ struct ColoredRange<T> {
color: Hsla,
}
#[derive(Clone, Debug)]
#[derive(Clone)]
struct ScrollbarLayout {
hitbox: Hitbox,
visible_range: Range<f32>,
@@ -8278,15 +8274,8 @@ fn compute_auto_height_layout(
let overscroll = size(em_width, px(0.));
let editor_width = text_width - gutter_dimensions.margin - overscroll.width - em_width;
let soft_wrap_disabled = editor
.soft_wrap_mode_override
.is_some_and(|soft_wrap| matches!(soft_wrap, language_settings::SoftWrap::None));
if !soft_wrap_disabled {
if editor.set_wrap_width(Some(editor_width), cx) {
snapshot = editor.snapshot(window, cx);
}
if editor.set_wrap_width(Some(editor_width), cx) {
snapshot = editor.snapshot(window, cx);
}
let scroll_height = Pixels::from(snapshot.max_point().row().next_row().0) * line_height;
@@ -8316,7 +8305,7 @@ mod tests {
init_test(cx, |_| {});
let window = cx.add_window(|window, cx| {
let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx);
Editor::new(EditorMode::Full, buffer, None, true, window, cx)
Editor::new(EditorMode::Full, buffer, None, window, cx)
});
let editor = window.root(cx).unwrap();
@@ -8416,7 +8405,7 @@ mod tests {
let window = cx.add_window(|window, cx| {
let buffer = MultiBuffer::build_simple(&(sample_text(6, 6, 'a') + "\n"), cx);
Editor::new(EditorMode::Full, buffer, None, true, window, cx)
Editor::new(EditorMode::Full, buffer, None, window, cx)
});
let cx = &mut VisualTestContext::from_window(*window, cx);
let editor = window.root(cx).unwrap();
@@ -8479,90 +8468,6 @@ mod tests {
state.active_rows.keys().cloned().collect::<Vec<_>>(),
vec![DisplayRow(0), DisplayRow(3), DisplayRow(5), DisplayRow(6)]
);
// multi-buffer support
// in DisplayPoint coordinates, this is what we're dealing with:
// 0: [[file
// 1: header
// 2: section]]
// 3: aaaaaa
// 4: bbbbbb
// 5: cccccc
// 6:
// 7: [[footer]]
// 8: [[header]]
// 9: ffffff
// 10: gggggg
// 11: hhhhhh
// 12:
// 13: [[footer]]
// 14: [[file
// 15: header
// 16: section]]
// 17: bbbbbb
// 18: cccccc
// 19: dddddd
// 20: [[footer]]
let window = cx.add_window(|window, cx| {
let buffer = MultiBuffer::build_multi(
[
(
&(sample_text(8, 6, 'a') + "\n"),
vec![
Point::new(0, 0)..Point::new(3, 0),
Point::new(4, 0)..Point::new(7, 0),
],
),
(
&(sample_text(8, 6, 'a') + "\n"),
vec![Point::new(1, 0)..Point::new(3, 0)],
),
],
cx,
);
Editor::new(EditorMode::Full, buffer, None, true, window, cx)
});
let editor = window.root(cx).unwrap();
let style = cx.update(|_, cx| editor.read(cx).style().unwrap().clone());
let _state = window.update(cx, |editor, window, cx| {
editor.cursor_shape = CursorShape::Block;
editor.change_selections(None, window, cx, |s| {
s.select_display_ranges([
DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(7), 0),
DisplayPoint::new(DisplayRow(10), 0)..DisplayPoint::new(DisplayRow(13), 0),
]);
});
});
let (_, state) = cx.draw(
point(px(500.), px(500.)),
size(px(500.), px(500.)),
|_, _| EditorElement::new(&editor, style),
);
assert_eq!(state.selections.len(), 1);
let local_selections = &state.selections[0].1;
assert_eq!(local_selections.len(), 2);
// moves cursor on excerpt boundary back a line
// and doesn't allow selection to bleed through
assert_eq!(
local_selections[0].range,
DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(7), 0)
);
assert_eq!(
local_selections[0].head,
DisplayPoint::new(DisplayRow(6), 0)
);
// moves cursor on buffer boundary back two lines
// and doesn't allow selection to bleed through
assert_eq!(
local_selections[1].range,
DisplayPoint::new(DisplayRow(10), 0)..DisplayPoint::new(DisplayRow(13), 0)
);
assert_eq!(
local_selections[1].head,
DisplayPoint::new(DisplayRow(12), 0)
);
}
#[gpui::test]
@@ -8571,7 +8476,7 @@ mod tests {
let window = cx.add_window(|window, cx| {
let buffer = MultiBuffer::build_simple("", cx);
Editor::new(EditorMode::Full, buffer, None, true, window, cx)
Editor::new(EditorMode::Full, buffer, None, window, cx)
});
let cx = &mut VisualTestContext::from_window(*window, cx);
let editor = window.root(cx).unwrap();
@@ -8797,7 +8702,7 @@ mod tests {
);
let window = cx.add_window(|window, cx| {
let buffer = MultiBuffer::build_simple(input_text, cx);
Editor::new(editor_mode, buffer, None, true, window, cx)
Editor::new(editor_mode, buffer, None, window, cx)
});
let cx = &mut VisualTestContext::from_window(*window, cx);
let editor = window.root(cx).unwrap();

View File

@@ -488,7 +488,7 @@ async fn parse_commit_messages(
},
))
} else {
continue;
None
};
let remote = parsed_remote_url

View File

@@ -2618,7 +2618,7 @@ pub mod tests {
cx.executor().run_until_parked();
let editor = cx.add_window(|window, cx| {
Editor::for_multibuffer(multibuffer, Some(project.clone()), true, window, cx)
Editor::for_multibuffer(multibuffer, Some(project.clone()), window, cx)
});
let editor_edited = Arc::new(AtomicBool::new(false));
@@ -2830,7 +2830,6 @@ pub mod tests {
"main hint #5".to_string(),
"other hint(edited) #0".to_string(),
"other hint(edited) #1".to_string(),
"other hint(edited) #2".to_string(),
];
assert_eq!(
expected_hints,
@@ -2921,7 +2920,7 @@ pub mod tests {
cx.executor().run_until_parked();
let editor = cx.add_window(|window, cx| {
Editor::for_multibuffer(multibuffer, Some(project.clone()), true, window, cx)
Editor::for_multibuffer(multibuffer, Some(project.clone()), window, cx)
});
let editor_edited = Arc::new(AtomicBool::new(false));
let fake_server = fake_servers.next().await.unwrap();

View File

@@ -129,13 +129,8 @@ impl FollowableItem for Editor {
});
cx.new(|cx| {
let mut editor = Editor::for_multibuffer(
multibuffer,
Some(project.clone()),
true,
window,
cx,
);
let mut editor =
Editor::for_multibuffer(multibuffer, Some(project.clone()), window, cx);
editor.remote_id = Some(remote_id);
editor
})

View File

@@ -893,8 +893,6 @@ mod tests {
font,
font_size,
None,
true,
1,
1,
1,
FoldPlaceholder::test(),
@@ -1110,138 +1108,136 @@ mod tests {
font,
px(14.0),
None,
true,
0,
2,
0,
FoldPlaceholder::test(),
cx,
)
});
let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
assert_eq!(snapshot.text(), "\n\nabc\ndefg\n\n\nhijkl\nmn");
assert_eq!(snapshot.text(), "abc\ndefg\nhijkl\nmn");
let col_2_x = snapshot
.x_for_display_point(DisplayPoint::new(DisplayRow(2), 2), &text_layout_details);
.x_for_display_point(DisplayPoint::new(DisplayRow(0), 2), &text_layout_details);
// Can't move up into the first excerpt's header
assert_eq!(
up(
&snapshot,
DisplayPoint::new(DisplayRow(2), 2),
DisplayPoint::new(DisplayRow(0), 2),
SelectionGoal::HorizontalPosition(col_2_x.0),
false,
&text_layout_details
),
(
DisplayPoint::new(DisplayRow(2), 0),
DisplayPoint::new(DisplayRow(0), 0),
SelectionGoal::HorizontalPosition(col_2_x.0),
),
);
assert_eq!(
up(
&snapshot,
DisplayPoint::new(DisplayRow(2), 0),
DisplayPoint::new(DisplayRow(0), 0),
SelectionGoal::None,
false,
&text_layout_details
),
(
DisplayPoint::new(DisplayRow(2), 0),
DisplayPoint::new(DisplayRow(0), 0),
SelectionGoal::HorizontalPosition(0.0),
),
);
let col_4_x = snapshot
.x_for_display_point(DisplayPoint::new(DisplayRow(3), 4), &text_layout_details);
.x_for_display_point(DisplayPoint::new(DisplayRow(1), 4), &text_layout_details);
// Move up and down within first excerpt
assert_eq!(
up(
&snapshot,
DisplayPoint::new(DisplayRow(3), 4),
DisplayPoint::new(DisplayRow(1), 4),
SelectionGoal::HorizontalPosition(col_4_x.0),
false,
&text_layout_details
),
(
DisplayPoint::new(DisplayRow(2), 3),
DisplayPoint::new(DisplayRow(0), 3),
SelectionGoal::HorizontalPosition(col_4_x.0)
),
);
assert_eq!(
down(
&snapshot,
DisplayPoint::new(DisplayRow(2), 3),
DisplayPoint::new(DisplayRow(0), 3),
SelectionGoal::HorizontalPosition(col_4_x.0),
false,
&text_layout_details
),
(
DisplayPoint::new(DisplayRow(3), 4),
DisplayPoint::new(DisplayRow(1), 4),
SelectionGoal::HorizontalPosition(col_4_x.0)
),
);
let col_5_x = snapshot
.x_for_display_point(DisplayPoint::new(DisplayRow(6), 5), &text_layout_details);
.x_for_display_point(DisplayPoint::new(DisplayRow(2), 5), &text_layout_details);
// Move up and down across second excerpt's header
assert_eq!(
up(
&snapshot,
DisplayPoint::new(DisplayRow(6), 5),
DisplayPoint::new(DisplayRow(2), 5),
SelectionGoal::HorizontalPosition(col_5_x.0),
false,
&text_layout_details
),
(
DisplayPoint::new(DisplayRow(3), 4),
DisplayPoint::new(DisplayRow(1), 4),
SelectionGoal::HorizontalPosition(col_5_x.0)
),
);
assert_eq!(
down(
&snapshot,
DisplayPoint::new(DisplayRow(3), 4),
DisplayPoint::new(DisplayRow(1), 4),
SelectionGoal::HorizontalPosition(col_5_x.0),
false,
&text_layout_details
),
(
DisplayPoint::new(DisplayRow(6), 5),
DisplayPoint::new(DisplayRow(2), 5),
SelectionGoal::HorizontalPosition(col_5_x.0)
),
);
let max_point_x = snapshot
.x_for_display_point(DisplayPoint::new(DisplayRow(7), 2), &text_layout_details);
.x_for_display_point(DisplayPoint::new(DisplayRow(3), 2), &text_layout_details);
// Can't move down off the end, and attempting to do so leaves the selection goal unchanged
assert_eq!(
down(
&snapshot,
DisplayPoint::new(DisplayRow(7), 0),
DisplayPoint::new(DisplayRow(3), 0),
SelectionGoal::HorizontalPosition(0.0),
false,
&text_layout_details
),
(
DisplayPoint::new(DisplayRow(7), 2),
DisplayPoint::new(DisplayRow(3), 2),
SelectionGoal::HorizontalPosition(0.0)
),
);
assert_eq!(
down(
&snapshot,
DisplayPoint::new(DisplayRow(7), 2),
DisplayPoint::new(DisplayRow(3), 2),
SelectionGoal::HorizontalPosition(max_point_x.0),
false,
&text_layout_details
),
(
DisplayPoint::new(DisplayRow(7), 2),
DisplayPoint::new(DisplayRow(3), 2),
SelectionGoal::HorizontalPosition(max_point_x.0)
),
);

View File

@@ -62,8 +62,7 @@ impl ProposedChangesEditor {
let (recalculate_diffs_tx, mut recalculate_diffs_rx) = mpsc::unbounded();
let mut this = Self {
editor: cx.new(|cx| {
let mut editor =
Editor::for_multibuffer(multibuffer.clone(), project, true, window, cx);
let mut editor = Editor::for_multibuffer(multibuffer.clone(), project, window, cx);
editor.set_expand_all_diff_hunks(cx);
editor.set_completion_provider(None);
editor.clear_code_action_providers();

View File

@@ -86,7 +86,7 @@ pub fn expand_macro_recursively(
cx.new(|cx| MultiBuffer::singleton(buffer, cx).with_title(macro_expansion.name));
workspace.add_item_to_active_pane(
Box::new(cx.new(|cx| {
let mut editor = Editor::for_multibuffer(multibuffer, None, false, window, cx);
let mut editor = Editor::for_multibuffer(multibuffer, None, window, cx);
editor.set_read_only(true);
editor
})),

View File

@@ -172,6 +172,13 @@ impl OngoingScroll {
}
}
#[derive(Default, Debug)]
pub struct HorizontalLayoutDetails {
pub letter_width: f32,
pub editor_width: f32,
pub scroll_max: f32,
}
pub struct ScrollManager {
pub(crate) vertical_scroll_margin: f32,
anchor: ScrollAnchor,
@@ -183,6 +190,7 @@ pub struct ScrollManager {
dragging_scrollbar: AxisPair<bool>,
visible_line_count: Option<f32>,
forbid_vertical_scroll: bool,
pub(crate) latest_horizontal_details: HorizontalLayoutDetails,
}
impl ScrollManager {
@@ -198,6 +206,7 @@ impl ScrollManager {
last_autoscroll: None,
visible_line_count: None,
forbid_vertical_scroll: false,
latest_horizontal_details: Default::default(),
}
}
@@ -379,6 +388,15 @@ impl ScrollManager {
cx.notify();
}
pub fn horizontal_scroll(
&mut self,
f: impl Fn(f32, &HorizontalLayoutDetails) -> f32,
cx: &mut Context<Editor>,
) {
self.anchor.offset.x = f(self.anchor.offset.x, &self.latest_horizontal_details);
cx.notify();
}
pub fn clamp_scroll_left(&mut self, max: f32) -> bool {
if max < self.anchor.offset.x {
self.anchor.offset.x = max;

View File

@@ -61,8 +61,6 @@ pub fn marked_display_snapshot(
font,
font_size,
None,
true,
1,
1,
1,
FoldPlaceholder::test(),
@@ -108,7 +106,7 @@ pub(crate) fn build_editor(
window: &mut Window,
cx: &mut Context<Editor>,
) -> Editor {
Editor::new(EditorMode::Full, buffer, None, true, window, cx)
Editor::new(EditorMode::Full, buffer, None, window, cx)
}
pub(crate) fn build_editor_with_project(
@@ -117,5 +115,5 @@ pub(crate) fn build_editor_with_project(
window: &mut Window,
cx: &mut Context<Editor>,
) -> Editor {
Editor::new(EditorMode::Full, buffer, Some(project), true, window, cx)
Editor::new(EditorMode::Full, buffer, Some(project), window, cx)
}

View File

@@ -1,4 +1,4 @@
use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Global, ReadGlobal as _};
use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Global};
pub fn init(cx: &mut App) {
let extension_events = cx.new(ExtensionEvents::new);
@@ -14,8 +14,10 @@ pub struct ExtensionEvents;
impl ExtensionEvents {
/// Returns the global [`ExtensionEvents`].
pub fn global(cx: &App) -> Entity<Self> {
GlobalExtensionEvents::global(cx).0.clone()
pub fn try_global(cx: &App) -> Option<Entity<Self>> {
return cx
.try_global::<GlobalExtensionEvents>()
.map(|g| g.0.clone());
}
fn new(_cx: &mut Context<Self>) -> Self {
@@ -29,7 +31,7 @@ impl ExtensionEvents {
#[derive(Clone)]
pub enum Event {
ExtensionsUpdated,
ExtensionsInstalledChanged,
}
impl EventEmitter<Event> for ExtensionEvents {}

View File

@@ -127,6 +127,7 @@ pub enum ExtensionOperation {
#[derive(Clone)]
pub enum Event {
ExtensionsUpdated,
StartedReloading,
ExtensionInstalled(Arc<str>),
ExtensionFailedToLoad(Arc<str>),
@@ -1213,9 +1214,7 @@ impl ExtensionStore {
self.extension_index = new_index;
cx.notify();
ExtensionEvents::global(cx).update(cx, |this, cx| {
this.emit(extension::Event::ExtensionsUpdated, cx)
});
cx.emit(Event::ExtensionsUpdated);
cx.spawn(|this, mut cx| async move {
cx.background_spawn({
@@ -1317,6 +1316,12 @@ impl ExtensionStore {
this.proxy.set_extensions_loaded();
this.proxy.reload_current_theme(cx);
this.proxy.reload_current_icon_theme(cx);
if let Some(events) = ExtensionEvents::try_global(cx) {
events.update(cx, |this, cx| {
this.emit(extension::Event::ExtensionsInstalledChanged, cx)
});
}
})
.ok();
})

View File

@@ -714,6 +714,7 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) {
})
.await
.unwrap()
.unwrap()
.into_iter()
.map(|c| c.label.text)
.collect::<Vec<_>>();

View File

@@ -17,7 +17,6 @@ client.workspace = true
collections.workspace = true
db.workspace = true
editor.workspace = true
extension.workspace = true
extension_host.workspace = true
feature_flags.workspace = true
fs.workspace = true

View File

@@ -9,7 +9,6 @@ use std::{ops::Range, sync::Arc};
use client::{ExtensionMetadata, ExtensionProvides};
use collections::{BTreeMap, BTreeSet};
use editor::{Editor, EditorElement, EditorStyle};
use extension::ExtensionEvents;
use extension_host::{ExtensionManifest, ExtensionOperation, ExtensionStore};
use feature_flags::FeatureFlagAppExt as _;
use fuzzy::{match_strings, StringMatchCandidate};
@@ -213,7 +212,7 @@ pub struct ExtensionsPage {
query_editor: Entity<Editor>,
query_contains_error: bool,
provides_filter: Option<ExtensionProvides>,
_subscriptions: Vec<gpui::Subscription>,
_subscriptions: [gpui::Subscription; 2],
extension_fetch_task: Option<Task<()>>,
upsells: BTreeSet<Feature>,
}
@@ -227,12 +226,15 @@ impl ExtensionsPage {
cx.new(|cx| {
let store = ExtensionStore::global(cx);
let workspace_handle = workspace.weak_handle();
let subscriptions = vec![
let subscriptions = [
cx.observe(&store, |_: &mut Self, _, cx| cx.notify()),
cx.subscribe_in(
&store,
window,
move |this, _, event, window, cx| match event {
extension_host::Event::ExtensionsUpdated => {
this.fetch_extensions_debounced(cx)
}
extension_host::Event::ExtensionInstalled(extension_id) => this
.on_extension_installed(
workspace_handle.clone(),
@@ -243,15 +245,6 @@ impl ExtensionsPage {
_ => {}
},
),
cx.subscribe_in(
&ExtensionEvents::global(cx),
window,
move |this, _, event, _window, cx| match event {
extension::Event::ExtensionsUpdated => {
this.fetch_extensions_debounced(cx);
}
},
),
];
let query_editor = cx.new(|cx| {

View File

@@ -795,9 +795,8 @@ impl GitRepository for RealGitRepository {
cx: AsyncApp,
) -> BoxFuture<Result<()>> {
let working_directory = self.working_directory();
let git_binary_path = self.git_binary_path.clone();
cx.background_spawn(async move {
let mut cmd = new_smol_command(&git_binary_path);
let mut cmd = new_smol_command("git");
cmd.current_dir(&working_directory?)
.envs(env)
.args(["commit", "--quiet", "-m"])

View File

@@ -35,7 +35,6 @@ use gpui::{
Transformation, UniformListScrollHandle, WeakEntity,
};
use itertools::Itertools;
use language::language_settings::SoftWrap;
use language::{Buffer, File};
use language_model::{
LanguageModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role,
@@ -359,7 +358,6 @@ pub(crate) fn commit_message_editor(
EditorMode::AutoHeight { max_lines },
buffer,
None,
false,
window,
cx,
);
@@ -369,8 +367,6 @@ pub(crate) fn commit_message_editor(
commit_editor.set_show_wrap_guides(false, cx);
commit_editor.set_show_indent_guides(false, cx);
commit_editor.set_hard_wrap(Some(72), cx);
commit_editor.set_soft_wrap_mode(SoftWrap::None, cx);
commit_editor.set_show_scrollbars(true, cx);
let placeholder = placeholder.unwrap_or("Enter commit message");
commit_editor.set_placeholder_text(placeholder, cx);
commit_editor
@@ -3051,21 +3047,24 @@ impl GitPanel {
"No Git repositories"
},
))
.children(self.active_repository.is_none().then(|| {
h_flex().w_full().justify_around().child(
panel_filled_button("Initialize Repository")
.tooltip(Tooltip::for_action_title_in(
"git init",
&git::Init,
&self.focus_handle,
))
.on_click(move |_, _, cx| {
cx.defer(move |cx| {
cx.dispatch_action(&git::Init);
})
}),
)
}))
.children({
let worktree_count = self.project.read(cx).visible_worktrees(cx).count();
(worktree_count > 0 && self.active_repository.is_none()).then(|| {
h_flex().w_full().justify_around().child(
panel_filled_button("Initialize Repository")
.tooltip(Tooltip::for_action_title_in(
"git init",
&git::Init,
&self.focus_handle,
))
.on_click(move |_, _, cx| {
cx.defer(move |cx| {
cx.dispatch_action(&git::Init);
})
}),
)
})
})
.text_ui_sm(cx)
.mx_auto()
.text_color(Color::Placeholder.color(cx)),

View File

@@ -141,13 +141,8 @@ impl ProjectDiff {
let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
let editor = cx.new(|cx| {
let mut diff_display_editor = Editor::for_multibuffer(
multibuffer.clone(),
Some(project.clone()),
true,
window,
cx,
);
let mut diff_display_editor =
Editor::for_multibuffer(multibuffer.clone(), Some(project.clone()), window, cx);
diff_display_editor.disable_inline_diagnostics();
diff_display_editor.set_expand_all_diff_hunks(cx);
diff_display_editor.register_addon(GitPanelAddon {
@@ -278,7 +273,7 @@ impl ProjectDiff {
has_staged_hunks = true;
has_unstaged_hunks = true;
}
DiffHunkSecondaryStatus::None
DiffHunkSecondaryStatus::NoSecondaryHunk
| DiffHunkSecondaryStatus::SecondaryHunkRemovalPending => {
has_staged_hunks = true;
}
@@ -308,7 +303,7 @@ impl ProjectDiff {
fn handle_editor_event(
&mut self,
_: &Entity<Editor>,
editor: &Entity<Editor>,
event: &EditorEvent,
window: &mut Window,
cx: &mut Context<Self>,
@@ -330,6 +325,11 @@ impl ProjectDiff {
}
_ => {}
}
if editor.focus_handle(cx).contains_focused(window, cx) {
if self.multibuffer.read(cx).is_empty() {
self.focus_handle.focus(window)
}
}
}
fn load_buffers(&mut self, cx: &mut Context<Self>) -> Vec<Task<Result<DiffBuffer>>> {

View File

@@ -98,8 +98,8 @@ profiling.workspace = true
rand = { optional = true, workspace = true }
raw-window-handle = "0.6"
refineable.workspace = true
resvg = { version = "0.44.0", default-features = false }
usvg = { version = "0.44.0", default-features = false }
resvg = { version = "0.45.0", default-features = false, features = ["text", "system-fonts", "memmap-fonts"] }
usvg = { version = "0.45.0", default-features = false }
schemars.workspace = true
seahash = "4.1"
semantic_version.workspace = true

View File

@@ -1046,7 +1046,7 @@ impl App {
&self.foreground_executor
}
/// Spawns the future returned by the given function on the thread pool. The closure will be invoked
/// Spawns the future returned by the given function on the main thread. The closure will be invoked
/// with [AsyncApp], which allows the application state to be accessed across await points.
#[track_caller]
pub fn spawn<Fut, R>(&self, f: impl FnOnce(AsyncApp) -> Fut) -> Task<R>

View File

@@ -512,6 +512,8 @@ impl MacWindow {
unsafe {
let pool = NSAutoreleasePool::new(nil);
let () = msg_send![class!(NSWindow), setAllowsAutomaticWindowTabbing: NO];
let mut style_mask;
if let Some(titlebar) = titlebar.as_ref() {
style_mask = NSWindowStyleMask::NSClosableWindowMask

View File

@@ -1,7 +1,10 @@
use crate::{AssetSource, DevicePixels, IsZero, Result, SharedString, Size};
use anyhow::anyhow;
use resvg::tiny_skia::Pixmap;
use std::{hash::Hash, sync::Arc};
use std::{
hash::Hash,
sync::{Arc, LazyLock},
};
/// When rendering SVGs, we render them at twice the size to get a higher-quality result.
pub const SMOOTH_SVG_SCALE_FACTOR: f32 = 2.;
@@ -15,6 +18,7 @@ pub(crate) struct RenderSvgParams {
#[derive(Clone)]
pub struct SvgRenderer {
asset_source: Arc<dyn AssetSource>,
usvg_options: Arc<usvg::Options<'static>>,
}
pub enum SvgSize {
@@ -24,7 +28,31 @@ pub enum SvgSize {
impl SvgRenderer {
pub fn new(asset_source: Arc<dyn AssetSource>) -> Self {
Self { asset_source }
let font_db = LazyLock::new(|| {
let mut db = usvg::fontdb::Database::new();
db.load_system_fonts();
Arc::new(db)
});
let default_font_resolver = usvg::FontResolver::default_font_selector();
let font_resolver = Box::new(
move |font: &usvg::Font, db: &mut Arc<usvg::fontdb::Database>| {
if db.is_empty() {
*db = font_db.clone();
}
default_font_resolver(font, db)
},
);
let options = usvg::Options {
font_resolver: usvg::FontResolver {
select_font: font_resolver,
select_fallback: usvg::FontResolver::default_fallback_selector(),
},
..Default::default()
};
Self {
asset_source,
usvg_options: Arc::new(options),
}
}
pub(crate) fn render(&self, params: &RenderSvgParams) -> Result<Option<Vec<u8>>> {
@@ -49,7 +77,7 @@ impl SvgRenderer {
}
pub fn render_pixmap(&self, bytes: &[u8], size: SvgSize) -> Result<Pixmap, usvg::Error> {
let tree = usvg::Tree::from_data(bytes, &usvg::Options::default())?;
let tree = usvg::Tree::from_data(bytes, &self.usvg_options)?;
let size = match size {
SvgSize::Size(size) => size,

View File

@@ -18,7 +18,7 @@ use gpui::{
use indoc::indoc;
use language::{
language_settings::{self, all_language_settings, AllLanguageSettings, EditPredictionProvider},
File, Language,
EditPredictionsMode, File, Language,
};
use regex::Regex;
use settings::{update_settings_file, Settings, SettingsStore};
@@ -456,13 +456,47 @@ impl InlineCompletionButton {
}
let settings = AllLanguageSettings::get_global(cx);
let globally_enabled = settings.show_edit_predictions(None, cx);
menu = menu.toggleable_entry("All Files", globally_enabled, IconPosition::Start, None, {
let fs = fs.clone();
move |_, cx| toggle_inline_completions_globally(fs.clone(), cx)
});
menu = menu.separator().header("Privacy Settings");
menu = menu.separator().header("Display Modes");
let current_mode = settings.edit_predictions_mode();
let subtle_mode = matches!(current_mode, EditPredictionsMode::Subtle);
let eager_mode = matches!(current_mode, EditPredictionsMode::Eager);
menu = menu.item(
ContextMenuEntry::new("Eager")
.toggleable(IconPosition::Start, eager_mode)
.documentation_aside(move |_| {
Label::new("Display predictions inline when there are no language server completions available.").into_any_element()
})
.handler({
let fs = fs.clone();
move |_, cx| {
toggle_edit_prediction_mode(fs.clone(), EditPredictionsMode::Eager, cx)
}
}),
);
menu = menu.item(
ContextMenuEntry::new("Subtle")
.toggleable(IconPosition::Start, subtle_mode)
.documentation_aside(move |_| {
Label::new("Display predictions inline only when holding a modifier key (alt by default).").into_any_element()
})
.handler({
let fs = fs.clone();
move |_, cx| {
toggle_edit_prediction_mode(fs.clone(), EditPredictionsMode::Subtle, cx)
}
}),
);
menu = menu.separator().header("Privacy Settings");
if let Some(provider) = &self.edit_prediction_provider {
let data_collection = provider.data_collection_state(cx);
if data_collection.is_supported() {
@@ -590,44 +624,6 @@ impl InlineCompletionButton {
);
}
if cx.has_flag::<feature_flags::PredictEditsNonEagerModeFeatureFlag>() {
let is_eager_preview_enabled = match settings.edit_predictions_mode() {
language::EditPredictionsMode::Subtle => false,
language::EditPredictionsMode::Eager => true,
};
menu = menu.separator().toggleable_entry(
"Eager Preview Mode",
is_eager_preview_enabled,
IconPosition::Start,
None,
{
let fs = fs.clone();
move |_window, cx| {
update_settings_file::<AllLanguageSettings>(
fs.clone(),
cx,
move |settings, _cx| {
let new_mode = match is_eager_preview_enabled {
true => language::EditPredictionsMode::Subtle,
false => language::EditPredictionsMode::Eager,
};
if let Some(edit_predictions) = settings.edit_predictions.as_mut() {
edit_predictions.mode = new_mode;
} else {
settings.edit_predictions =
Some(language_settings::EditPredictionSettingsContent {
mode: new_mode,
..Default::default()
});
}
},
);
}
},
);
}
if let Some(editor_focus_handle) = self.editor_focus_handle.clone() {
menu = menu
.separator()
@@ -861,3 +857,22 @@ fn hide_copilot(fs: Arc<dyn Fs>, cx: &mut App) {
.edit_prediction_provider = Some(EditPredictionProvider::None);
});
}
fn toggle_edit_prediction_mode(fs: Arc<dyn Fs>, mode: EditPredictionsMode, cx: &mut App) {
let settings = AllLanguageSettings::get_global(cx);
let current_mode = settings.edit_predictions_mode();
if current_mode != mode {
update_settings_file::<AllLanguageSettings>(fs, cx, move |settings, _cx| {
if let Some(edit_predictions) = settings.edit_predictions.as_mut() {
edit_predictions.mode = mode;
} else {
settings.edit_predictions =
Some(language_settings::EditPredictionSettingsContent {
mode,
..Default::default()
});
}
});
}
}

View File

@@ -526,8 +526,8 @@ impl DerefMut for ChunkRendererContext<'_, '_> {
/// A set of edits to a given version of a buffer, computed asynchronously.
#[derive(Debug)]
pub struct Diff {
pub(crate) base_version: clock::Global,
line_ending: LineEnding,
pub base_version: clock::Global,
pub line_ending: LineEnding,
pub edits: Vec<(Range<usize>, Arc<str>)>,
}
@@ -4146,12 +4146,9 @@ impl BufferSnapshot {
}
}
pub fn words_in_range(
&self,
query: Option<&str>,
range: Range<usize>,
) -> HashMap<String, Range<Anchor>> {
if query.map_or(false, |query| query.is_empty()) {
pub fn words_in_range(&self, query: WordsQuery) -> HashMap<String, Range<Anchor>> {
let query_str = query.fuzzy_contents;
if query_str.map_or(false, |query| query.is_empty()) {
return HashMap::default();
}
@@ -4161,13 +4158,13 @@ impl BufferSnapshot {
}));
let mut query_ix = 0;
let query = query.map(|query| query.chars().collect::<Vec<_>>());
let query_len = query.as_ref().map_or(0, |query| query.len());
let query_chars = query_str.map(|query| query.chars().collect::<Vec<_>>());
let query_len = query_chars.as_ref().map_or(0, |query| query.len());
let mut words = HashMap::default();
let mut current_word_start_ix = None;
let mut chunk_ix = range.start;
for chunk in self.chunks(range, false) {
let mut chunk_ix = query.range.start;
for chunk in self.chunks(query.range, false) {
for (i, c) in chunk.text.char_indices() {
let ix = chunk_ix + i;
if classifier.is_word(c) {
@@ -4175,12 +4172,9 @@ impl BufferSnapshot {
current_word_start_ix = Some(ix);
}
if let Some(query) = &query {
if let Some(query_chars) = &query_chars {
if query_ix < query_len {
let query_c = query.get(query_ix).expect(
"query_ix is a vec of chars, which we access only if before the end",
);
if c.to_lowercase().eq(query_c.to_lowercase()) {
if c.to_lowercase().eq(query_chars[query_ix].to_lowercase()) {
query_ix += 1;
}
}
@@ -4189,10 +4183,16 @@ impl BufferSnapshot {
} else if let Some(word_start) = current_word_start_ix.take() {
if query_ix == query_len {
let word_range = self.anchor_before(word_start)..self.anchor_after(ix);
words.insert(
self.text_for_range(word_start..ix).collect::<String>(),
word_range,
);
let mut word_text = self.text_for_range(word_start..ix).peekable();
let first_char = word_text
.peek()
.and_then(|first_chunk| first_chunk.chars().next());
// Skip empty and "words" starting with digits as a heuristic to reduce useless completions
if !query.skip_digits
|| first_char.map_or(true, |first_char| !first_char.is_digit(10))
{
words.insert(word_text.collect(), word_range);
}
}
}
query_ix = 0;
@@ -4204,6 +4204,15 @@ impl BufferSnapshot {
}
}
pub struct WordsQuery<'a> {
/// Only returns words with all chars from the fuzzy string in them.
pub fuzzy_contents: Option<&'a str>,
/// Skips words that start with a digit.
pub skip_digits: bool,
/// Buffer offset range, to look for words.
pub range: Range<usize>,
}
fn indent_size_for_line(text: &text::BufferSnapshot, row: u32) -> IndentSize {
indent_size_for_text(text.chars_at(Point::new(row, 0)))
}

View File

@@ -3145,7 +3145,11 @@ fn test_trailing_whitespace_ranges(mut rng: StdRng) {
fn test_words_in_range(cx: &mut gpui::App) {
init_settings(cx, |_| {});
let contents = r#"let word=öäpple.bar你 Öäpple word2-öÄpPlE-Pizza-word ÖÄPPLE word"#;
// The first line are words excluded from the results with heuristics, we do not expect them in the test assertions.
let contents = r#"
0_isize 123 3.4 4  
let word=öäpple.bar你 Öäpple word2-öÄpPlE-Pizza-word ÖÄPPLE word
"#;
let buffer = cx.new(|cx| {
let buffer = Buffer::local(contents, cx).with_language(Arc::new(rust_lang()), cx);
@@ -3159,7 +3163,11 @@ fn test_words_in_range(cx: &mut gpui::App) {
assert_eq!(
BTreeSet::from_iter(["Pizza".to_string()]),
snapshot
.words_in_range(Some("piz"), 0..snapshot.len())
.words_in_range(WordsQuery {
fuzzy_contents: Some("piz"),
skip_digits: true,
range: 0..snapshot.len(),
})
.into_keys()
.collect::<BTreeSet<_>>()
);
@@ -3171,7 +3179,11 @@ fn test_words_in_range(cx: &mut gpui::App) {
"ÖÄPPLE".to_string(),
]),
snapshot
.words_in_range(Some("öp"), 0..snapshot.len())
.words_in_range(WordsQuery {
fuzzy_contents: Some("öp"),
skip_digits: true,
range: 0..snapshot.len(),
})
.into_keys()
.collect::<BTreeSet<_>>()
);
@@ -3183,28 +3195,44 @@ fn test_words_in_range(cx: &mut gpui::App) {
"öäpple".to_string(),
]),
snapshot
.words_in_range(Some("öÄ"), 0..snapshot.len())
.words_in_range(WordsQuery {
fuzzy_contents: Some("öÄ"),
skip_digits: true,
range: 0..snapshot.len(),
})
.into_keys()
.collect::<BTreeSet<_>>()
);
assert_eq!(
BTreeSet::default(),
snapshot
.words_in_range(Some("öÄ好"), 0..snapshot.len())
.words_in_range(WordsQuery {
fuzzy_contents: Some("öÄ好"),
skip_digits: true,
range: 0..snapshot.len(),
})
.into_keys()
.collect::<BTreeSet<_>>()
);
assert_eq!(
BTreeSet::from_iter(["bar你".to_string(),]),
snapshot
.words_in_range(Some(""), 0..snapshot.len())
.words_in_range(WordsQuery {
fuzzy_contents: Some(""),
skip_digits: true,
range: 0..snapshot.len(),
})
.into_keys()
.collect::<BTreeSet<_>>()
);
assert_eq!(
BTreeSet::default(),
snapshot
.words_in_range(Some(""), 0..snapshot.len())
.words_in_range(WordsQuery {
fuzzy_contents: Some(""),
skip_digits: true,
range: 0..snapshot.len(),
},)
.into_keys()
.collect::<BTreeSet<_>>()
);
@@ -3221,7 +3249,36 @@ fn test_words_in_range(cx: &mut gpui::App) {
"word2".to_string(),
]),
snapshot
.words_in_range(None, 0..snapshot.len())
.words_in_range(WordsQuery {
fuzzy_contents: None,
skip_digits: true,
range: 0..snapshot.len(),
})
.into_keys()
.collect::<BTreeSet<_>>()
);
assert_eq!(
BTreeSet::from_iter([
"0_isize".to_string(),
"123".to_string(),
"3".to_string(),
"4".to_string(),
"bar你".to_string(),
"öÄpPlE".to_string(),
"Öäpple".to_string(),
"ÖÄPPLE".to_string(),
"öäpple".to_string(),
"let".to_string(),
"Pizza".to_string(),
"word".to_string(),
"word2".to_string(),
]),
snapshot
.words_in_range(WordsQuery {
fuzzy_contents: None,
skip_digits: false,
range: 0..snapshot.len(),
})
.into_keys()
.collect::<BTreeSet<_>>()
);

View File

@@ -555,6 +555,23 @@ pub trait LspAdapter: 'static + Send + Sync {
// By default all language servers are rooted at the root of the worktree.
Some(Arc::from("".as_ref()))
}
/// Method only implemented by the default JSON language server adapter.
/// Used to provide dynamic reloading of the JSON schemas used to
/// provide autocompletion and diagnostics in Zed setting and keybind
/// files
fn is_primary_zed_json_schema_adapter(&self) -> bool {
false
}
/// Method only implemented by the default JSON language server adapter.
/// Used to clear the cache of JSON schemas that are used to provide
/// autocompletion and diagnostics in Zed settings and keybinds files.
/// Should not be called unless the callee is sure that
/// `Self::is_primary_zed_json_schema_adapter` returns `true`
async fn clear_zed_json_schema_cache(&self) {
unreachable!("Not implemented for this adapter. This method should only be called on the default JSON language server adapter");
}
}
async fn try_fetch_server_binary<L: LspAdapter + 'static + Send + Sync + ?Sized>(

View File

@@ -326,8 +326,8 @@ pub struct CompletionSettings {
/// When fetching LSP completions, determines how long to wait for a response of a particular server.
/// When set to 0, waits indefinitely.
///
/// Default: 500
#[serde(default = "lsp_fetch_timeout_ms")]
/// Default: 0
#[serde(default = "default_lsp_fetch_timeout_ms")]
pub lsp_fetch_timeout_ms: u64,
}
@@ -335,12 +335,13 @@ pub struct CompletionSettings {
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum WordsCompletionMode {
/// Always fetch document's words for completions.
/// Always fetch document's words for completions along with LSP completions.
Enabled,
/// Only if LSP response errors/times out/is empty,
/// Only if LSP response errors or times out,
/// use document's words to show completions.
Fallback,
/// Never fetch or complete document's words for completions.
/// (Word-based completions can still be queried via a separate action)
Disabled,
}
@@ -348,8 +349,8 @@ fn default_words_completion_mode() -> WordsCompletionMode {
WordsCompletionMode::Fallback
}
fn lsp_fetch_timeout_ms() -> u64 {
500
fn default_lsp_fetch_timeout_ms() -> u64 {
0
}
/// The settings for a particular language.
@@ -580,8 +581,6 @@ pub struct CopilotSettingsContent {
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub struct FeaturesContent {
/// Whether the GitHub Copilot feature is enabled.
pub copilot: Option<bool>,
/// Determines which edit prediction provider to use.
pub edit_prediction_provider: Option<EditPredictionProvider>,
}
@@ -1158,7 +1157,6 @@ impl settings::Settings for AllLanguageSettings {
languages.insert(language_name.clone(), language_settings);
}
let mut copilot_enabled = default_value.features.as_ref().and_then(|f| f.copilot);
let mut edit_prediction_provider = default_value
.features
.as_ref()
@@ -1205,9 +1203,6 @@ impl settings::Settings for AllLanguageSettings {
}
for user_settings in sources.customizations() {
if let Some(copilot) = user_settings.features.as_ref().and_then(|f| f.copilot) {
copilot_enabled = Some(copilot);
}
if let Some(provider) = user_settings
.features
.as_ref()
@@ -1282,8 +1277,6 @@ impl settings::Settings for AllLanguageSettings {
edit_predictions: EditPredictionSettings {
provider: if let Some(provider) = edit_prediction_provider {
provider
} else if copilot_enabled.unwrap_or(true) {
EditPredictionProvider::Copilot
} else {
EditPredictionProvider::None
},

View File

@@ -17,9 +17,11 @@ use proto::Plan;
use schemars::JsonSchema;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use std::fmt;
use std::ops::{Add, Sub};
use std::{future::Future, sync::Arc};
use thiserror::Error;
use ui::IconName;
use util::serde::is_default;
pub use crate::model::*;
pub use crate::rate_limiter::*;
@@ -59,6 +61,7 @@ pub enum LanguageModelCompletionEvent {
Text(String),
ToolUse(LanguageModelToolUse),
StartMessage { message_id: String },
UsageUpdate(TokenUsage),
}
#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize)]
@@ -69,6 +72,46 @@ pub enum StopReason {
ToolUse,
}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Default)]
pub struct TokenUsage {
#[serde(default, skip_serializing_if = "is_default")]
pub input_tokens: u32,
#[serde(default, skip_serializing_if = "is_default")]
pub output_tokens: u32,
#[serde(default, skip_serializing_if = "is_default")]
pub cache_creation_input_tokens: u32,
#[serde(default, skip_serializing_if = "is_default")]
pub cache_read_input_tokens: u32,
}
impl Add<TokenUsage> for TokenUsage {
type Output = Self;
fn add(self, other: Self) -> Self {
Self {
input_tokens: self.input_tokens + other.input_tokens,
output_tokens: self.output_tokens + other.output_tokens,
cache_creation_input_tokens: self.cache_creation_input_tokens
+ other.cache_creation_input_tokens,
cache_read_input_tokens: self.cache_read_input_tokens + other.cache_read_input_tokens,
}
}
}
impl Sub<TokenUsage> for TokenUsage {
type Output = Self;
fn sub(self, other: Self) -> Self {
Self {
input_tokens: self.input_tokens - other.input_tokens,
output_tokens: self.output_tokens - other.output_tokens,
cache_creation_input_tokens: self.cache_creation_input_tokens
- other.cache_creation_input_tokens,
cache_read_input_tokens: self.cache_read_input_tokens - other.cache_read_input_tokens,
}
}
}
#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
pub struct LanguageModelToolUseId(Arc<str>);
@@ -176,6 +219,7 @@ pub trait LanguageModel: Send + Sync {
Ok(LanguageModelCompletionEvent::Text(text)) => Some(Ok(text)),
Ok(LanguageModelCompletionEvent::Stop(_)) => None,
Ok(LanguageModelCompletionEvent::ToolUse(_)) => None,
Ok(LanguageModelCompletionEvent::UsageUpdate(_)) => None,
Err(err) => Some(Err(err)),
}
}))

View File

@@ -33,6 +33,7 @@ gpui_tokio.workspace = true
http_client.workspace = true
language_model.workspace = true
lmstudio = { workspace = true, features = ["schemars"] }
log.workspace = true
menu.workspace = true
mistral = { workspace = true, features = ["schemars"] }
ollama = { workspace = true, features = ["schemars"] }

View File

@@ -1,6 +1,6 @@
use crate::ui::InstructionListItem;
use crate::AllLanguageModelSettings;
use anthropic::{AnthropicError, ContentDelta, Event, ResponseContent};
use anthropic::{AnthropicError, ContentDelta, Event, ResponseContent, Usage};
use anyhow::{anyhow, Context as _, Result};
use collections::{BTreeMap, HashMap};
use credentials_provider::CredentialsProvider;
@@ -582,12 +582,16 @@ pub fn map_to_language_model_completion_events(
struct State {
events: Pin<Box<dyn Send + Stream<Item = Result<Event, AnthropicError>>>>,
tool_uses_by_index: HashMap<usize, RawToolUse>,
usage: Usage,
stop_reason: StopReason,
}
futures::stream::unfold(
State {
events,
tool_uses_by_index: HashMap::default(),
usage: Usage::default(),
stop_reason: StopReason::EndTurn,
},
|mut state| async move {
while let Some(event) = state.events.next().await {
@@ -599,7 +603,7 @@ pub fn map_to_language_model_completion_events(
} => match content_block {
ResponseContent::Text { text } => {
return Some((
Some(Ok(LanguageModelCompletionEvent::Text(text))),
vec![Ok(LanguageModelCompletionEvent::Text(text))],
state,
));
}
@@ -612,28 +616,25 @@ pub fn map_to_language_model_completion_events(
input_json: String::new(),
},
);
return Some((None, state));
}
},
Event::ContentBlockDelta { index, delta } => match delta {
ContentDelta::TextDelta { text } => {
return Some((
Some(Ok(LanguageModelCompletionEvent::Text(text))),
vec![Ok(LanguageModelCompletionEvent::Text(text))],
state,
));
}
ContentDelta::InputJsonDelta { partial_json } => {
if let Some(tool_use) = state.tool_uses_by_index.get_mut(&index) {
tool_use.input_json.push_str(&partial_json);
return Some((None, state));
}
}
},
Event::ContentBlockStop { index } => {
if let Some(tool_use) = state.tool_uses_by_index.remove(&index) {
return Some((
Some(maybe!({
vec![maybe!({
Ok(LanguageModelCompletionEvent::ToolUse(
LanguageModelToolUse {
id: tool_use.id.into(),
@@ -650,44 +651,63 @@ pub fn map_to_language_model_completion_events(
},
},
))
})),
})],
state,
));
}
}
Event::MessageStart { message } => {
update_usage(&mut state.usage, &message.usage);
return Some((
Some(Ok(LanguageModelCompletionEvent::StartMessage {
message_id: message.id,
})),
vec![
Ok(LanguageModelCompletionEvent::StartMessage {
message_id: message.id,
}),
Ok(LanguageModelCompletionEvent::UsageUpdate(convert_usage(
&state.usage,
))),
],
state,
))
));
}
Event::MessageDelta { delta, .. } => {
Event::MessageDelta { delta, usage } => {
update_usage(&mut state.usage, &usage);
if let Some(stop_reason) = delta.stop_reason.as_deref() {
let stop_reason = match stop_reason {
state.stop_reason = match stop_reason {
"end_turn" => StopReason::EndTurn,
"max_tokens" => StopReason::MaxTokens,
"tool_use" => StopReason::ToolUse,
_ => StopReason::EndTurn,
_ => {
log::error!(
"Unexpected anthropic stop_reason: {stop_reason}"
);
StopReason::EndTurn
}
};
return Some((
Some(Ok(LanguageModelCompletionEvent::Stop(stop_reason))),
state,
));
}
return Some((
vec![Ok(LanguageModelCompletionEvent::UsageUpdate(
convert_usage(&state.usage),
))],
state,
));
}
Event::MessageStop => {
return Some((
vec![Ok(LanguageModelCompletionEvent::Stop(state.stop_reason))],
state,
));
}
Event::Error { error } => {
return Some((
Some(Err(anyhow!(AnthropicError::ApiError(error)))),
vec![Err(anyhow!(AnthropicError::ApiError(error)))],
state,
));
}
_ => {}
},
Err(err) => {
return Some((Some(Err(anyhow!(err))), state));
return Some((vec![Err(anyhow!(err))], state));
}
}
}
@@ -695,7 +715,32 @@ pub fn map_to_language_model_completion_events(
None
},
)
.filter_map(|event| async move { event })
.flat_map(futures::stream::iter)
}
/// Updates usage data by preferring counts from `new`.
fn update_usage(usage: &mut Usage, new: &Usage) {
if let Some(input_tokens) = new.input_tokens {
usage.input_tokens = Some(input_tokens);
}
if let Some(output_tokens) = new.output_tokens {
usage.output_tokens = Some(output_tokens);
}
if let Some(cache_creation_input_tokens) = new.cache_creation_input_tokens {
usage.cache_creation_input_tokens = Some(cache_creation_input_tokens);
}
if let Some(cache_read_input_tokens) = new.cache_read_input_tokens {
usage.cache_read_input_tokens = Some(cache_read_input_tokens);
}
}
fn convert_usage(usage: &Usage) -> language_model::TokenUsage {
language_model::TokenUsage {
input_tokens: usage.input_tokens.unwrap_or(0),
output_tokens: usage.output_tokens.unwrap_or(0),
cache_creation_input_tokens: usage.cache_creation_input_tokens.unwrap_or(0),
cache_read_input_tokens: usage.cache_read_input_tokens.unwrap_or(0),
}
}
struct ConfigurationView {

View File

@@ -129,7 +129,7 @@ impl LanguageModelProvider for CopilotChatLanguageModelProvider {
Status::Error(err) => anyhow!(format!("Received the following error while signing into Copilot: {err}")),
Status::Starting { task: _ } => anyhow!("Copilot is still starting, please wait for Copilot to start then try again"),
Status::Unauthorized => anyhow!("Unable to authorize with Copilot. Please make sure that you have an active Copilot and Copilot Chat subscription."),
Status::SignedOut => anyhow!("You have signed out of Copilot. Please sign in to Copilot and try again."),
Status::SignedOut {..} => anyhow!("You have signed out of Copilot. Please sign in to Copilot and try again."),
Status::SigningIn { prompt: _ } => anyhow!("Still signing into Copilot..."),
};
@@ -366,7 +366,6 @@ impl Render for ConfigurationView {
match &self.copilot_status {
Some(status) => match status {
Status::Disabled => v_flex().gap_6().p_4().child(Label::new(ERROR_LABEL)),
Status::Starting { task: _ } => {
const LABEL: &str = "Starting Copilot...";
v_flex()
@@ -376,7 +375,10 @@ impl Render for ConfigurationView {
.child(Label::new(LABEL))
.child(loading_icon)
}
Status::SigningIn { prompt: _ } => {
Status::SigningIn { prompt: _ }
| Status::SignedOut {
awaiting_signing_in: true,
} => {
const LABEL: &str = "Signing in to Copilot...";
v_flex()
.gap_6()

View File

@@ -15,6 +15,7 @@ use settings::{KeymapFile, SettingsJsonSchemaParams, SettingsStore};
use smol::{
fs::{self},
io::BufReader,
lock::RwLock,
};
use std::{
any::Any,
@@ -22,7 +23,7 @@ use std::{
ffi::OsString,
path::{Path, PathBuf},
str::FromStr,
sync::{Arc, OnceLock},
sync::Arc,
};
use task::{TaskTemplate, TaskTemplates, VariableName};
use util::{fs::remove_matching, maybe, merge_json_value_into, ResultExt};
@@ -60,7 +61,7 @@ fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
pub struct JsonLspAdapter {
node: NodeRuntime,
languages: Arc<LanguageRegistry>,
workspace_config: OnceLock<Value>,
workspace_config: RwLock<Option<Value>>,
}
impl JsonLspAdapter {
@@ -141,6 +142,20 @@ impl JsonLspAdapter {
}
})
}
async fn get_or_init_workspace_config(&self, cx: &mut AsyncApp) -> Result<Value> {
{
let reader = self.workspace_config.read().await;
if let Some(config) = reader.as_ref() {
return Ok(config.clone());
}
}
let mut writer = self.workspace_config.write().await;
let config =
cx.update(|cx| Self::get_workspace_config(self.languages.language_names(), cx))?;
writer.replace(config.clone());
return Ok(config);
}
}
#[async_trait(?Send)]
@@ -251,11 +266,7 @@ impl LspAdapter for JsonLspAdapter {
_: Arc<dyn LanguageToolchainStore>,
cx: &mut AsyncApp,
) -> Result<Value> {
let mut config = cx.update(|cx| {
self.workspace_config
.get_or_init(|| Self::get_workspace_config(self.languages.language_names(), cx))
.clone()
})?;
let mut config = self.get_or_init_workspace_config(cx).await?;
let project_options = cx.update(|cx| {
language_server_settings(delegate.as_ref(), &self.name(), cx)
@@ -277,6 +288,14 @@ impl LspAdapter for JsonLspAdapter {
.into_iter()
.collect()
}
fn is_primary_zed_json_schema_adapter(&self) -> bool {
true
}
async fn clear_zed_json_schema_cache(&self) {
self.workspace_config.write().await.take();
}
}
async fn get_cached_server_binary(

View File

@@ -10,3 +10,8 @@ brackets = [
]
tab_size = 2
prettier_parser_name = "json"
scope_opt_in_language_servers = ["json-language-server"]
[overrides.string]
word_characters = [":"]
opt_into_language_servers = ["json-language-server"]

View File

@@ -10,3 +10,8 @@ brackets = [
]
tab_size = 2
prettier_parser_name = "jsonc"
scope_opt_in_language_servers = ["json-language-server"]
[overrides.string]
word_characters = [":"]
opt_into_language_servers = ["json-language-server"]

View File

@@ -68,7 +68,8 @@ pub struct MultiBuffer {
/// Contains the state of the buffers being edited
buffers: RefCell<HashMap<BufferId, BufferState>>,
// only used by consumers using `set_excerpts_for_buffer`
buffers_by_path: BTreeMap<PathKey, Vec<ExcerptId>>,
excerpts_by_path: BTreeMap<PathKey, Vec<ExcerptId>>,
paths_by_excerpt: HashMap<ExcerptId, PathKey>,
diffs: HashMap<BufferId, DiffState>,
// all_diff_hunks_expanded: bool,
subscriptions: Topic,
@@ -360,12 +361,19 @@ impl ExcerptBoundary {
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct ExpandInfo {
pub direction: ExpandExcerptDirection,
pub excerpt_id: ExcerptId,
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub struct RowInfo {
pub buffer_id: Option<BufferId>,
pub buffer_row: Option<u32>,
pub multibuffer_row: Option<MultiBufferRow>,
pub diff_status: Option<buffer_diff::DiffHunkStatus>,
pub expand_info: Option<ExpandInfo>,
}
/// A slice into a [`Buffer`] that is being edited in a [`MultiBuffer`].
@@ -438,6 +446,7 @@ pub struct DiffTransformSummary {
pub struct MultiBufferRows<'a> {
point: Point,
is_empty: bool,
is_singleton: bool,
cursor: MultiBufferCursor<'a, Point>,
}
@@ -569,7 +578,8 @@ impl MultiBuffer {
singleton: false,
capability,
title: None,
buffers_by_path: Default::default(),
excerpts_by_path: Default::default(),
paths_by_excerpt: Default::default(),
buffer_changed_since_sync: Default::default(),
history: History {
next_transaction_id: clock::Lamport::default(),
@@ -585,7 +595,8 @@ impl MultiBuffer {
Self {
snapshot: Default::default(),
buffers: Default::default(),
buffers_by_path: Default::default(),
excerpts_by_path: Default::default(),
paths_by_excerpt: Default::default(),
diffs: HashMap::default(),
subscriptions: Default::default(),
singleton: false,
@@ -630,7 +641,8 @@ impl MultiBuffer {
Self {
snapshot: RefCell::new(self.snapshot.borrow().clone()),
buffers: RefCell::new(buffers),
buffers_by_path: Default::default(),
excerpts_by_path: Default::default(),
paths_by_excerpt: Default::default(),
diffs: diff_bases,
subscriptions: Default::default(),
singleton: self.singleton,
@@ -1470,7 +1482,7 @@ impl MultiBuffer {
}
pub fn location_for_path(&self, path: &PathKey, cx: &App) -> Option<Anchor> {
let excerpt_id = self.buffers_by_path.get(path)?.first()?;
let excerpt_id = self.excerpts_by_path.get(path)?.first()?;
let snapshot = self.snapshot(cx);
let excerpt = snapshot.excerpt(*excerpt_id)?;
Some(Anchor::in_buffer(
@@ -1481,7 +1493,93 @@ impl MultiBuffer {
}
pub fn excerpt_paths(&self) -> impl Iterator<Item = &PathKey> {
self.buffers_by_path.keys()
self.excerpts_by_path.keys()
}
fn expand_excerpts_with_paths(
&mut self,
ids: impl IntoIterator<Item = ExcerptId>,
line_count: u32,
direction: ExpandExcerptDirection,
cx: &mut Context<Self>,
) {
let grouped = ids
.into_iter()
.chunk_by(|id| self.paths_by_excerpt.get(id).cloned())
.into_iter()
.flat_map(|(k, v)| Some((k?, v.into_iter().collect::<Vec<_>>())))
.collect::<Vec<_>>();
let snapshot = self.snapshot(cx);
for (path, ids) in grouped.into_iter() {
let Some(excerpt_ids) = self.excerpts_by_path.get(&path) else {
continue;
};
let ids_to_expand = HashSet::from_iter(ids);
let expanded_ranges = excerpt_ids.iter().filter_map(|excerpt_id| {
let excerpt = snapshot.excerpt(*excerpt_id)?;
let mut context = excerpt.range.context.to_point(&excerpt.buffer);
if ids_to_expand.contains(excerpt_id) {
match direction {
ExpandExcerptDirection::Up => {
context.start.row = context.start.row.saturating_sub(line_count);
context.start.column = 0;
}
ExpandExcerptDirection::Down => {
context.end.row =
(context.end.row + line_count).min(excerpt.buffer.max_point().row);
context.end.column = excerpt.buffer.line_len(context.end.row);
}
ExpandExcerptDirection::UpAndDown => {
context.start.row = context.start.row.saturating_sub(line_count);
context.start.column = 0;
context.end.row =
(context.end.row + line_count).min(excerpt.buffer.max_point().row);
context.end.column = excerpt.buffer.line_len(context.end.row);
}
}
}
Some(ExcerptRange {
context,
primary: excerpt
.range
.primary
.as_ref()
.map(|range| range.to_point(&excerpt.buffer)),
})
});
let mut merged_ranges: Vec<ExcerptRange<Point>> = Vec::new();
for range in expanded_ranges {
if let Some(last_range) = merged_ranges.last_mut() {
if last_range.context.end >= range.context.start {
last_range.context.end = range.context.end;
continue;
}
}
merged_ranges.push(range)
}
let Some(excerpt_id) = excerpt_ids.first() else {
continue;
};
let Some(buffer_id) = &snapshot.buffer_id_for_excerpt(*excerpt_id) else {
continue;
};
let Some(buffer) = self
.buffers
.borrow()
.get(buffer_id)
.map(|b| b.buffer.clone())
else {
continue;
};
let buffer_snapshot = buffer.read(cx).snapshot();
self.update_path_excerpts(path.clone(), buffer, &buffer_snapshot, merged_ranges, cx);
}
}
/// Sets excerpts, returns `true` if at least one new excerpt was added.
@@ -1495,15 +1593,30 @@ impl MultiBuffer {
) -> bool {
let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
let (new, _) = build_excerpt_ranges(&buffer_snapshot, &ranges, context_line_count);
self.update_path_excerpts(path, buffer, &buffer_snapshot, new, cx)
}
fn update_path_excerpts(
&mut self,
path: PathKey,
buffer: Entity<Buffer>,
buffer_snapshot: &BufferSnapshot,
new: Vec<ExcerptRange<Point>>,
cx: &mut Context<Self>,
) -> bool {
let mut insert_after = self
.buffers_by_path
.excerpts_by_path
.range(..path.clone())
.next_back()
.map(|(_, value)| *value.last().unwrap())
.unwrap_or(ExcerptId::min());
let existing = self.buffers_by_path.get(&path).cloned().unwrap_or_default();
let (new, _) = build_excerpt_ranges(&buffer_snapshot, &ranges, context_line_count);
let existing = self
.excerpts_by_path
.get(&path)
.cloned()
.unwrap_or_default();
let mut new_iter = new.into_iter().peekable();
let mut existing_iter = existing.into_iter().peekable();
@@ -1586,20 +1699,23 @@ impl MultiBuffer {
));
self.remove_excerpts(to_remove, cx);
if new_excerpt_ids.is_empty() {
self.buffers_by_path.remove(&path);
self.excerpts_by_path.remove(&path);
} else {
self.buffers_by_path.insert(path, new_excerpt_ids);
for excerpt_id in &new_excerpt_ids {
self.paths_by_excerpt.insert(*excerpt_id, path.clone());
}
self.excerpts_by_path.insert(path, new_excerpt_ids);
}
added_a_new_excerpt
}
pub fn paths(&self) -> impl Iterator<Item = PathKey> + '_ {
self.buffers_by_path.keys().cloned()
self.excerpts_by_path.keys().cloned()
}
pub fn remove_excerpts_for_path(&mut self, path: PathKey, cx: &mut Context<Self>) {
if let Some(to_remove) = self.buffers_by_path.remove(&path) {
if let Some(to_remove) = self.excerpts_by_path.remove(&path) {
self.remove_excerpts(to_remove, cx)
}
}
@@ -2007,22 +2123,17 @@ impl MultiBuffer {
cx: &App,
) -> Option<(Entity<Buffer>, Point, ExcerptId)> {
let snapshot = self.read(cx);
let point = point.to_point(&snapshot);
let mut cursor = snapshot.cursor::<Point>();
cursor.seek(&point);
cursor.region().and_then(|region| {
if !region.is_main_buffer {
return None;
}
let overshoot = point - region.range.start;
let buffer_point = region.buffer_range.start + overshoot;
let buffer = self.buffers.borrow()[&region.buffer.remote_id()]
let (buffer, point, is_main_buffer) =
snapshot.point_to_buffer_point(point.to_point(&snapshot))?;
Some((
self.buffers
.borrow()
.get(&buffer.remote_id())?
.buffer
.clone();
Some((buffer, buffer_point, region.excerpt.id))
})
.clone(),
point,
is_main_buffer,
))
}
pub fn buffer_point_to_anchor(
@@ -2076,6 +2187,7 @@ impl MultiBuffer {
let mut removed_buffer_ids = Vec::new();
while let Some(excerpt_id) = excerpt_ids.next() {
self.paths_by_excerpt.remove(&excerpt_id);
// Seek to the next excerpt to remove, preserving any preceding excerpts.
let locator = snapshot.excerpt_locator_for_id(excerpt_id);
new_excerpts.append(cursor.slice(&Some(locator), Bias::Left, &()), &());
@@ -2640,6 +2752,10 @@ impl MultiBuffer {
return;
}
self.sync(cx);
if !self.excerpts_by_path.is_empty() {
self.expand_excerpts_with_paths(ids, line_count, direction, cx);
return;
}
let mut snapshot = self.snapshot.borrow_mut();
let ids = ids.into_iter().collect::<Vec<_>>();
@@ -4039,6 +4155,7 @@ impl MultiBufferSnapshot {
let mut result = MultiBufferRows {
point: Point::new(0, 0),
is_empty: self.excerpts.is_empty(),
is_singleton: self.is_singleton(),
cursor,
};
result.seek(start_row);
@@ -4176,22 +4293,36 @@ impl MultiBufferSnapshot {
let region = cursor.region()?;
let overshoot = offset - region.range.start;
let buffer_offset = region.buffer_range.start + overshoot;
if buffer_offset > region.buffer.len() {
if buffer_offset == region.buffer.len() + 1
&& region.has_trailing_newline
&& !region.is_main_buffer
{
return Some((&cursor.excerpt()?.buffer, cursor.main_buffer_position()?));
} else if buffer_offset > region.buffer.len() {
return None;
}
Some((region.buffer, buffer_offset))
}
pub fn point_to_buffer_point(&self, point: Point) -> Option<(&BufferSnapshot, Point, bool)> {
pub fn point_to_buffer_point(
&self,
point: Point,
) -> Option<(&BufferSnapshot, Point, ExcerptId)> {
let mut cursor = self.cursor::<Point>();
cursor.seek(&point);
let region = cursor.region()?;
let overshoot = point - region.range.start;
let buffer_point = region.buffer_range.start + overshoot;
if buffer_point > region.buffer.max_point() {
let excerpt = cursor.excerpt()?;
if buffer_point == region.buffer.max_point() + Point::new(1, 0)
&& region.has_trailing_newline
&& !region.is_main_buffer
{
return Some((&excerpt.buffer, cursor.main_buffer_position()?, excerpt.id));
} else if buffer_point > region.buffer.max_point() {
return None;
}
Some((region.buffer, buffer_point, region.is_main_buffer))
Some((region.buffer, buffer_point, excerpt.id))
}
pub fn suggested_indents(
@@ -4733,6 +4864,9 @@ impl MultiBufferSnapshot {
.buffer
.text_summary_for_range(region.buffer_range.start.key..buffer_point),
);
if point == region.range.end.key && region.has_trailing_newline {
position.add_assign(&D::from_text_summary(&TextSummary::newline()));
}
return Some(position);
} else {
return Some(D::from_text_summary(&self.text_summary()));
@@ -6204,6 +6338,22 @@ where
self.cached_region.clone()
}
fn is_at_start_of_excerpt(&mut self) -> bool {
if self.diff_transforms.start().1 > *self.excerpts.start() {
return false;
} else if self.diff_transforms.start().1 < *self.excerpts.start() {
return true;
}
self.diff_transforms.prev(&());
let prev_transform = self.diff_transforms.item();
self.diff_transforms.next(&());
prev_transform.map_or(true, |next_transform| {
matches!(next_transform, DiffTransform::BufferContent { .. })
})
}
fn is_at_end_of_excerpt(&mut self) -> bool {
if self.diff_transforms.end(&()).1 < self.excerpts.end(&()) {
return false;
@@ -7080,6 +7230,7 @@ impl Iterator for MultiBufferRows<'_> {
buffer_row: Some(0),
multibuffer_row: Some(MultiBufferRow(0)),
diff_status: None,
expand_info: None,
});
}
@@ -7091,7 +7242,6 @@ impl Iterator for MultiBufferRows<'_> {
} else {
if self.point == self.cursor.diff_transforms.end(&()).0 .0 {
let multibuffer_row = MultiBufferRow(self.point.row);
self.point += Point::new(1, 0);
let last_excerpt = self
.cursor
.excerpts
@@ -7103,11 +7253,43 @@ impl Iterator for MultiBufferRows<'_> {
.end
.to_point(&last_excerpt.buffer)
.row;
let first_row = last_excerpt
.range
.context
.start
.to_point(&last_excerpt.buffer)
.row;
let expand_info = if self.is_singleton {
None
} else {
let needs_expand_up = first_row == last_row
&& last_row > 0
&& !region.diff_hunk_status.is_some_and(|d| d.is_deleted());
let needs_expand_down = last_row < last_excerpt.buffer.max_point().row;
if needs_expand_up && needs_expand_down {
Some(ExpandExcerptDirection::UpAndDown)
} else if needs_expand_up {
Some(ExpandExcerptDirection::Up)
} else if needs_expand_down {
Some(ExpandExcerptDirection::Down)
} else {
None
}
.map(|direction| ExpandInfo {
direction,
excerpt_id: last_excerpt.id,
})
};
self.point += Point::new(1, 0);
return Some(RowInfo {
buffer_id: Some(last_excerpt.buffer_id),
buffer_row: Some(last_row),
multibuffer_row: Some(multibuffer_row),
diff_status: None,
expand_info,
});
} else {
return None;
@@ -7117,6 +7299,33 @@ impl Iterator for MultiBufferRows<'_> {
let overshoot = self.point - region.range.start;
let buffer_point = region.buffer_range.start + overshoot;
let expand_info = if self.is_singleton {
None
} else {
let needs_expand_up = self.point.row == region.range.start.row
&& self.cursor.is_at_start_of_excerpt()
&& buffer_point.row > 0;
let needs_expand_down = (region.excerpt.has_trailing_newline
&& self.point.row + 1 == region.range.end.row
|| !region.excerpt.has_trailing_newline && self.point.row == region.range.end.row)
&& self.cursor.is_at_end_of_excerpt()
&& buffer_point.row < region.buffer.max_point().row;
if needs_expand_up && needs_expand_down {
Some(ExpandExcerptDirection::UpAndDown)
} else if needs_expand_up {
Some(ExpandExcerptDirection::Up)
} else if needs_expand_down {
Some(ExpandExcerptDirection::Down)
} else {
None
}
.map(|direction| ExpandInfo {
direction,
excerpt_id: region.excerpt.id,
})
};
let result = Some(RowInfo {
buffer_id: Some(region.buffer.remote_id()),
buffer_row: Some(buffer_point.row),
@@ -7124,6 +7333,7 @@ impl Iterator for MultiBufferRows<'_> {
diff_status: region
.diff_hunk_status
.filter(|_| self.point < region.range.end),
expand_info,
});
self.point += Point::new(1, 0);
result

View File

@@ -29,7 +29,8 @@ fn test_empty_singleton(cx: &mut App) {
buffer_id: Some(buffer_id),
buffer_row: Some(0),
multibuffer_row: Some(MultiBufferRow(0)),
diff_status: None
diff_status: None,
expand_info: None,
}]
);
}
@@ -2118,6 +2119,7 @@ struct ReferenceRegion {
range: Range<usize>,
buffer_start: Option<Point>,
status: Option<DiffHunkStatus>,
excerpt_id: Option<ExcerptId>,
}
impl ReferenceMultibuffer {
@@ -2274,6 +2276,7 @@ impl ReferenceMultibuffer {
range: len..text.len(),
buffer_start: Some(buffer.offset_to_point(offset)),
status: None,
excerpt_id: Some(excerpt.id),
});
// Add the deleted text for the hunk.
@@ -2293,6 +2296,7 @@ impl ReferenceMultibuffer {
base_buffer.offset_to_point(hunk.diff_base_byte_range.start),
),
status: Some(DiffHunkStatus::deleted(hunk.secondary_status)),
excerpt_id: Some(excerpt.id),
});
}
@@ -2308,6 +2312,7 @@ impl ReferenceMultibuffer {
range: len..text.len(),
buffer_start: Some(buffer.offset_to_point(offset)),
status: Some(DiffHunkStatus::added(hunk.secondary_status)),
excerpt_id: Some(excerpt.id),
});
offset = hunk_range.end;
}
@@ -2322,6 +2327,7 @@ impl ReferenceMultibuffer {
range: len..text.len(),
buffer_start: Some(buffer.offset_to_point(offset)),
status: None,
excerpt_id: Some(excerpt.id),
});
}
@@ -2332,6 +2338,7 @@ impl ReferenceMultibuffer {
range: 0..1,
buffer_start: Some(Point::new(0, 0)),
status: None,
excerpt_id: None,
});
} else {
text.pop();
@@ -2345,12 +2352,58 @@ impl ReferenceMultibuffer {
.map(|line| {
let row_info = regions
.iter()
.find(|region| region.range.contains(&ix))
.map_or(RowInfo::default(), |region| {
.position(|region| region.range.contains(&ix))
.map_or(RowInfo::default(), |region_ix| {
let region = &regions[region_ix];
let buffer_row = region.buffer_start.map(|start_point| {
start_point.row
+ text[region.range.start..ix].matches('\n').count() as u32
});
let is_excerpt_start = region_ix == 0
|| &regions[region_ix - 1].excerpt_id != &region.excerpt_id
|| regions[region_ix - 1].range.is_empty();
let mut is_excerpt_end = region_ix == regions.len() - 1
|| &regions[region_ix + 1].excerpt_id != &region.excerpt_id;
let is_start = !text[region.range.start..ix].contains('\n');
let mut is_end = if region.range.end > text.len() {
!text[ix..].contains('\n')
} else {
text[ix..region.range.end.min(text.len())]
.matches('\n')
.count()
== 1
};
if region_ix < regions.len() - 1
&& !text[ix..].contains("\n")
&& region.status == Some(DiffHunkStatus::added_none())
&& regions[region_ix + 1].excerpt_id == region.excerpt_id
&& regions[region_ix + 1].range.start == text.len()
{
is_end = true;
is_excerpt_end = true;
}
let mut expand_direction = None;
if let Some(buffer) = &self
.excerpts
.iter()
.find(|e| e.id == region.excerpt_id.unwrap())
.map(|e| e.buffer.clone())
{
let needs_expand_up =
is_excerpt_start && is_start && buffer_row.unwrap() > 0;
let needs_expand_down = is_excerpt_end
&& is_end
&& buffer.read(cx).max_point().row > buffer_row.unwrap();
expand_direction = if needs_expand_up && needs_expand_down {
Some(ExpandExcerptDirection::UpAndDown)
} else if needs_expand_up {
Some(ExpandExcerptDirection::Up)
} else if needs_expand_down {
Some(ExpandExcerptDirection::Down)
} else {
None
};
}
RowInfo {
buffer_id: region.buffer_id,
diff_status: region.status,
@@ -2358,6 +2411,12 @@ impl ReferenceMultibuffer {
multibuffer_row: Some(MultiBufferRow(
text[..ix].matches('\n').count() as u32
)),
expand_info: expand_direction.zip(region.excerpt_id).map(
|(direction, excerpt_id)| ExpandInfo {
direction,
excerpt_id,
},
),
}
});
ix += line.len() + 1;
@@ -3121,6 +3180,100 @@ fn test_summaries_for_anchors(cx: &mut TestAppContext) {
assert_eq!(point_2, Point::new(3, 0));
}
#[gpui::test]
fn test_trailing_deletion_without_newline(cx: &mut TestAppContext) {
let base_text_1 = "one\ntwo".to_owned();
let text_1 = "one\n".to_owned();
let buffer_1 = cx.new(|cx| Buffer::local(text_1, cx));
let diff_1 = cx.new(|cx| BufferDiff::new_with_base_text(&base_text_1, &buffer_1, cx));
cx.run_until_parked();
let multibuffer = cx.new(|cx| {
let mut multibuffer = MultiBuffer::singleton(buffer_1.clone(), cx);
multibuffer.add_diff(diff_1.clone(), cx);
multibuffer.expand_diff_hunks(vec![Anchor::min()..Anchor::max()], cx);
multibuffer
});
let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| {
(multibuffer.snapshot(cx), multibuffer.subscribe())
});
assert_new_snapshot(
&multibuffer,
&mut snapshot,
&mut subscription,
cx,
indoc!(
"
one
- two
"
),
);
assert_eq!(snapshot.max_point(), Point::new(2, 0));
assert_eq!(snapshot.len(), 8);
assert_eq!(
snapshot
.dimensions_from_points::<Point>([Point::new(2, 0)])
.collect::<Vec<_>>(),
vec![Point::new(2, 0)]
);
let (_, translated_offset) = snapshot.point_to_buffer_offset(Point::new(2, 0)).unwrap();
assert_eq!(translated_offset, "one\n".len());
let (_, translated_point, _) = snapshot.point_to_buffer_point(Point::new(2, 0)).unwrap();
assert_eq!(translated_point, Point::new(1, 0));
// The same, for an excerpt that's not at the end of the multibuffer.
let text_2 = "foo\n".to_owned();
let buffer_2 = cx.new(|cx| Buffer::local(&text_2, cx));
multibuffer.update(cx, |multibuffer, cx| {
multibuffer.push_excerpts(
buffer_2.clone(),
[ExcerptRange {
context: Point::new(0, 0)..Point::new(1, 0),
primary: None,
}],
cx,
);
});
assert_new_snapshot(
&multibuffer,
&mut snapshot,
&mut subscription,
cx,
indoc!(
"
one
- two
foo
"
),
);
assert_eq!(
snapshot
.dimensions_from_points::<Point>([Point::new(2, 0)])
.collect::<Vec<_>>(),
vec![Point::new(2, 0)]
);
let buffer_1_id = buffer_1.read_with(cx, |buffer_1, _| buffer_1.remote_id());
let (buffer, translated_offset) = snapshot.point_to_buffer_offset(Point::new(2, 0)).unwrap();
assert_eq!(buffer.remote_id(), buffer_1_id);
assert_eq!(translated_offset, "one\n".len());
let (buffer, translated_point, _) = snapshot.point_to_buffer_point(Point::new(2, 0)).unwrap();
assert_eq!(buffer.remote_id(), buffer_1_id);
assert_eq!(translated_point, Point::new(1, 0));
}
fn format_diff(
text: &str,
row_infos: &Vec<RowInfo>,
@@ -3379,16 +3532,12 @@ fn assert_position_translation(snapshot: &MultiBufferSnapshot) {
}
}
let point = snapshot.max_point();
let Some((buffer, offset)) = snapshot.point_to_buffer_offset(point) else {
return;
};
assert!(offset <= buffer.len(),);
let Some((buffer, point, _)) = snapshot.point_to_buffer_point(point) else {
return;
};
assert!(point <= buffer.max_point(),);
if let Some((buffer, offset)) = snapshot.point_to_buffer_offset(snapshot.max_point()) {
assert!(offset <= buffer.len());
}
if let Some((buffer, point, _)) = snapshot.point_to_buffer_point(snapshot.max_point()) {
assert!(point <= buffer.max_point());
}
}
fn assert_line_indents(snapshot: &MultiBufferSnapshot) {

View File

@@ -277,6 +277,7 @@ impl PickerDelegate for OutlineViewDelegate {
cx: &mut Context<Picker<OutlineViewDelegate>>,
) {
self.prev_scroll_position.take();
self.set_selected_index(self.selected_match_index, true, cx);
self.active_editor.update(cx, |active_editor, cx| {
let highlight = active_editor
@@ -381,12 +382,13 @@ mod tests {
path!("/dir"),
json!({
"a.rs": indoc!{"
struct SingleLine; // display line 0
// display line 1
struct MultiLine { // display line 2
field_1: i32, // display line 3
field_2: i32, // display line 4
} // display line 5
// display line 0
struct SingleLine; // display line 1
// display line 2
struct MultiLine { // display line 3
field_1: i32, // display line 4
field_2: i32, // display line 5
} // display line 6
"}
}),
)
@@ -439,23 +441,29 @@ mod tests {
);
assert_single_caret_at_row(&editor, 0, cx);
cx.dispatch_action(menu::Confirm);
// Ensures that outline still goes to entry even if no queries have been made
assert_single_caret_at_row(&editor, 1, cx);
let outline_view = open_outline_view(&workspace, cx);
cx.dispatch_action(menu::SelectNext);
ensure_outline_view_contents(&outline_view, cx);
assert_eq!(
highlighted_display_rows(&editor, cx),
vec![2, 3, 4, 5],
vec![3, 4, 5, 6],
"Second struct's rows should be highlighted"
);
assert_single_caret_at_row(&editor, 0, cx);
assert_single_caret_at_row(&editor, 1, cx);
cx.dispatch_action(menu::SelectPrevious);
ensure_outline_view_contents(&outline_view, cx);
assert_eq!(
highlighted_display_rows(&editor, cx),
vec![0],
vec![1],
"First struct's row should be highlighted"
);
assert_single_caret_at_row(&editor, 0, cx);
assert_single_caret_at_row(&editor, 1, cx);
cx.dispatch_action(menu::Cancel);
ensure_outline_view_contents(&outline_view, cx);
@@ -464,7 +472,7 @@ mod tests {
Vec::<u32>::new(),
"No rows should be highlighted after outline view is cancelled and closed"
);
assert_single_caret_at_row(&editor, 0, cx);
assert_single_caret_at_row(&editor, 1, cx);
let outline_view = open_outline_view(&workspace, cx);
ensure_outline_view_contents(&outline_view, cx);
@@ -473,16 +481,16 @@ mod tests {
Vec::<u32>::new(),
"Reopened outline view should have no highlights"
);
assert_single_caret_at_row(&editor, 0, cx);
assert_single_caret_at_row(&editor, 1, cx);
let expected_first_highlighted_row = 2;
let expected_first_highlighted_row = 3;
cx.dispatch_action(menu::SelectNext);
ensure_outline_view_contents(&outline_view, cx);
assert_eq!(
highlighted_display_rows(&editor, cx),
vec![expected_first_highlighted_row, 3, 4, 5]
vec![expected_first_highlighted_row, 4, 5, 6]
);
assert_single_caret_at_row(&editor, 0, cx);
assert_single_caret_at_row(&editor, 1, cx);
cx.dispatch_action(menu::Confirm);
ensure_outline_view_contents(&outline_view, cx);
assert_eq!(

View File

@@ -1075,45 +1075,36 @@ impl OutlinePanel {
});
} else {
let mut offset = Point::default();
let show_excerpt_controls = active_editor
.read(cx)
.display_map
.read(cx)
.show_excerpt_controls();
let expand_excerpt_control_height = 1.0;
if let Some(buffer_id) = scroll_to_buffer {
let current_folded = active_editor.read(cx).is_buffer_folded(buffer_id, cx);
if current_folded {
if show_excerpt_controls {
let previous_buffer_id = self
.fs_entries
.iter()
.rev()
.filter_map(|entry| match entry {
FsEntry::File(file) => Some(file.buffer_id),
FsEntry::ExternalFile(external_file) => {
Some(external_file.buffer_id)
}
FsEntry::Directory(..) => None,
})
.skip_while(|id| *id != buffer_id)
.nth(1);
if let Some(previous_buffer_id) = previous_buffer_id {
if !active_editor
.read(cx)
.is_buffer_folded(previous_buffer_id, cx)
{
offset.y += expand_excerpt_control_height;
let previous_buffer_id = self
.fs_entries
.iter()
.rev()
.filter_map(|entry| match entry {
FsEntry::File(file) => Some(file.buffer_id),
FsEntry::ExternalFile(external_file) => {
Some(external_file.buffer_id)
}
FsEntry::Directory(..) => None,
})
.skip_while(|id| *id != buffer_id)
.nth(1);
if let Some(previous_buffer_id) = previous_buffer_id {
if !active_editor
.read(cx)
.is_buffer_folded(previous_buffer_id, cx)
{
offset.y += expand_excerpt_control_height;
}
}
} else {
if multi_buffer_snapshot.as_singleton().is_none() {
offset.y = -(active_editor.read(cx).file_header_size() as f32);
}
if show_excerpt_controls {
offset.y -= expand_excerpt_control_height;
}
offset.y -= expand_excerpt_control_height;
}
}
active_editor.update(cx, |editor, cx| {

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