Compare commits

..

45 Commits

Author SHA1 Message Date
Peter Tripp
b69573a366 Add conflicts-macos.json and conflicts-linux.json. 2024-08-22 15:25:06 -04:00
张小白
c0ea806afe windows: Treat pwsh as PowerShell (#16409)
`pwsh` is the newer version of `PowerShell`, while the one that comes
pre-installed on Windows is called `Windows PowerShell` and is an older
version. I have no idea why Microsoft dose this and not updated the
`Windows Powershell` on Windows.

Release Notes:

- N/A
2024-08-22 13:05:00 -06:00
renovate[bot]
1404e328cf Update Rust crate tree-sitter-css to v0.21.1 (#16635)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [tree-sitter-css](https://togithub.com/tree-sitter/tree-sitter-css) |
workspace.dependencies | patch | `0.21.0` -> `0.21.1` |

---

### Release Notes

<details>
<summary>tree-sitter/tree-sitter-css (tree-sitter-css)</summary>

###
[`v0.21.1`](https://togithub.com/tree-sitter/tree-sitter-css/compare/v0.21.0...v0.21.1)

[Compare
Source](https://togithub.com/tree-sitter/tree-sitter-css/compare/v0.21.0...v0.21.1)

</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:eyJjcmVhdGVkSW5WZXIiOiIzOC4yNi4xIiwidXBkYXRlZEluVmVyIjoiMzguMjYuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-22 13:02:23 -06:00
renovate[bot]
8ea8e81c86 Update Rust crate tree-sitter-html to v0.20.4 (#16642)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [tree-sitter-html](https://togithub.com/tree-sitter/tree-sitter-html)
| workspace.dependencies | patch | `0.20.3` -> `0.20.4` |

---

### Release Notes

<details>
<summary>tree-sitter/tree-sitter-html (tree-sitter-html)</summary>

###
[`v0.20.4`](https://togithub.com/tree-sitter/tree-sitter-html/compare/v0.20.3...v0.20.4)

[Compare
Source](https://togithub.com/tree-sitter/tree-sitter-html/compare/v0.20.3...v0.20.4)

</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:eyJjcmVhdGVkSW5WZXIiOiIzOC4yNi4xIiwidXBkYXRlZEluVmVyIjoiMzguMjYuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-22 13:02:04 -06:00
Cherry
e1c42a5c85 fs: Fix atomic_write failing on windows if destination is in different drive than the temp dir (#16648)
I have my system temp dir on a different drive than the default, so this
error was spammed in the logs. This also broke Zed in many ways, one of
which was the AI system failing to work since it couldn't save settings.
```
2024-08-20T22:39:54.0660708-07:00 [ERROR] Failed to write settings to file "\\\\?\\C:\\Users\\myuser\\AppData\\Roaming\\Zed\\settings.json"
Caused by:

0: failed to persist temporary file: The system cannot move the file to a different disk drive. (os error 17)

1: The system cannot move the file to a different disk drive. (os error 17)
```

Note: This problem is probably present on MacOS due to the requirement
of the underlying api being used. I do not have Mac, so I cannot test
this. This PR only solves this issue on Windows.

Closes #16571

Release Notes:

- fix atomic_write failing on windows if destination is on a different
drive than the OS's temp dir.
2024-08-22 12:59:00 -06:00
bestgopher
e17a5c1412 Fix log timestamps not using local timezone (#16400)
Get time offset by time crate will fail if there are mutli threads. So
call `config_builder.set_time_offset_to_local()` is useless.

Closes #16397

after:
<img width="664" alt="image"
src="https://github.com/user-attachments/assets/2b15fa06-c411-44f9-9ea1-871d25eb577f">

Release Notes:

- Fixed Local Timezone not showing Zed.log
2024-08-22 12:56:49 -06:00
张小白
20f85b946d windows: Don't panic if terminal creation fails (#16370)
Related #16352 

This PR picks up the upstream change
https://github.com/alacritty/alacritty/pull/8132, now when the terminal
creation fails, it will return an `Err` instead of directly panicing.

Release Notes:

- N/A
2024-08-22 12:55:16 -06:00
0x2CA
abb5800d20 Add bounded soft wrap (#16586)
Closes #14700 #8164

Release Notes:

- Added `soft_wrap` value `bounded`,EditorWidth and PreferredLineLength
min value

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2024-08-22 12:51:32 -06:00
Peter Tripp
4e2b08b909 docs: Terminal line_height (#16687)
Closes https://github.com/zed-industries/zed/issues/16686

Release Notes:

- N/A
2024-08-22 13:48:33 -04:00
Conrad Irwin
c697eaba82 Use split direction preferences more (#16679)
Use new split direction preferences in more places (#16345)

Release Notes:

- N/A
2024-08-22 11:13:33 -06:00
Marshall Bowers
93642c9c51 Pass through Anthropic cache configuration when using Zed provider (#16685)
This PR makes it so the model's cache configuration gets passed through
from the base model when using the Zed provider.

Release Notes:

- Fixed caching for Anthropic models when using the Zed provider.
2024-08-22 12:48:47 -04:00
Cherry
25cdd2ad25 Update blade to 7f54ddf to fix compilation error in opengl mode (#16682)
Update blade to latest commit. This fixes a compilation error in zed
when compiling with `RUSTFLAGS="--cfg gles"`.

Closes #16677 

Release Notes:

- N/A
2024-08-22 19:06:42 +03:00
Piotr Osiewicz
182b7af299 ui: Use popover menus for tab bar in panes (#16497)
Closes #ISSUE

Release Notes:

- N/A
2024-08-22 18:05:23 +02:00
Kirill Bulatov
72b5cda356 Deduplicate /tab all buffers inserted (#16681)
Closes https://github.com/zed-industries/zed/issues/16678

Release Notes:

- Fixed `/tab all` inserting duplicate buffers
([!16678](https://github.com/zed-industries/zed/issues/16678))
2024-08-22 19:04:03 +03:00
renovate[bot]
912ed20a3b Update Rust crate clap to v4.5.16 (#16625)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [clap](https://togithub.com/clap-rs/clap) | workspace.dependencies |
patch | `4.5.15` -> `4.5.16` |

---

### Release Notes

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

###
[`v4.5.16`](https://togithub.com/clap-rs/clap/compare/clap_complete-v4.5.15...clap_complete-v4.5.16)

[Compare
Source](https://togithub.com/clap-rs/clap/compare/v4.5.15...v4.5.16)

</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:eyJjcmVhdGVkSW5WZXIiOiIzOC4yNi4xIiwidXBkYXRlZEluVmVyIjoiMzguMjYuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-22 11:46:11 -04:00
renovate[bot]
3d94ed3242 Update serde monorepo to v1.0.208 (#16647)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [serde](https://serde.rs)
([source](https://togithub.com/serde-rs/serde)) | dependencies | patch |
`1.0.207` -> `1.0.208` |
| [serde](https://serde.rs)
([source](https://togithub.com/serde-rs/serde)) | workspace.dependencies
| patch | `1.0.207` -> `1.0.208` |
| [serde_derive](https://serde.rs)
([source](https://togithub.com/serde-rs/serde)) | workspace.dependencies
| patch | `1.0.207` -> `1.0.208` |

---

### Release Notes

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

###
[`v1.0.208`](https://togithub.com/serde-rs/serde/releases/tag/v1.0.208)

[Compare
Source](https://togithub.com/serde-rs/serde/compare/v1.0.207...v1.0.208)

- Support serializing and deserializing unit structs in a `flatten`
field ([#&#8203;2802](https://togithub.com/serde-rs/serde/issues/2802),
thanks [@&#8203;jonhoo](https://togithub.com/jonhoo))

</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 these
updates again.

---

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

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOC4yNi4xIiwidXBkYXRlZEluVmVyIjoiMzguMjYuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-22 11:25:45 -04:00
Jeroen van Baarsen
3a593fe803 Add option to set split direction (#16345)
This adds an option to set the split direction for both the horizontal
splits, and the vertical splits.

A couple of things to look for when reviewing:

* The `derive` keywords on the Enums were copy pasted, no clue what they
should be
* Tried adding tests for this, but got stuck.

Co-authored with @Tobbe

Fixes: https://github.com/zed-industries/zed/issues/11342
2024-08-22 08:53:43 -06:00
renovate[bot]
f08be779c0 Update Rust crate tree-sitter-go to v0.21.2 (#16641)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [tree-sitter-go](https://togithub.com/tree-sitter/tree-sitter-go) |
workspace.dependencies | patch | `0.21.0` -> `0.21.2` |

---

### Release Notes

<details>
<summary>tree-sitter/tree-sitter-go (tree-sitter-go)</summary>

###
[`v0.21.2`](https://togithub.com/tree-sitter/tree-sitter-go/compare/v0.21.1...v0.21.2)

[Compare
Source](https://togithub.com/tree-sitter/tree-sitter-go/compare/v0.21.1...v0.21.2)

###
[`v0.21.1`](https://togithub.com/tree-sitter/tree-sitter-go/compare/v0.21.0...v0.21.1)

[Compare
Source](https://togithub.com/tree-sitter/tree-sitter-go/compare/v0.21.0...v0.21.1)

</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:eyJjcmVhdGVkSW5WZXIiOiIzOC4yNi4xIiwidXBkYXRlZEluVmVyIjoiMzguMjYuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-22 16:49:34 +02:00
Jonathan Dickinson
278864e19f lsp_log: Show messages before init and add filtering (#15893)
Allows language server logs to be published prior to the completion of
the initialize request. OmniSharp is one example of an LSP that
publishes (many) messages prior to the initialization response, and this
completely floods the Zed logs.

Also adds level filtering as demonstrated below. Again, this is due to
my experience with the massive amount of log messages that OmniSharp
publishes.

Release Notes:

- Added level filtering to language server logs

![Log Level
Filtering](https://github.com/user-attachments/assets/e3fcb7af-28cb-4787-9068-8e5e7eb3cf7d)

---------

Co-authored-by: Thorsten Ball <mrnugget@gmail.com>
2024-08-22 16:47:34 +02:00
Tau Gärtli
9245015d1a terminal: Retain relative order of responses (#16456)
Partially addresses #8497 (namely, the occurring with `delta`)

As I mentioned in
https://github.com/zed-industries/zed/issues/8497#issuecomment-2226896371,
zed currently replies to OSC color requests (`OSC 10`, `OSC 11`, ...)
out of order when immediately followed by another request (for example
`CSI c`). All other terminals that [I have
tested](https://github.com/bash/terminal-colorsaurus/blob/main/doc/terminal-survey.md)
maintain relative order when replying to requests.

## Solution
Respond to the `ColorRequest` in `process_event` (in the same place
where other PTY writes happen) instead of queuing it up in the internal
event queue.

## Alternative
I initially thought that I could handle the color request similarly to
the `TextAreaSizeRequest` where the size is stored in `last_content` and
updated on `sync`. However this causes the terminal to report
out-of-date values when a "set color" sequence is followed by a color
request.

## Tests

1. `OSC 11; ?` (request bg color) + `CSI c` (request device attributes):
   ```shell
   printf '\e]11;?\e\\ \e[c' && cat -v
   # Expected result: ^[]11;rgb:dcdc/dcdc/dddd^[\^[[?6c
# Current result: ^[[?6c^[]11;rgb:dcdc/dcdc/dddd^[\ ()
# Result with this PR: ^[]11;rgb:dcdc/dcdc/dddd^[\^[[?6c ()
# Result with alternative: ^[]11;rgb:dcdc/dcdc/dddd^[\^[[?6c ()
   ```
2. `OSC 11; rgb:f0f0/f0f0/f0f0` (set bg color) + `OSC 11; ?` (request bg
color)
   ```shell
   printf '\e]11;rgb:f0f0/f0f0/f0f0\e\\ \e]11;?\e\\' && cat -v
   # Expected result: ^[]11;rgb:f0f0/f0f0/f0f0^[\
# Current result: ^[]11;rgb:f0f0/f0f0/f0f0^[\ ()
# Result with this PR: ^[]11;rgb:f0f0/f0f0/f0f0^[\ ()
# Result with alternative: ^[]11;rgb:OUT_OF_DATE_COLOR_HERE^[\ ()
   ```

Release Notes:

- N/A
2024-08-22 16:19:24 +02:00
Kajus
b7a66e4491 project_panel: Allow copying the paths of multiple selected files at once (#16558)
Closes #16555 

Release Notes:

- Improved the "Copy Path" and "Copy Relative Path" actions in the
project panel's context menu when selecting multiple files. All selected
files' paths will now be copied, separated by newlines.
2024-08-22 16:05:01 +02:00
Thorsten Ball
59dd7c9138 zig: Bump to v0.3.0 (#16669)
This PR bumps the Zig extension to v0.3.0

Changes:

- #16645

Release Notes:

- N/A
2024-08-22 15:56:09 +02:00
renovate[bot]
3c577e1a42 Update Rust crate aws-sdk-s3 to v1.46.0 (#16651)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [aws-sdk-s3](https://togithub.com/awslabs/aws-sdk-rust) | dependencies
| minor | `1.43.0` -> `1.46.0` |

---

### 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:eyJjcmVhdGVkSW5WZXIiOiIzOC4yNi4xIiwidXBkYXRlZEluVmVyIjoiMzguMjYuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-22 09:53:42 -04:00
versecafe
bb725d3158 zig: Unpin Zig LSP grab newest version off GH releases, and download from zigtools.org (#16645)
Fixed Zig LSP being pinned to 0.11.0 due to discontinuation of `.tar.gz`

Release Notes:

- N/A
2024-08-22 15:45:04 +02:00
renovate[bot]
5250866c1a Update Rust crate which to v6.0.3 (#16646)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [which](https://togithub.com/harryfei/which-rs) |
workspace.dependencies | patch | `6.0.2` -> `6.0.3` |

---

### Release Notes

<details>
<summary>harryfei/which-rs (which)</summary>

###
[`v6.0.3`](https://togithub.com/harryfei/which-rs/blob/HEAD/CHANGELOG.md#603)

[Compare
Source](https://togithub.com/harryfei/which-rs/compare/6.0.2...6.0.3)

- Enhance `tracing` feature with some `debug` level logs for higher
level logic.

</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:eyJjcmVhdGVkSW5WZXIiOiIzOC4yNi4xIiwidXBkYXRlZEluVmVyIjoiMzguMjYuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-22 15:31:50 +02:00
renovate[bot]
1ae96025f5 Update Rust crate arrayvec to v0.7.6 (#16614)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [arrayvec](https://togithub.com/bluss/arrayvec) | dependencies | patch
| `0.7.4` -> `0.7.6` |

---

### Release Notes

<details>
<summary>bluss/arrayvec (arrayvec)</summary>

###
[`v0.7.6`](https://togithub.com/bluss/arrayvec/blob/HEAD/CHANGELOG.md#076)

[Compare
Source](https://togithub.com/bluss/arrayvec/compare/0.7.5...0.7.6)

- Fix no-std build
[#&#8203;274](https://togithub.com/bluss/arrayvec/pull/274)

###
[`v0.7.5`](https://togithub.com/bluss/arrayvec/blob/HEAD/CHANGELOG.md#075)

[Compare
Source](https://togithub.com/bluss/arrayvec/compare/0.7.4...0.7.5)

- Add `as_ptr` and `as_mut_ptr` to `ArrayString`
[@&#8203;YuhanLiin](https://togithub.com/YuhanLiin)
[#&#8203;260](https://togithub.com/bluss/arrayvec/pull/260)
- Add borsh serialization support by
[@&#8203;honzasp](https://togithub.com/honzasp) and
[@&#8203;Fuuzetsu](https://togithub.com/Fuuzetsu)
[#&#8203;259](https://togithub.com/bluss/arrayvec/pull/259)
- Move length field before before data in ArrayVec and ArrayString by
[@&#8203;JakkuSakura](https://togithub.com/JakkuSakura)
[#&#8203;255](https://togithub.com/bluss/arrayvec/pull/255)
- Fix miri error for ZST case in extend by
[@&#8203;bluss](https://togithub.com/bluss)
- implement AsRef<Path> for ArrayString by
[@&#8203;Zoybean](https://togithub.com/Zoybean)
[#&#8203;218](https://togithub.com/bluss/arrayvec/pull/218)
- Fix typos in changelog by
[@&#8203;striezel](https://togithub.com/striezel)
[#&#8203;241](https://togithub.com/bluss/arrayvec/pull/241)
- Add `as_slice`, `as_mut_slice` methods to `IntoIter` by
[@&#8203;clarfonthey](https://togithub.com/clarfonthey)
[#&#8203;224](https://togithub.com/bluss/arrayvec/pull/224)

</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:eyJjcmVhdGVkSW5WZXIiOiIzOC4yNi4xIiwidXBkYXRlZEluVmVyIjoiMzguMjYuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-22 08:53:27 -04:00
Kirill Bulatov
6b9fa68dc5 Force Vue and Svelte language servers to be the first in the list for their languages (#16654)
Follow-up of https://github.com/zed-industries/zed/pull/15624

Fixes https://github.com/zed-industries/zed/issues/13769
Fixes https://github.com/zed-industries/zed/issues/16469

This way, those are considered "primary" and serve all LSP requests like
go to definition. Before, Tailwind language server was first and
returned nothing for all LSP requests.

- Fixed Vue and Svelte languages integrations not handling LSP requests
properly ([#13769](https://github.com/zed-industries/zed/issues/13769))
([#16469](https://github.com/zed-industries/zed/issues/16469))
2024-08-22 15:36:31 +03:00
Thorsten Ball
db0c1fd592 vim: Add 'gf' command, make files cmd-clickable (#16534)
Release Notes:

- vim: Added `gf` command to open files under the cursor.
- Filenames can now be `cmd`/`ctrl`-clicked, which opens them.

TODOs:

- [x] `main_test.go` <-- works
- [x] `./my-pkg/my_pkg.go` <-- works
- [x] `../go.mod` <-- works
- [x] `my-pkg/my_pkg.go` <-- works
- [x] `my-pkg/subpkg/subpkg_test.go` <-- works
- [x] `file\ with\ space\ in\ it.txt` <-- works
- [x] `"file\ with\ space\ in\ it.txt"` <-- works
- [x] `"main_test.go"` <-- works
- [x] `/Users/thorstenball/.vimrc` <-- works, but only locally
- [x] `~/.vimrc` <--works, but only locally
- [x] Get it working over collab
- [x] Get hover links working

Demo:



https://github.com/user-attachments/assets/26af7f3b-c392-4aaf-849a-95d6c3b00067

Collab demo:




https://github.com/user-attachments/assets/272598bd-0e82-4556-8f9c-ba53d3a95682
2024-08-22 14:27:11 +02:00
Henrikh Kantuni
1e39d407c2 Fix typo (#16657)
`format_on_save` → `formatter`

Release Notes:
- N/A
2024-08-22 14:02:41 +03:00
Kirill Bulatov
61ca36ecad Document proper default value for auto_fold_dirs 2024-08-22 13:56:27 +03:00
Conrad Irwin
eb9eae09b1 Fix manual copilot with show_inline_completions: false (#16621)
For @mre and friends!

Release Notes:

- Fixed manually trigging completions when `show_inline_completions:
false`
2024-08-21 20:27:19 -06:00
Peter Tripp
136f75ee9a docs: Update telemetry documentation (#16628)
- Add references to locations in code for Metrics and Panic telemetry
- Remove outdated documentation (ClickhouseEvent,
ClickhouseEventWrapper, ClickhouseEventRequestBody)
- Migrate struct documentation from web docs to inline doc comments on
struct members.
2024-08-21 20:24:35 -04:00
Danilo Leal
1f8fa82ac3 docs: Add missing link to the Prompt Library page (#16639)
Added in the Command page within the Assistant section.

Release Notes:

- N/A
2024-08-21 20:01:49 -03:00
renovate[bot]
8895084604 Update Rust crate tokio to v1.39.3 (#16634)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [tokio](https://tokio.rs)
([source](https://togithub.com/tokio-rs/tokio)) | dependencies | patch |
`1.39.2` -> `1.39.3` |
| [tokio](https://tokio.rs)
([source](https://togithub.com/tokio-rs/tokio)) | workspace.dependencies
| patch | `1.39.2` -> `1.39.3` |

---

### Release Notes

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

###
[`v1.39.3`](https://togithub.com/tokio-rs/tokio/releases/tag/tokio-1.39.3):
Tokio v1.39.3

[Compare
Source](https://togithub.com/tokio-rs/tokio/compare/tokio-1.39.2...tokio-1.39.3)

### 1.39.3 (August 17th, 2024)

This release fixes a regression where the unix socket api stopped
accepting the abstract socket namespace. ([#&#8203;6772])

[#&#8203;6772]: https://togithub.com/tokio-rs/tokio/pull/6772

</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 these
updates again.

---

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

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOC4yNi4xIiwidXBkYXRlZEluVmVyIjoiMzguMjYuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-21 18:54:46 -04:00
evren
1abbe9c65d Update .mailmap (#16640)
Updated mailmap to contain my correct github noreply mail address.

Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-08-21 18:51:58 -04:00
Danilo Leal
ec98e71190 docs: Add tweaks to the assistant Configuration page (#16632)
This PR adds some slight writing tweaks to the Configuration page under
the assistant section. As a general rule of thumb, I usually avoid
adding links in the word "here" when that's within a sentence; a more
descriptive approach can be clearer.

---

Release Notes:

- N/A
2024-08-21 19:26:15 -03:00
Marshall Bowers
1d986b0c77 collab: Report active user counts separately, as well (#16629)
This PR adds additional reporting of the active user counts as separate
logs.

We were already reporting these on individual rate limit events/logs,
but it seems like something that would be good to report on independent
of user activity.

Release Notes:

- N/A
2024-08-21 18:15:15 -04:00
Cherry
feab1261c8 Fix some typos (#16623)
This PR fixes some typos I found in the source code.

Release Notes:

- N/A
2024-08-21 17:33:19 -04:00
Michael Angerman
406d3b413d gpui: Remove extra "which" in comment (#16620)
Fix a typo in the comment...

Release Notes:

- N/A
2024-08-21 17:24:38 -04:00
renovate[bot]
643d60f551 Update rui314/setup-mold digest to 0bf4f07 (#16613)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [rui314/setup-mold](https://togithub.com/rui314/setup-mold) | action |
digest | `2e332a0` -> `0bf4f07` |

---

### 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:eyJjcmVhdGVkSW5WZXIiOiIzOC4yNi4xIiwidXBkYXRlZEluVmVyIjoiMzguMjYuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-21 17:23:57 -04:00
Marshall Bowers
0229d3ccac collab: Track active user counts independently for each model (#16624)
This PR fixes an issue where the active user count spanned individual
models.

We now track the active user counts on a per-model basis.

Release Notes:

- N/A
2024-08-21 17:19:47 -04:00
Thorben Kröger
f85ca387a7 clangd: Implement switch source/header extension (#14646)
Release Notes:

- Added switch source/header action for clangd language server (fixes
[#12801](https://github.com/zed-industries/zed/issues/12801)).

Note: I'm new to both rust and this codebase. I started my
implementation by copying how rust analyzer's "expand macro" LSP
extension is implemented. I don't yet understand some of the code I
copied (mostly the way to get the `server_to_query` in `clangd_ext.rs`
and the whole proto implementation).

---------

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2024-08-21 22:15:08 +03:00
Marshall Bowers
96bcceed40 collab: Add traces for user LLM rate limits (#16610)
This PR adds traces for when users hit LLM rate limits.

We were already emitting telemetry events for these to Clickhouse, but
it will be handy to have them available in Axiom as well.

Release Notes:

- N/A
2024-08-21 15:13:55 -04:00
Kyle Kelley
2ad9a742dd repl: Add restart kernel action and improve shutdown (#16609)
- Implement restart kernel functionality
- Clean up shutdown process to properly drop messaging and exit status
tasks
- Refactor kernel state handling for better consistency

Closes #16037

Release Notes:

- repl: Added restart kernel action
- repl: Fixed issue with shutting down kernels that are in a failure
state
2024-08-21 11:51:58 -07:00
Ikko Eltociear Ashimine
9f0438b540 gpui: Remove extra "the" in comment (#16608)
Release Notes:

- N/A
2024-08-21 14:07:51 -04:00
69 changed files with 2276 additions and 999 deletions

View File

@@ -363,7 +363,7 @@ jobs:
sudo apt-get install -y llvm-10 clang-10 build-essential cmake pkg-config libasound2-dev libfontconfig-dev libwayland-dev libxkbcommon-x11-dev libssl-dev libsqlite3-dev libzstd-dev libvulkan1 libgit2-dev
echo "/usr/lib/llvm-10/bin" >> $GITHUB_PATH
- uses: rui314/setup-mold@2e332a0b602c2fc65d2d3995941b1b29a5f554a0 # v1
- uses: rui314/setup-mold@0bf4f07ef9048ec62a45f9dbf2f098afa49695f0 # v1
with:
mold-version: 2.32.0

View File

@@ -157,7 +157,7 @@ jobs:
sudo apt-get install -y llvm-10 clang-10 build-essential cmake pkg-config libasound2-dev libfontconfig-dev libwayland-dev libxkbcommon-x11-dev libssl-dev libsqlite3-dev libzstd-dev libvulkan1 libgit2-dev
echo "/usr/lib/llvm-10/bin" >> $GITHUB_PATH
- uses: rui314/setup-mold@2e332a0b602c2fc65d2d3995941b1b29a5f554a0 # v1
- uses: rui314/setup-mold@0bf4f07ef9048ec62a45f9dbf2f098afa49695f0 # v1
with:
mold-version: 2.32.0

View File

@@ -24,7 +24,8 @@ Conrad Irwin <conrad@zed.dev>
Conrad Irwin <conrad@zed.dev> <conrad.irwin@gmail.com>
Danilo Leal <danilo@zed.dev>
Danilo Leal <danilo@zed.dev> <67129314+danilo-leal@users.noreply.github.com>
Evren Sen <146845123+evrsen@users.noreply.github.com>
Evren Sen <146845123+evrensen467@users.noreply.github.com>
Evren Sen <146845123+evrensen467@users.noreply.github.com> <146845123+evrsen@users.noreply.github.com>
Fernando Tagawa <tagawafernando@gmail.com>
Fernando Tagawa <tagawafernando@gmail.com> <fernando.tagawa.gamail.com@gmail.com>
Greg Morenz <greg-morenz@droid.cafe>

69
Cargo.lock generated
View File

@@ -83,7 +83,7 @@ dependencies = [
[[package]]
name = "alacritty_terminal"
version = "0.24.1-dev"
source = "git+https://github.com/alacritty/alacritty?rev=cacdb5bb3b72bad2c729227537979d95af75978f#cacdb5bb3b72bad2c729227537979d95af75978f"
source = "git+https://github.com/alacritty/alacritty?rev=91d034ff8b53867143c005acfaa14609147c9a2c#91d034ff8b53867143c005acfaa14609147c9a2c"
dependencies = [
"base64 0.22.1",
"bitflags 2.6.0",
@@ -282,9 +282,9 @@ checksum = "9d151e35f61089500b617991b791fc8bfd237ae50cd5950803758a179b41e67a"
[[package]]
name = "arrayvec"
version = "0.7.4"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "as-raw-xcb-connection"
@@ -1055,9 +1055,9 @@ dependencies = [
[[package]]
name = "aws-sdk-s3"
version = "1.43.0"
version = "1.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ccda7e730ace3cb8bbd4071bc650c6d294364891f9564bd4e43adfc8dea3177"
checksum = "4abf69a87be33b6f125a93d5046b5f7395c26d1f449bf8d3927f5577463b6de0"
dependencies = [
"ahash 0.8.11",
"aws-credential-types",
@@ -1269,9 +1269,9 @@ dependencies = [
[[package]]
name = "aws-smithy-runtime"
version = "1.6.2"
version = "1.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce87155eba55e11768b8c1afa607f3e864ae82f03caf63258b37455b0ad02537"
checksum = "0abbf454960d0db2ad12684a1640120e7557294b0ff8e2f11236290a1b293225"
dependencies = [
"aws-smithy-async",
"aws-smithy-http",
@@ -1313,9 +1313,9 @@ dependencies = [
[[package]]
name = "aws-smithy-types"
version = "1.2.0"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfe321a6b21f5d8eabd0ade9c55d3d0335f3c3157fc2b3e87f05f34b539e4df5"
checksum = "6cee7cadb433c781d3299b916fbf620fea813bf38f49db282fb6858141a05cc8"
dependencies = [
"base64-simd",
"bytes 1.7.1",
@@ -1541,7 +1541,7 @@ dependencies = [
"bitflags 2.6.0",
"cexpr",
"clang-sys",
"itertools 0.12.1",
"itertools 0.10.5",
"lazy_static",
"lazycell",
"proc-macro2",
@@ -1624,7 +1624,7 @@ dependencies = [
[[package]]
name = "blade-graphics"
version = "0.4.0"
source = "git+https://github.com/kvark/blade?rev=ac25c77ed8d86c386a541c935ffe0a0f6024e701#ac25c77ed8d86c386a541c935ffe0a0f6024e701"
source = "git+https://github.com/kvark/blade?rev=7f54ddfc001edc48225e6602d3c38ebb855421aa#7f54ddfc001edc48225e6602d3c38ebb855421aa"
dependencies = [
"ash",
"ash-window",
@@ -1654,7 +1654,7 @@ dependencies = [
[[package]]
name = "blade-macros"
version = "0.2.1"
source = "git+https://github.com/kvark/blade?rev=ac25c77ed8d86c386a541c935ffe0a0f6024e701#ac25c77ed8d86c386a541c935ffe0a0f6024e701"
source = "git+https://github.com/kvark/blade?rev=7f54ddfc001edc48225e6602d3c38ebb855421aa#7f54ddfc001edc48225e6602d3c38ebb855421aa"
dependencies = [
"proc-macro2",
"quote",
@@ -1664,7 +1664,7 @@ dependencies = [
[[package]]
name = "blade-util"
version = "0.1.0"
source = "git+https://github.com/kvark/blade?rev=ac25c77ed8d86c386a541c935ffe0a0f6024e701#ac25c77ed8d86c386a541c935ffe0a0f6024e701"
source = "git+https://github.com/kvark/blade?rev=7f54ddfc001edc48225e6602d3c38ebb855421aa#7f54ddfc001edc48225e6602d3c38ebb855421aa"
dependencies = [
"blade-graphics",
"bytemuck",
@@ -2211,9 +2211,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.15"
version = "4.5.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11d8838454fda655dafd3accb2b6e2bea645b9e4078abe84a22ceb947235c5cc"
checksum = "ed6719fffa43d0d87e5fd8caeab59be1554fb028cd30edc88fc4369b17971019"
dependencies = [
"clap_builder",
"clap_derive",
@@ -5874,9 +5874,9 @@ dependencies = [
[[package]]
name = "khronos-egl"
version = "5.0.0"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1382b16c04aeb821453d6215a3c80ba78f24c6595c5aa85653378aabe0c83e3"
checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76"
dependencies = [
"libc",
"libloading",
@@ -6174,7 +6174,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4"
dependencies = [
"cfg-if",
"windows-targets 0.52.6",
"windows-targets 0.48.5",
]
[[package]]
@@ -8127,7 +8127,7 @@ dependencies = [
"text",
"unindent",
"util",
"which 6.0.2",
"which 6.0.3",
"worktree",
]
@@ -9573,18 +9573,18 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.207"
version = "1.0.208"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5665e14a49a4ea1b91029ba7d3bca9f299e1f7cfa194388ccc20f14743e784f2"
checksum = "cff085d2cb684faa248efb494c39b68e522822ac0de72ccf08109abde717cfb2"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.207"
version = "1.0.208"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6aea2634c86b0e8ef2cfdc0c340baede54ec27b1e46febd7f80dffb2aa44a00e"
checksum = "24008e81ff7613ed8e5ba0cfaf24e2c2f1e5b8a0495711e44fcd4882fca62bcf"
dependencies = [
"proc-macro2",
"quote",
@@ -11290,9 +11290,9 @@ dependencies = [
[[package]]
name = "tokio"
version = "1.39.2"
version = "1.39.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1"
checksum = "9babc99b9923bfa4804bd74722ff02c0381021eafa4db9949217e3be8e84fff5"
dependencies = [
"backtrace",
"bytes 1.7.1",
@@ -11657,9 +11657,9 @@ dependencies = [
[[package]]
name = "tree-sitter-css"
version = "0.21.0"
version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2f806f96136762b0121f5fdd7172a3dcd8f42d37a2f23ed7f11b35895e20eb4"
checksum = "5e08e324b1cf60fd3291774b49724c66de2ce8fcf4d358d0b4b82e37b41b1c9b"
dependencies = [
"cc",
"tree-sitter",
@@ -11687,9 +11687,9 @@ dependencies = [
[[package]]
name = "tree-sitter-go"
version = "0.21.0"
version = "0.21.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55cb318be5ccf75f44e054acf6898a5c95d59b53443eed578e16be0cd7ec037f"
checksum = "b8d702a98d3c7e70e466456e58ff2b1ac550bf1e29b97e5770676d2fdabec00d"
dependencies = [
"cc",
"tree-sitter",
@@ -11724,9 +11724,9 @@ dependencies = [
[[package]]
name = "tree-sitter-html"
version = "0.20.3"
version = "0.20.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95b3492b08a786bf5cc79feb0ef2ff3b115d5174364e0ddfd7860e0b9b088b53"
checksum = "8766b5ad3721517f8259e6394aefda9c686aebf7a8c74ab8624f2c3b46902fd5"
dependencies = [
"cc",
"tree-sitter",
@@ -12879,9 +12879,9 @@ dependencies = [
[[package]]
name = "which"
version = "6.0.2"
version = "6.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d9c5ed668ee1f17edb3b627225343d210006a90bb1e3745ce1f30b1fb115075"
checksum = "b4ee928febd44d98f2f459a4a79bd4d928591333a494a10a868418ac1b39cf1f"
dependencies = [
"either",
"home",
@@ -13913,6 +13913,7 @@ dependencies = [
"terminal_view",
"theme",
"theme_selector",
"time",
"tree-sitter-md",
"tree-sitter-rust",
"ui",
@@ -14161,7 +14162,7 @@ dependencies = [
[[package]]
name = "zed_zig"
version = "0.2.0"
version = "0.3.0"
dependencies = [
"zed_extension_api 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
]

View File

@@ -305,7 +305,7 @@ zed_actions = { path = "crates/zed_actions" }
#
aho-corasick = "1.1"
alacritty_terminal = { git = "https://github.com/alacritty/alacritty", rev = "cacdb5bb3b72bad2c729227537979d95af75978f" }
alacritty_terminal = { git = "https://github.com/alacritty/alacritty", rev = "91d034ff8b53867143c005acfaa14609147c9a2c" }
any_vec = "0.14"
anyhow = "1.0.86"
ashpd = "0.9.1"
@@ -321,9 +321,9 @@ async-watch = "0.3.1"
async_zip = { version = "0.0.17", features = ["deflate", "deflate64"] }
base64 = "0.22"
bitflags = "2.6.0"
blade-graphics = { git = "https://github.com/kvark/blade", rev = "ac25c77ed8d86c386a541c935ffe0a0f6024e701" }
blade-macros = { git = "https://github.com/kvark/blade", rev = "ac25c77ed8d86c386a541c935ffe0a0f6024e701" }
blade-util = { git = "https://github.com/kvark/blade", rev = "ac25c77ed8d86c386a541c935ffe0a0f6024e701" }
blade-graphics = { git = "https://github.com/kvark/blade", rev = "7f54ddfc001edc48225e6602d3c38ebb855421aa" }
blade-macros = { git = "https://github.com/kvark/blade", rev = "7f54ddfc001edc48225e6602d3c38ebb855421aa" }
blade-util = { git = "https://github.com/kvark/blade", rev = "7f54ddfc001edc48225e6602d3c38ebb855421aa" }
cargo_metadata = "0.18"
cargo_toml = "0.20"
chrono = { version = "0.4", features = ["serde"] }
@@ -388,7 +388,7 @@ runtimelib = { version = "0.15", default-features = false, features = [
rusqlite = { version = "0.29.0", features = ["blob", "array", "modern_sqlite"] }
rustc-demangle = "0.1.23"
rust-embed = { version = "8.4", features = ["include-exclude"] }
schemars = {version = "0.8", features = ["impl_json_schema"]}
schemars = { version = "0.8", features = ["impl_json_schema"] }
semver = "1.0"
serde = { version = "1.0", features = ["derive", "rc"] }
serde_derive = { version = "1.0", features = ["deserialize_in_place"] }

View File

@@ -0,0 +1 @@
{}

View File

@@ -0,0 +1,59 @@
{
// https://github.com/zed-industries/zed/issues/16343
// See: https://support.apple.com/en-us/102650
"ctrl-up": "Mission Control",
"ctrl-down": "Application windows",
"ctrl-left": "Move left a space",
"ctrl-right": "Move right a space",
"f11": "Show Desktop",
"f14": "Decrease brightness",
"f15": "Increase brightness",
"ctrl-f14": "Decrease brightness (external display)",
"ctrl-f15": "Increase brightness (external display)",
"alt-f14": "Open Displays preferences",
"alt-f15": "Open Displays preferences",
"cmd-shift-q": "Logout (with confirmation)",
"cmd-alt-shift-q": "Logout (without confirmation)",
"cmd-ctrl-q": "Lock Screen",
"cmd-space": "Spotlight",
"cmd-alt-space": "Show Finder search Window",
"cmd-alt-d": "Turn Dock hiding on/off",
"cmd-alt-escape": "Force Quit",
"cmd-h": "Hide the windows of the front app",
"cmd-alt-h": "Hide the windows of all other apps",
"cmd-m": "Minimize the front window to the Dock.",
"cmd-tab": "Switch apps",
"cmd-shift-tab": "Switch apps in reverse",
"cmd-`": "Activate the next open window in the front app.",
"cmd-<": "Activate the next open window in the front app. (Swedish, etc)",
"cmd->": "Activate the previous open window in the front app. (Swedish, etc)",
"cmd-shift-`": "Activate the previous open window in the front app.",
"cmd-alt-f5": "Show Accessibility controls",
"cmd-f5": "Turn VoiceOver on or off",
"ctrl-f1": "Turn keyboard access on or off",
"ctrl-f2": "Move focus to the menu bar",
"ctrl-f3": "Move focus to the Dock.",
"ctrl-f4": "Move focus to the active or next window.",
"ctrl-f5": "Move focus to the window toolbar.",
"ctrl-f6": "Move focus to the floating window.",
"ctrl-f7": "Change the way Tab moves focus",
"ctrl-f8": "Move focus to the status menu in the menu bar",
"ctrl-shift-f4": "Move focus to the previous window.",
"ctrl-shift-f5": "Move focus to the window toolbar.",
"ctrl-shift-f6": "Move focus to the previous panel.",
// Screenshots
"cmd-shift-3": "Save picture of screen as file",
"cmd-shift-4": "Save picture of selected area as file",
"cmd-shift-5": "Screenshot and recording options",
"cmd-shift-ctrl-3": "Copy picture of screen to the clipboard",
"cmd-shift-ctrl-4": "Copy picture of selected area to the clipboard",
// Input
"ctrl-space": "Select the previous input source",
"ctrl-shift-space": "Select the next source in Input menu",
"ctrl-cmd-space": "Show the Character Viewer (emojis)",
// Others
"ctrl-alt-cmd-8": "invert colors (disabled by default)",
"ctrl-alt-cmd-,": "decrease contrast (disabled by default)",
"ctrl-alt-cmd-.": "increase contrast (disabled by default)",
"cmd-alt-`": "Move focus to the window drawer. (deprecated)"
}

View File

@@ -92,6 +92,7 @@
"g y": "editor::GoToTypeDefinition",
"g shift-i": "editor::GoToImplementation",
"g x": "editor::OpenUrl",
"g f": "editor::OpenFile",
"g n": "vim::SelectNextMatch",
"g shift-n": "vim::SelectPreviousMatch",
"g l": "vim::SelectNext",
@@ -176,19 +177,19 @@
"ctrl-w ctrl-p": "workspace::ActivatePreviousPane",
"ctrl-w shift-w": "workspace::ActivatePreviousPane",
"ctrl-w ctrl-shift-w": "workspace::ActivatePreviousPane",
"ctrl-w v": "pane::SplitLeft",
"ctrl-w ctrl-v": "pane::SplitLeft",
"ctrl-w s": "pane::SplitUp",
"ctrl-w shift-s": "pane::SplitUp",
"ctrl-w ctrl-s": "pane::SplitUp",
"ctrl-w v": "pane::SplitVertical",
"ctrl-w ctrl-v": "pane::SplitVertical",
"ctrl-w s": "pane::SplitHorizontal",
"ctrl-w shift-s": "pane::SplitHorizontal",
"ctrl-w ctrl-s": "pane::SplitHorizontal",
"ctrl-w c": "pane::CloseAllItems",
"ctrl-w ctrl-c": "pane::CloseAllItems",
"ctrl-w q": "pane::CloseAllItems",
"ctrl-w ctrl-q": "pane::CloseAllItems",
"ctrl-w o": "workspace::CloseInactiveTabsAndPanes",
"ctrl-w ctrl-o": "workspace::CloseInactiveTabsAndPanes",
"ctrl-w n": ["workspace::NewFileInDirection", "Up"],
"ctrl-w ctrl-n": ["workspace::NewFileInDirection", "Up"],
"ctrl-w n": "workspace::NewFileSplitHorizontal",
"ctrl-w ctrl-n": "workspace::NewFileSplitHorizontal",
"ctrl-w d": "editor::GoToDefinitionSplit",
"ctrl-w g d": "editor::GoToDefinitionSplit",
"ctrl-w shift-d": "editor::GoToTypeDefinitionSplit",

View File

@@ -69,6 +69,10 @@
// The factor to grow the active pane by. Defaults to 1.0
// which gives the same size as all other panes.
"active_pane_magnification": 1.0,
// The direction that you want to split panes horizontally. Defaults to "up"
"pane_split_direction_horizontal": "up",
// The direction that you want to split panes horizontally. Defaults to "left"
"pane_split_direction_vertical": "left",
// Centered layout related settings.
"centered_layout": {
// The relative width of the left padding of the central pane from the
@@ -506,6 +510,8 @@
// "soft_wrap": "editor_width",
// 4. Soft wrap lines at the preferred line length.
// "soft_wrap": "preferred_line_length",
// 5. Soft wrap lines at the preferred line length or the editor width (whichever is smaller).
// "soft_wrap": "bounded",
"soft_wrap": "prefer_line",
// The column at which to soft-wrap lines, for buffers where soft-wrap
// is enabled.
@@ -838,6 +844,7 @@
"language_servers": ["starpls", "!buck2-lsp", "..."]
},
"Svelte": {
"language_servers": ["svelte-language-server", "..."],
"prettier": {
"allowed": true,
"plugins": ["prettier-plugin-svelte"]
@@ -861,6 +868,7 @@
}
},
"Vue.js": {
"language_servers": ["vue-language-server", "..."],
"prettier": {
"allowed": true
}

View File

@@ -3,10 +3,9 @@ use editor::Editor;
use extension::ExtensionStore;
use futures::StreamExt;
use gpui::{
actions, anchored, deferred, percentage, Animation, AnimationExt as _, AppContext, CursorStyle,
DismissEvent, EventEmitter, InteractiveElement as _, Model, ParentElement as _, Render,
SharedString, StatefulInteractiveElement, Styled, Transformation, View, ViewContext,
VisualContext as _,
actions, percentage, Animation, AnimationExt as _, AppContext, CursorStyle, EventEmitter,
InteractiveElement as _, Model, ParentElement as _, Render, SharedString,
StatefulInteractiveElement, Styled, Transformation, View, ViewContext, VisualContext as _,
};
use language::{
LanguageRegistry, LanguageServerBinaryStatus, LanguageServerId, LanguageServerName,
@@ -14,7 +13,7 @@ use language::{
use project::{LanguageServerProgress, Project};
use smallvec::SmallVec;
use std::{cmp::Reverse, fmt::Write, sync::Arc, time::Duration};
use ui::{prelude::*, ContextMenu};
use ui::{prelude::*, ButtonLike, ContextMenu, PopoverMenu, PopoverMenuHandle};
use workspace::{item::ItemHandle, StatusItemView, Workspace};
actions!(activity_indicator, [ShowErrorMessage]);
@@ -27,7 +26,7 @@ pub struct ActivityIndicator {
statuses: Vec<LspStatus>,
project: Model<Project>,
auto_updater: Option<Model<AutoUpdater>>,
context_menu: Option<View<ContextMenu>>,
context_menu_handle: PopoverMenuHandle<ContextMenu>,
}
struct LspStatus {
@@ -79,7 +78,7 @@ impl ActivityIndicator {
statuses: Default::default(),
project: project.clone(),
auto_updater,
context_menu: None,
context_menu_handle: Default::default(),
}
});
@@ -368,72 +367,7 @@ impl ActivityIndicator {
}
fn toggle_language_server_work_context_menu(&mut self, cx: &mut ViewContext<Self>) {
if self.context_menu.take().is_some() {
return;
}
self.build_lsp_work_context_menu(cx);
cx.notify();
}
fn build_lsp_work_context_menu(&mut self, cx: &mut ViewContext<Self>) {
let mut has_work = false;
let this = cx.view().downgrade();
let context_menu = ContextMenu::build(cx, |mut menu, cx| {
for work in self.pending_language_server_work(cx) {
has_work = true;
let this = this.clone();
let title = SharedString::from(
work.progress
.title
.as_deref()
.unwrap_or(work.progress_token)
.to_string(),
);
if work.progress.is_cancellable {
let language_server_id = work.language_server_id;
let token = work.progress_token.to_string();
menu = menu.custom_entry(
move |_| {
h_flex()
.w_full()
.justify_between()
.child(Label::new(title.clone()))
.child(Icon::new(IconName::XCircle))
.into_any_element()
},
move |cx| {
this.update(cx, |this, cx| {
this.project.update(cx, |project, cx| {
project.cancel_language_server_work(
language_server_id,
Some(token.clone()),
cx,
);
});
this.context_menu.take();
})
.ok();
},
);
} else {
menu = menu.label(title.clone());
}
}
menu
});
if has_work {
cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| {
this.context_menu.take();
cx.notify();
})
.detach();
cx.focus_view(&context_menu);
self.context_menu = Some(context_menu);
cx.notify();
}
self.context_menu_handle.toggle(cx);
}
}
@@ -455,19 +389,72 @@ impl Render for ActivityIndicator {
on_click(this, cx);
}))
}
result
.gap_2()
.children(content.icon)
.child(Label::new(SharedString::from(content.message)).size(LabelSize::Small))
.children(self.context_menu.as_ref().map(|menu| {
deferred(
anchored()
.anchor(gpui::AnchorCorner::BottomLeft)
.child(menu.clone()),
let this = cx.view().downgrade();
result.gap_2().child(
PopoverMenu::new("activity-indicator-popover")
.trigger(
ButtonLike::new("activity-indicator-trigger").child(
h_flex()
.gap_2()
.children(content.icon)
.child(Label::new(content.message).size(LabelSize::Small)),
),
)
.with_priority(1)
}))
.anchor(gpui::AnchorCorner::BottomLeft)
.menu(move |cx| {
let strong_this = this.upgrade()?;
ContextMenu::build(cx, |mut menu, cx| {
for work in strong_this.read(cx).pending_language_server_work(cx) {
let this = this.clone();
let mut title = work
.progress
.title
.as_deref()
.unwrap_or(work.progress_token)
.to_owned();
if work.progress.is_cancellable {
let language_server_id = work.language_server_id;
let token = work.progress_token.to_string();
let title = SharedString::from(title);
menu = menu.custom_entry(
move |_| {
h_flex()
.w_full()
.justify_between()
.child(Label::new(title.clone()))
.child(Icon::new(IconName::XCircle))
.into_any_element()
},
move |cx| {
this.update(cx, |this, cx| {
this.project.update(cx, |project, cx| {
project.cancel_language_server_work(
language_server_id,
Some(token.clone()),
cx,
);
});
this.context_menu_handle.hide(cx);
cx.notify();
})
.ok();
},
);
} else {
if let Some(progress_message) = work.progress.message.as_ref() {
title.push_str(": ");
title.push_str(progress_message);
}
menu = menu.label(title);
}
}
menu
})
.into()
}),
)
}
}

View File

@@ -36,10 +36,10 @@ use fs::Fs;
use gpui::{
canvas, div, img, percentage, point, pulsating_between, size, Action, Animation, AnimationExt,
AnyElement, AnyView, AppContext, AsyncWindowContext, ClipboardEntry, ClipboardItem,
Context as _, DismissEvent, Empty, Entity, EntityId, EventEmitter, FocusHandle, FocusableView,
FontWeight, InteractiveElement, IntoElement, Model, ParentElement, Pixels, ReadGlobal, Render,
RenderImage, SharedString, Size, StatefulInteractiveElement, Styled, Subscription, Task,
Transformation, UpdateGlobal, View, VisualContext, WeakView, WindowContext,
Context as _, Empty, Entity, EntityId, EventEmitter, FocusHandle, FocusableView, FontWeight,
InteractiveElement, IntoElement, Model, ParentElement, Pixels, ReadGlobal, Render, RenderImage,
SharedString, Size, StatefulInteractiveElement, Styled, Subscription, Task, Transformation,
UpdateGlobal, View, VisualContext, WeakView, WindowContext,
};
use indexed_docs::IndexedDocsStore;
use language::{
@@ -349,6 +349,7 @@ impl AssistantPanel {
model_summary_editor.clone(),
)
});
let pane = cx.new_view(|cx| {
let mut pane = Pane::new(
workspace.weak_handle(),
@@ -385,6 +386,7 @@ impl AssistantPanel {
pane.active_item()
.map_or(false, |item| item.downcast::<ContextHistory>().is_some()),
);
let _pane = cx.view().clone();
let right_children = h_flex()
.gap(Spacing::Small.rems(cx))
.child(
@@ -395,32 +397,27 @@ impl AssistantPanel {
.tooltip(|cx| Tooltip::for_action("New Context", &NewFile, cx)),
)
.child(
IconButton::new("menu", IconName::Menu)
.icon_size(IconSize::Small)
.on_click(cx.listener(|pane, _, cx| {
let zoom_label = if pane.is_zoomed() {
PopoverMenu::new("assistant-panel-popover-menu")
.trigger(
IconButton::new("menu", IconName::Menu).icon_size(IconSize::Small),
)
.menu(move |cx| {
let zoom_label = if _pane.read(cx).is_zoomed() {
"Zoom Out"
} else {
"Zoom In"
};
let menu = ContextMenu::build(cx, |menu, cx| {
menu.context(pane.focus_handle(cx))
let focus_handle = _pane.focus_handle(cx);
Some(ContextMenu::build(cx, move |menu, _| {
menu.context(focus_handle.clone())
.action("New Context", Box::new(NewFile))
.action("History", Box::new(DeployHistory))
.action("Prompt Library", Box::new(DeployPromptLibrary))
.action("Configure", Box::new(ShowConfiguration))
.action(zoom_label, Box::new(ToggleZoom))
});
cx.subscribe(&menu, |pane, _, _: &DismissEvent, _| {
pane.new_item_menu = None;
})
.detach();
pane.new_item_menu = Some(menu);
})),
}))
}),
)
.when_some(pane.new_item_menu.as_ref(), |el, new_item_menu| {
el.child(Pane::render_menu_overlay(new_item_menu))
})
.into_any_element()
.into();

View File

@@ -197,6 +197,7 @@ fn tab_items_for_queries(
}
let mut timestamps_by_entity_id = HashMap::default();
let mut visited_buffers = HashSet::default();
let mut open_buffers = Vec::new();
for pane in workspace.panes() {
@@ -211,9 +212,11 @@ fn tab_items_for_queries(
if let Some(timestamp) =
timestamps_by_entity_id.get(&editor.entity_id())
{
let snapshot = buffer.read(cx).snapshot();
let full_path = snapshot.resolve_file_path(cx, true);
open_buffers.push((full_path, snapshot, *timestamp));
if visited_buffers.insert(buffer.read(cx).remote_id()) {
let snapshot = buffer.read(cx).snapshot();
let full_path = snapshot.resolve_file_path(cx, true);
open_buffers.push((full_path, snapshot, *timestamp));
}
}
}
}

View File

@@ -18,6 +18,7 @@ use axum::{
Extension, Json, Router, TypedHeader,
};
use chrono::{DateTime, Duration, Utc};
use collections::HashMap;
use db::{usage_measure::UsageMeasure, ActiveUserCount, LlmDatabase};
use futures::{Stream, StreamExt as _};
use http_client::IsahcHttpClient;
@@ -29,6 +30,7 @@ use std::{
sync::Arc,
task::{Context, Poll},
};
use strum::IntoEnumIterator;
use telemetry::{report_llm_rate_limit, report_llm_usage, LlmRateLimitEventRow, LlmUsageEventRow};
use tokio::sync::RwLock;
use util::ResultExt;
@@ -41,7 +43,8 @@ pub struct LlmState {
pub db: Arc<LlmDatabase>,
pub http_client: IsahcHttpClient,
pub clickhouse_client: Option<clickhouse::Client>,
active_user_count: RwLock<Option<(DateTime<Utc>, ActiveUserCount)>>,
active_user_count_by_model:
RwLock<HashMap<(LanguageModelProvider, String), (DateTime<Utc>, ActiveUserCount)>>,
}
const ACTIVE_USER_COUNT_CACHE_DURATION: Duration = Duration::seconds(30);
@@ -69,9 +72,6 @@ impl LlmState {
.build()
.context("failed to construct http client")?;
let initial_active_user_count =
Some((Utc::now(), db.get_active_user_count(Utc::now()).await?));
let this = Self {
executor,
db,
@@ -80,25 +80,34 @@ impl LlmState {
.clickhouse_url
.as_ref()
.and_then(|_| build_clickhouse_client(&config).log_err()),
active_user_count: RwLock::new(initial_active_user_count),
active_user_count_by_model: RwLock::new(HashMap::default()),
config,
};
Ok(Arc::new(this))
}
pub async fn get_active_user_count(&self) -> Result<ActiveUserCount> {
pub async fn get_active_user_count(
&self,
provider: LanguageModelProvider,
model: &str,
) -> Result<ActiveUserCount> {
let now = Utc::now();
if let Some((last_updated, count)) = self.active_user_count.read().await.as_ref() {
if now - *last_updated < ACTIVE_USER_COUNT_CACHE_DURATION {
return Ok(*count);
{
let active_user_count_by_model = self.active_user_count_by_model.read().await;
if let Some((last_updated, count)) =
active_user_count_by_model.get(&(provider, model.to_string()))
{
if now - *last_updated < ACTIVE_USER_COUNT_CACHE_DURATION {
return Ok(*count);
}
}
}
let mut cache = self.active_user_count.write().await;
let new_count = self.db.get_active_user_count(now).await?;
*cache = Some((now, new_count));
let mut cache = self.active_user_count_by_model.write().await;
let new_count = self.db.get_active_user_count(provider, model, now).await?;
cache.insert((provider, model.to_string()), (now, new_count));
Ok(new_count)
}
}
@@ -419,7 +428,7 @@ async fn check_usage_limit(
)
.await?;
let active_users = state.get_active_user_count().await?;
let active_users = state.get_active_user_count(provider, model_name).await?;
let users_in_recent_minutes = active_users.users_in_recent_minutes.max(1);
let users_in_recent_days = active_users.users_in_recent_days.max(1);
@@ -463,6 +472,24 @@ async fn check_usage_limit(
};
if let Some(client) = state.clickhouse_client.as_ref() {
tracing::info!(
target: "user rate limit",
user_id = claims.user_id,
login = claims.github_user_login,
authn.jti = claims.jti,
is_staff = claims.is_staff,
provider = provider.to_string(),
model = model.name,
requests_this_minute = usage.requests_this_minute,
tokens_this_minute = usage.tokens_this_minute,
tokens_this_day = usage.tokens_this_day,
users_in_recent_minutes = users_in_recent_minutes,
users_in_recent_days = users_in_recent_days,
max_requests_per_minute = per_user_max_requests_per_minute,
max_tokens_per_minute = per_user_max_tokens_per_minute,
max_tokens_per_day = per_user_max_tokens_per_day,
);
report_llm_rate_limit(
client,
LlmRateLimitEventRow {
@@ -605,23 +632,39 @@ pub fn log_usage_periodically(state: Arc<LlmState>) {
.sleep(std::time::Duration::from_secs(30))
.await;
let Some(usages) = state
for provider in LanguageModelProvider::iter() {
for model in state.db.model_names_for_provider(provider) {
if let Some(active_user_count) = state
.get_active_user_count(provider, &model)
.await
.log_err()
{
tracing::info!(
target: "active user counts",
provider = provider.to_string(),
model = model,
users_in_recent_minutes = active_user_count.users_in_recent_minutes,
users_in_recent_days = active_user_count.users_in_recent_days,
);
}
}
}
if let Some(usages) = state
.db
.get_application_wide_usages_by_model(Utc::now())
.await
.log_err()
else {
continue;
};
for usage in usages {
tracing::info!(
target: "computed usage",
provider = usage.provider.to_string(),
model = usage.model,
requests_this_minute = usage.requests_this_minute,
tokens_this_minute = usage.tokens_this_minute,
);
{
for usage in usages {
tracing::info!(
target: "computed usage",
provider = usage.provider.to_string(),
model = usage.model,
requests_this_minute = usage.requests_this_minute,
tokens_this_minute = usage.tokens_this_minute,
);
}
}
}
})

View File

@@ -343,15 +343,27 @@ impl LlmDatabase {
.await
}
pub async fn get_active_user_count(&self, now: DateTimeUtc) -> Result<ActiveUserCount> {
/// Returns the active user count for the specified model.
pub async fn get_active_user_count(
&self,
provider: LanguageModelProvider,
model_name: &str,
now: DateTimeUtc,
) -> Result<ActiveUserCount> {
self.transaction(|tx| async move {
let minute_since = now - Duration::minutes(5);
let day_since = now - Duration::days(5);
let model = self
.models
.get(&(provider, model_name.to_string()))
.ok_or_else(|| anyhow!("unknown model {provider}:{model_name}"))?;
let users_in_recent_minutes = usage::Entity::find()
.filter(
usage::Column::Timestamp
.gte(minute_since.naive_utc())
usage::Column::ModelId
.eq(model.id)
.and(usage::Column::Timestamp.gte(minute_since.naive_utc()))
.and(usage::Column::IsStaff.eq(false)),
)
.select_only()
@@ -362,8 +374,9 @@ impl LlmDatabase {
let users_in_recent_days = usage::Entity::find()
.filter(
usage::Column::Timestamp
.gte(day_since.naive_utc())
usage::Column::ModelId
.eq(model.id)
.and(usage::Column::Timestamp.gte(day_since.naive_utc()))
.and(usage::Column::IsStaff.eq(false)),
)
.select_only()

View File

@@ -302,10 +302,7 @@ async fn handle_liveness_probe(
}
if let Some(llm_state) = llm_state {
llm_state
.db
.get_active_user_count(chrono::Utc::now())
.await?;
llm_state.db.list_providers().await?;
}
Ok("ok".to_string())

View File

@@ -1060,7 +1060,7 @@ mod tests {
editor.change_selections(None, cx, |selections| {
selections.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
});
editor.next_inline_completion(&Default::default(), cx);
editor.refresh_inline_completion(true, false, cx);
});
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
@@ -1070,7 +1070,7 @@ mod tests {
editor.change_selections(None, cx, |s| {
s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
});
editor.next_inline_completion(&Default::default(), cx);
editor.refresh_inline_completion(true, false, cx);
});
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);

View File

@@ -262,6 +262,7 @@ gpui::actions!(
OpenExcerptsSplit,
OpenPermalinkToLine,
OpenUrl,
OpenFile,
Outdent,
PageDown,
PageUp,
@@ -306,6 +307,7 @@ gpui::actions!(
SortLinesCaseInsensitive,
SortLinesCaseSensitive,
SplitSelectionIntoLines,
SwitchSourceHeader,
Tab,
TabPrev,
ToggleAutoSignatureHelp,

View File

@@ -0,0 +1,93 @@
use std::path::PathBuf;
use anyhow::Context as _;
use gpui::{View, ViewContext, WindowContext};
use language::Language;
use url::Url;
use crate::lsp_ext::find_specific_language_server_in_selection;
use crate::{element::register_action, Editor, SwitchSourceHeader};
static CLANGD_SERVER_NAME: &str = "clangd";
fn is_c_language(language: &Language) -> bool {
return language.name().as_ref() == "C++" || language.name().as_ref() == "C";
}
pub fn switch_source_header(
editor: &mut Editor,
_: &SwitchSourceHeader,
cx: &mut ViewContext<'_, Editor>,
) {
let Some(project) = &editor.project else {
return;
};
let Some(workspace) = editor.workspace() else {
return;
};
let Some((_, _, server_to_query, buffer)) =
find_specific_language_server_in_selection(&editor, cx, &is_c_language, CLANGD_SERVER_NAME)
else {
return;
};
let project = project.clone();
let buffer_snapshot = buffer.read(cx).snapshot();
let source_file = buffer_snapshot
.file()
.unwrap()
.file_name(cx)
.to_str()
.unwrap()
.to_owned();
let switch_source_header_task = project.update(cx, |project, cx| {
project.request_lsp(
buffer,
project::LanguageServerToQuery::Other(server_to_query),
project::lsp_ext_command::SwitchSourceHeader,
cx,
)
});
cx.spawn(|_editor, mut cx| async move {
let switch_source_header = switch_source_header_task
.await
.with_context(|| format!("Switch source/header LSP request for path \"{}\" failed", source_file))?;
if switch_source_header.0.is_empty() {
log::info!("Clangd returned an empty string when requesting to switch source/header from \"{}\"", source_file);
return Ok(());
}
let goto = Url::parse(&switch_source_header.0).with_context(|| {
format!(
"Parsing URL \"{}\" returned from switch source/header failed",
switch_source_header.0
)
})?;
workspace
.update(&mut cx, |workspace, view_cx| {
workspace.open_abs_path(PathBuf::from(goto.path()), false, view_cx)
})
.with_context(|| {
format!(
"Switch source/header could not open \"{}\" in workspace",
goto.path()
)
})?
.await
.map(|_| ())
})
.detach_and_log_err(cx);
}
pub fn apply_related_actions(editor: &View<Editor>, cx: &mut WindowContext) {
if editor.update(cx, |e, cx| {
find_specific_language_server_in_selection(e, cx, &is_c_language, CLANGD_SERVER_NAME)
.is_some()
}) {
register_action(editor, cx, switch_source_header);
}
}

View File

@@ -15,6 +15,7 @@
pub mod actions;
mod blame_entry_tooltip;
mod blink_manager;
mod clangd_ext;
mod debounced_delay;
pub mod display_map;
mod editor_settings;
@@ -30,6 +31,7 @@ mod inlay_hint_cache;
mod inline_completion_provider;
pub mod items;
mod linked_editing_ranges;
mod lsp_ext;
mod mouse_context_menu;
pub mod movement;
mod persistence;
@@ -97,7 +99,7 @@ use language::{point_to_lsp, BufferRow, Runnable, RunnableRange};
use linked_editing_ranges::refresh_linked_ranges;
use task::{ResolvedTask, TaskTemplate, TaskVariables};
use hover_links::{HoverLink, HoveredLinkState, InlayHighlight};
use hover_links::{find_file, HoverLink, HoveredLinkState, InlayHighlight};
pub use lsp::CompletionContext;
use lsp::{
CompletionItemKind, CompletionTriggerKind, DiagnosticSeverity, InsertTextFormat,
@@ -296,7 +298,8 @@ pub fn init(cx: &mut AppContext) {
cx.observe_new_views(
|workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
workspace.register_action(Editor::new_file);
workspace.register_action(Editor::new_file_in_direction);
workspace.register_action(Editor::new_file_vertical);
workspace.register_action(Editor::new_file_horizontal);
},
)
.detach();
@@ -372,6 +375,7 @@ pub enum SoftWrap {
PreferLine,
EditorWidth,
Column(u32),
Bounded(u32),
}
#[derive(Clone)]
@@ -2066,14 +2070,29 @@ impl Editor {
})
}
pub fn new_file_in_direction(
fn new_file_vertical(
workspace: &mut Workspace,
action: &workspace::NewFileInDirection,
_: &workspace::NewFileSplitVertical,
cx: &mut ViewContext<Workspace>,
) {
Self::new_file_in_direction(workspace, SplitDirection::vertical(cx), cx)
}
fn new_file_horizontal(
workspace: &mut Workspace,
_: &workspace::NewFileSplitHorizontal,
cx: &mut ViewContext<Workspace>,
) {
Self::new_file_in_direction(workspace, SplitDirection::horizontal(cx), cx)
}
fn new_file_in_direction(
workspace: &mut Workspace,
direction: SplitDirection,
cx: &mut ViewContext<Workspace>,
) {
let project = workspace.project().clone();
let create = project.update(cx, |project, cx| project.create_buffer(cx));
let direction = action.0;
cx.spawn(|workspace, mut cx| async move {
let buffer = create.await?;
@@ -2199,7 +2218,7 @@ impl Editor {
}),
provider: Arc::new(provider),
});
self.refresh_inline_completion(false, cx);
self.refresh_inline_completion(false, false, cx);
}
pub fn placeholder_text(&self, _cx: &WindowContext) -> Option<&str> {
@@ -3317,7 +3336,7 @@ impl Editor {
let trigger_in_words = !had_active_inline_completion;
this.trigger_completion_on_input(&text, trigger_in_words, cx);
linked_editing_ranges::refresh_linked_ranges(this, cx);
this.refresh_inline_completion(true, cx);
this.refresh_inline_completion(true, false, cx);
});
}
@@ -3503,7 +3522,7 @@ impl Editor {
.collect();
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections));
this.refresh_inline_completion(true, cx);
this.refresh_inline_completion(true, false, cx);
});
}
@@ -4391,7 +4410,7 @@ impl Editor {
})
}
this.refresh_inline_completion(true, cx);
this.refresh_inline_completion(true, false, cx);
});
let show_new_completions_on_confirm = completion
@@ -4891,17 +4910,19 @@ impl Editor {
None
}
fn refresh_inline_completion(
pub fn refresh_inline_completion(
&mut self,
debounce: bool,
user_requested: bool,
cx: &mut ViewContext<Self>,
) -> Option<()> {
let provider = self.inline_completion_provider()?;
let cursor = self.selections.newest_anchor().head();
let (buffer, cursor_buffer_position) =
self.buffer.read(cx).text_anchor_for_position(cursor, cx)?;
if !self.show_inline_completions
|| !provider.is_enabled(&buffer, cursor_buffer_position, cx)
if !user_requested
&& (!self.show_inline_completions
|| !provider.is_enabled(&buffer, cursor_buffer_position, cx))
{
self.discard_inline_completion(false, cx);
return None;
@@ -4935,7 +4956,7 @@ impl Editor {
pub fn show_inline_completion(&mut self, _: &ShowInlineCompletion, cx: &mut ViewContext<Self>) {
if !self.has_active_inline_completion(cx) {
self.refresh_inline_completion(false, cx);
self.refresh_inline_completion(false, true, cx);
return;
}
@@ -4964,7 +4985,7 @@ impl Editor {
if self.has_active_inline_completion(cx) {
self.cycle_inline_completion(Direction::Next, cx);
} else {
let is_copilot_disabled = self.refresh_inline_completion(false, cx).is_none();
let is_copilot_disabled = self.refresh_inline_completion(false, true, cx).is_none();
if is_copilot_disabled {
cx.propagate();
}
@@ -4979,7 +5000,7 @@ impl Editor {
if self.has_active_inline_completion(cx) {
self.cycle_inline_completion(Direction::Prev, cx);
} else {
let is_copilot_disabled = self.refresh_inline_completion(false, cx).is_none();
let is_copilot_disabled = self.refresh_inline_completion(false, true, cx).is_none();
if is_copilot_disabled {
cx.propagate();
}
@@ -5007,7 +5028,7 @@ impl Editor {
self.change_selections(None, cx, |s| s.select_ranges([range]))
}
self.insert_with_autoindent_mode(&completion.text.to_string(), None, cx);
self.refresh_inline_completion(true, cx);
self.refresh_inline_completion(true, true, cx);
cx.notify();
}
@@ -5043,7 +5064,7 @@ impl Editor {
}
self.insert_with_autoindent_mode(&partial_completion, None, cx);
self.refresh_inline_completion(true, cx);
self.refresh_inline_completion(true, true, cx);
cx.notify();
}
}
@@ -5509,7 +5530,7 @@ impl Editor {
this.edit(edits, None, cx);
})
}
this.refresh_inline_completion(true, cx);
this.refresh_inline_completion(true, false, cx);
linked_editing_ranges::refresh_linked_ranges(this, cx);
});
}
@@ -5528,7 +5549,7 @@ impl Editor {
})
});
this.insert("", cx);
this.refresh_inline_completion(true, cx);
this.refresh_inline_completion(true, false, cx);
});
}
@@ -5615,7 +5636,7 @@ impl Editor {
self.transact(cx, |this, cx| {
this.buffer.update(cx, |b, cx| b.edit(edits, None, cx));
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
this.refresh_inline_completion(true, cx);
this.refresh_inline_completion(true, false, cx);
});
}
@@ -6783,7 +6804,7 @@ impl Editor {
}
self.request_autoscroll(Autoscroll::fit(), cx);
self.unmark_text(cx);
self.refresh_inline_completion(true, cx);
self.refresh_inline_completion(true, false, cx);
cx.emit(EditorEvent::Edited { transaction_id });
cx.emit(EditorEvent::TransactionUndone { transaction_id });
}
@@ -6804,7 +6825,7 @@ impl Editor {
}
self.request_autoscroll(Autoscroll::fit(), cx);
self.unmark_text(cx);
self.refresh_inline_completion(true, cx);
self.refresh_inline_completion(true, false, cx);
cx.emit(EditorEvent::Edited { transaction_id });
}
}
@@ -9175,6 +9196,38 @@ impl Editor {
.detach();
}
pub fn open_file(&mut self, _: &OpenFile, cx: &mut ViewContext<Self>) {
let Some(workspace) = self.workspace() else {
return;
};
let position = self.selections.newest_anchor().head();
let Some((buffer, buffer_position)) =
self.buffer.read(cx).text_anchor_for_position(position, cx)
else {
return;
};
let Some(project) = self.project.clone() else {
return;
};
cx.spawn(|_, mut cx| async move {
let result = find_file(&buffer, project, buffer_position, &mut cx).await;
if let Some((_, path)) = result {
workspace
.update(&mut cx, |workspace, cx| {
workspace.open_resolved_path(path, cx)
})?
.await?;
}
anyhow::Ok(())
})
.detach();
}
pub(crate) fn navigate_to_hover_links(
&mut self,
kind: Option<GotoDefinitionKind>,
@@ -9185,21 +9238,49 @@ impl Editor {
// If there is one definition, just open it directly
if definitions.len() == 1 {
let definition = definitions.pop().unwrap();
enum TargetTaskResult {
Location(Option<Location>),
AlreadyNavigated,
}
let target_task = match definition {
HoverLink::Text(link) => Task::Ready(Some(Ok(Some(link.target)))),
HoverLink::Text(link) => {
Task::ready(anyhow::Ok(TargetTaskResult::Location(Some(link.target))))
}
HoverLink::InlayHint(lsp_location, server_id) => {
self.compute_target_location(lsp_location, server_id, cx)
let computation = self.compute_target_location(lsp_location, server_id, cx);
cx.background_executor().spawn(async move {
let location = computation.await?;
Ok(TargetTaskResult::Location(location))
})
}
HoverLink::Url(url) => {
cx.open_url(&url);
Task::ready(Ok(None))
Task::ready(Ok(TargetTaskResult::AlreadyNavigated))
}
HoverLink::File(path) => {
if let Some(workspace) = self.workspace() {
cx.spawn(|_, mut cx| async move {
workspace
.update(&mut cx, |workspace, cx| {
workspace.open_resolved_path(path, cx)
})?
.await
.map(|_| TargetTaskResult::AlreadyNavigated)
})
} else {
Task::ready(Ok(TargetTaskResult::Location(None)))
}
}
};
cx.spawn(|editor, mut cx| async move {
let target = target_task.await.context("target resolution task")?;
let Some(target) = target else {
return Ok(Navigated::No);
let target = match target_task.await.context("target resolution task")? {
TargetTaskResult::AlreadyNavigated => return Ok(Navigated::Yes),
TargetTaskResult::Location(None) => return Ok(Navigated::No),
TargetTaskResult::Location(Some(target)) => target,
};
editor.update(&mut cx, |editor, cx| {
let Some(workspace) = editor.workspace() else {
return Navigated::No;
@@ -9277,6 +9358,7 @@ impl Editor {
}),
HoverLink::InlayHint(_, _) => None,
HoverLink::Url(_) => None,
HoverLink::File(_) => None,
})
.unwrap_or(tab_kind.to_string());
let location_tasks = definitions
@@ -9287,6 +9369,7 @@ impl Editor {
editor.compute_target_location(lsp_location, server_id, cx)
}
HoverLink::Url(_) => Task::ready(Ok(None)),
HoverLink::File(_) => Task::ready(Ok(None)),
})
.collect::<Vec<_>>();
(title, location_tasks, editor.workspace().clone())
@@ -10409,6 +10492,8 @@ impl Editor {
if settings.show_wrap_guides {
if let SoftWrap::Column(soft_wrap) = self.soft_wrap_mode(cx) {
wrap_guides.push((soft_wrap as usize, true));
} else if let SoftWrap::Bounded(soft_wrap) = self.soft_wrap_mode(cx) {
wrap_guides.push((soft_wrap as usize, true));
}
wrap_guides.extend(settings.wrap_guides.iter().map(|guide| (*guide, false)))
}
@@ -10428,6 +10513,9 @@ impl Editor {
language_settings::SoftWrap::PreferredLineLength => {
SoftWrap::Column(settings.preferred_line_length)
}
language_settings::SoftWrap::Bounded => {
SoftWrap::Bounded(settings.preferred_line_length)
}
}
}
@@ -10469,7 +10557,7 @@ impl Editor {
} else {
let soft_wrap = match self.soft_wrap_mode(cx) {
SoftWrap::None | SoftWrap::PreferLine => language_settings::SoftWrap::EditorWidth,
SoftWrap::EditorWidth | SoftWrap::Column(_) => {
SoftWrap::EditorWidth | SoftWrap::Column(_) | SoftWrap::Bounded(_) => {
language_settings::SoftWrap::PreferLine
}
};
@@ -11418,7 +11506,7 @@ impl Editor {
fn settings_changed(&mut self, cx: &mut ViewContext<Self>) {
self.tasks_update_task = Some(self.refresh_runnables(cx));
self.refresh_inline_completion(true, cx);
self.refresh_inline_completion(true, false, cx);
self.refresh_inlay_hints(
InlayHintRefreshReason::SettingsChange(inlay_hint_settings(
self.selections.newest_anchor().head(),

View File

@@ -165,6 +165,7 @@ impl EditorElement {
});
crate::rust_analyzer_ext::apply_related_actions(view, cx);
crate::clangd_ext::apply_related_actions(view, cx);
register_action(view, cx, Editor::move_left);
register_action(view, cx, Editor::move_right);
register_action(view, cx, Editor::move_down);
@@ -330,6 +331,7 @@ impl EditorElement {
.detach_and_log_err(cx);
});
register_action(view, cx, Editor::open_url);
register_action(view, cx, Editor::open_file);
register_action(view, cx, Editor::fold);
register_action(view, cx, Editor::fold_at);
register_action(view, cx, Editor::unfold_lines);
@@ -4994,7 +4996,8 @@ impl Element for EditorElement {
Some((MAX_LINE_LEN / 2) as f32 * em_advance)
}
SoftWrap::EditorWidth => Some(editor_width),
SoftWrap::Column(column) => {
SoftWrap::Column(column) => Some(column as f32 * em_advance),
SoftWrap::Bounded(column) => {
Some(editor_width.min(column as f32 * em_advance))
}
};

View File

@@ -9,8 +9,8 @@ use language::{Bias, ToOffset};
use linkify::{LinkFinder, LinkKind};
use lsp::LanguageServerId;
use project::{
HoverBlock, HoverBlockKind, InlayHintLabelPartTooltip, InlayHintTooltip, LocationLink,
ResolveState,
HoverBlock, HoverBlockKind, InlayHintLabelPartTooltip, InlayHintTooltip, LocationLink, Project,
ResolveState, ResolvedPath,
};
use std::ops::Range;
use theme::ActiveTheme as _;
@@ -63,6 +63,7 @@ impl RangeInEditor {
#[derive(Debug, Clone)]
pub enum HoverLink {
Url(String),
File(ResolvedPath),
Text(LocationLink),
InlayHint(lsp::Location, LanguageServerId),
}
@@ -522,35 +523,54 @@ pub fn show_link_definition(
})
.ok()
} else if let Some(project) = project {
// query the LSP for definition info
project
.update(&mut cx, |project, cx| match preferred_kind {
LinkDefinitionKind::Symbol => {
project.definition(&buffer, buffer_position, cx)
}
if let Some((filename_range, filename)) =
find_file(&buffer, project.clone(), buffer_position, &mut cx).await
{
let range = maybe!({
let start =
snapshot.anchor_in_excerpt(excerpt_id, filename_range.start)?;
let end =
snapshot.anchor_in_excerpt(excerpt_id, filename_range.end)?;
Some(RangeInEditor::Text(start..end))
});
LinkDefinitionKind::Type => {
project.type_definition(&buffer, buffer_position, cx)
}
})?
.await
.ok()
.map(|definition_result| {
(
definition_result.iter().find_map(|link| {
link.origin.as_ref().and_then(|origin| {
let start = snapshot.anchor_in_excerpt(
excerpt_id,
origin.range.start,
)?;
let end = snapshot
.anchor_in_excerpt(excerpt_id, origin.range.end)?;
Some(RangeInEditor::Text(start..end))
})
}),
definition_result.into_iter().map(HoverLink::Text).collect(),
)
})
Some((range, vec![HoverLink::File(filename)]))
} else {
// query the LSP for definition info
project
.update(&mut cx, |project, cx| match preferred_kind {
LinkDefinitionKind::Symbol => {
project.definition(&buffer, buffer_position, cx)
}
LinkDefinitionKind::Type => {
project.type_definition(&buffer, buffer_position, cx)
}
})?
.await
.ok()
.map(|definition_result| {
(
definition_result.iter().find_map(|link| {
link.origin.as_ref().and_then(|origin| {
let start = snapshot.anchor_in_excerpt(
excerpt_id,
origin.range.start,
)?;
let end = snapshot.anchor_in_excerpt(
excerpt_id,
origin.range.end,
)?;
Some(RangeInEditor::Text(start..end))
})
}),
definition_result
.into_iter()
.map(HoverLink::Text)
.collect(),
)
})
}
} else {
None
}
@@ -686,6 +706,116 @@ pub(crate) fn find_url(
None
}
pub(crate) async fn find_file(
buffer: &Model<language::Buffer>,
project: Model<Project>,
position: text::Anchor,
cx: &mut AsyncWindowContext,
) -> Option<(Range<text::Anchor>, ResolvedPath)> {
let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot()).ok()?;
let (range, candidate_file_path) = surrounding_filename(snapshot, position)?;
let existing_path = project
.update(cx, |project, cx| {
project.resolve_existing_file_path(&candidate_file_path, &buffer, cx)
})
.ok()?
.await?;
Some((range, existing_path))
}
fn surrounding_filename(
snapshot: language::BufferSnapshot,
position: text::Anchor,
) -> Option<(Range<text::Anchor>, String)> {
const LIMIT: usize = 2048;
let offset = position.to_offset(&snapshot);
let mut token_start = offset;
let mut token_end = offset;
let mut found_start = false;
let mut found_end = false;
let mut inside_quotes = false;
let mut filename = String::new();
let mut backwards = snapshot.reversed_chars_at(offset).take(LIMIT).peekable();
while let Some(ch) = backwards.next() {
// Escaped whitespace
if ch.is_whitespace() && backwards.peek() == Some(&'\\') {
filename.push(ch);
token_start -= ch.len_utf8();
backwards.next();
token_start -= '\\'.len_utf8();
continue;
}
if ch.is_whitespace() {
found_start = true;
break;
}
if (ch == '"' || ch == '\'') && !inside_quotes {
found_start = true;
inside_quotes = true;
break;
}
filename.push(ch);
token_start -= ch.len_utf8();
}
if !found_start && token_start != 0 {
return None;
}
filename = filename.chars().rev().collect();
let mut forwards = snapshot
.chars_at(offset)
.take(LIMIT - (offset - token_start))
.peekable();
while let Some(ch) = forwards.next() {
// Skip escaped whitespace
if ch == '\\' && forwards.peek().map_or(false, |ch| ch.is_whitespace()) {
token_end += ch.len_utf8();
let whitespace = forwards.next().unwrap();
token_end += whitespace.len_utf8();
filename.push(whitespace);
continue;
}
if ch.is_whitespace() {
found_end = true;
break;
}
if ch == '"' || ch == '\'' {
// If we're inside quotes, we stop when we come across the next quote
if inside_quotes {
found_end = true;
break;
} else {
// Otherwise, we skip the quote
inside_quotes = true;
continue;
}
}
filename.push(ch);
token_end += ch.len_utf8();
}
if !found_end && (token_end - token_start >= LIMIT) {
return None;
}
if filename.is_empty() {
return None;
}
let range = snapshot.anchor_before(token_start)..snapshot.anchor_after(token_end);
Some((range, filename))
}
#[cfg(test)]
mod tests {
use super::*;
@@ -1268,4 +1398,184 @@ mod tests {
cx.simulate_click(screen_coord, Modifiers::secondary_key());
assert_eq!(cx.opened_url(), Some("https://zed.dev/releases".into()));
}
#[gpui::test]
async fn test_surrounding_filename(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorLspTestContext::new_rust(
lsp::ServerCapabilities {
..Default::default()
},
cx,
)
.await;
let test_cases = [
("file ˇ name", None),
("ˇfile name", Some("file")),
("file ˇname", Some("name")),
("fiˇle name", Some("file")),
("filenˇame", Some("filename")),
// Absolute path
("foobar ˇ/home/user/f.txt", Some("/home/user/f.txt")),
("foobar /home/useˇr/f.txt", Some("/home/user/f.txt")),
// Windows
("C:\\Useˇrs\\user\\f.txt", Some("C:\\Users\\user\\f.txt")),
// Whitespace
("ˇfile\\ -\\ name.txt", Some("file - name.txt")),
("file\\ -\\ naˇme.txt", Some("file - name.txt")),
// Tilde
("ˇ~/file.txt", Some("~/file.txt")),
("~/fiˇle.txt", Some("~/file.txt")),
// Double quotes
("\"fˇile.txt\"", Some("file.txt")),
("ˇ\"file.txt\"", Some("file.txt")),
("ˇ\"fi\\ le.txt\"", Some("fi le.txt")),
// Single quotes
("'fˇile.txt'", Some("file.txt")),
("ˇ'file.txt'", Some("file.txt")),
("ˇ'fi\\ le.txt'", Some("fi le.txt")),
];
for (input, expected) in test_cases {
cx.set_state(input);
let (position, snapshot) = cx.editor(|editor, cx| {
let positions = editor.selections.newest_anchor().head().text_anchor;
let snapshot = editor
.buffer()
.clone()
.read(cx)
.as_singleton()
.unwrap()
.read(cx)
.snapshot();
(positions, snapshot)
});
let result = surrounding_filename(snapshot, position);
if let Some(expected) = expected {
assert!(result.is_some(), "Failed to find file path: {}", input);
let (_, path) = result.unwrap();
assert_eq!(&path, expected, "Incorrect file path for input: {}", input);
} else {
assert!(
result.is_none(),
"Expected no result, but got one: {:?}",
result
);
}
}
}
#[gpui::test]
async fn test_hover_filenames(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorLspTestContext::new_rust(
lsp::ServerCapabilities {
..Default::default()
},
cx,
)
.await;
// Insert a new file
let fs = cx.update_workspace(|workspace, cx| workspace.project().read(cx).fs().clone());
fs.as_fake()
.insert_file("/root/dir/file2.rs", "This is file2.rs".as_bytes().to_vec())
.await;
cx.set_state(indoc! {"
You can't go to a file that does_not_exist.txt.
Go to file2.rs if you want.
Or go to ../dir/file2.rs if you want.
Or go to /root/dir/file2.rs if project is local.ˇ
"});
// File does not exist
let screen_coord = cx.pixel_position(indoc! {"
You can't go to a file that dˇoes_not_exist.txt.
Go to file2.rs if you want.
Or go to ../dir/file2.rs if you want.
Or go to /root/dir/file2.rs if project is local.
"});
cx.simulate_mouse_move(screen_coord, None, Modifiers::secondary_key());
// No highlight
cx.update_editor(|editor, cx| {
assert!(editor
.snapshot(cx)
.text_highlight_ranges::<HoveredLinkState>()
.unwrap_or_default()
.1
.is_empty());
});
// Moving the mouse over a file that does exist should highlight it.
let screen_coord = cx.pixel_position(indoc! {"
You can't go to a file that does_not_exist.txt.
Go to fˇile2.rs if you want.
Or go to ../dir/file2.rs if you want.
Or go to /root/dir/file2.rs if project is local.
"});
cx.simulate_mouse_move(screen_coord, None, Modifiers::secondary_key());
cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
You can't go to a file that does_not_exist.txt.
Go to «file2.rsˇ» if you want.
Or go to ../dir/file2.rs if you want.
Or go to /root/dir/file2.rs if project is local.
"});
// Moving the mouse over a relative path that does exist should highlight it
let screen_coord = cx.pixel_position(indoc! {"
You can't go to a file that does_not_exist.txt.
Go to file2.rs if you want.
Or go to ../dir/fˇile2.rs if you want.
Or go to /root/dir/file2.rs if project is local.
"});
cx.simulate_mouse_move(screen_coord, None, Modifiers::secondary_key());
cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
You can't go to a file that does_not_exist.txt.
Go to file2.rs if you want.
Or go to «../dir/file2.rsˇ» if you want.
Or go to /root/dir/file2.rs if project is local.
"});
// Moving the mouse over an absolute path that does exist should highlight it
let screen_coord = cx.pixel_position(indoc! {"
You can't go to a file that does_not_exist.txt.
Go to file2.rs if you want.
Or go to ../dir/file2.rs if you want.
Or go to /root/diˇr/file2.rs if project is local.
"});
cx.simulate_mouse_move(screen_coord, None, Modifiers::secondary_key());
cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
You can't go to a file that does_not_exist.txt.
Go to file2.rs if you want.
Or go to ../dir/file2.rs if you want.
Or go to «/root/dir/file2.rsˇ» if project is local.
"});
cx.simulate_click(screen_coord, Modifiers::secondary_key());
cx.update_workspace(|workspace, cx| assert_eq!(workspace.items(cx).count(), 2));
cx.update_workspace(|workspace, cx| {
let active_editor = workspace.active_item_as::<Editor>(cx).unwrap();
let buffer = active_editor
.read(cx)
.buffer()
.read(cx)
.as_singleton()
.unwrap();
let file = buffer.read(cx).file().unwrap();
let file_path = file.as_local().unwrap().abs_path(cx);
assert_eq!(file_path.to_str().unwrap(), "/root/dir/file2.rs");
});
}
}

View File

@@ -0,0 +1,54 @@
use std::sync::Arc;
use crate::Editor;
use gpui::{Model, WindowContext};
use language::Buffer;
use language::Language;
use lsp::LanguageServerId;
use multi_buffer::Anchor;
pub(crate) fn find_specific_language_server_in_selection<F>(
editor: &Editor,
cx: &WindowContext,
filter_language: F,
language_server_name: &str,
) -> Option<(Anchor, Arc<Language>, LanguageServerId, Model<Buffer>)>
where
F: Fn(&Language) -> bool,
{
let Some(project) = &editor.project else {
return None;
};
let multibuffer = editor.buffer().read(cx);
editor
.selections
.disjoint_anchors()
.into_iter()
.filter(|selection| selection.start == selection.end)
.filter_map(|selection| Some((selection.start.buffer_id?, selection.start)))
.filter_map(|(buffer_id, trigger_anchor)| {
let buffer = multibuffer.buffer(buffer_id)?;
let language = buffer.read(cx).language_at(trigger_anchor.text_anchor)?;
if !filter_language(&language) {
return None;
}
Some((trigger_anchor, language, buffer))
})
.find_map(|(trigger_anchor, language, buffer)| {
project
.read(cx)
.language_servers_for_buffer(buffer.read(cx), cx)
.find_map(|(adapter, server)| {
if adapter.name.0.as_ref() == language_server_name {
Some((
trigger_anchor,
Arc::clone(&language),
server.server_id(),
buffer.clone(),
))
} else {
None
}
})
})
}

View File

@@ -1,5 +1,3 @@
use std::sync::Arc;
use anyhow::Context as _;
use gpui::{Context, View, ViewContext, VisualContext, WindowContext};
use language::Language;
@@ -7,22 +5,24 @@ use multi_buffer::MultiBuffer;
use project::lsp_ext_command::ExpandMacro;
use text::ToPointUtf16;
use crate::{element::register_action, Editor, ExpandMacroRecursively};
use crate::{
element::register_action, lsp_ext::find_specific_language_server_in_selection, Editor,
ExpandMacroRecursively,
};
static RUST_ANALYZER_NAME: &str = "rust-analyzer";
fn is_rust_language(language: &Language) -> bool {
language.name().as_ref() == "Rust"
}
pub fn apply_related_actions(editor: &View<Editor>, cx: &mut WindowContext) {
let is_rust_related = editor.update(cx, |editor, cx| {
editor
.buffer()
.read(cx)
.all_buffers()
.iter()
.any(|b| match b.read(cx).language() {
Some(l) => is_rust_language(l),
None => false,
})
});
if is_rust_related {
if editor
.update(cx, |e, cx| {
find_specific_language_server_in_selection(e, cx, &is_rust_language, RUST_ANALYZER_NAME)
})
.is_some()
{
register_action(editor, cx, expand_macro_recursively);
}
}
@@ -42,39 +42,13 @@ pub fn expand_macro_recursively(
return;
};
let multibuffer = editor.buffer().read(cx);
let Some((trigger_anchor, rust_language, server_to_query, buffer)) = editor
.selections
.disjoint_anchors()
.into_iter()
.filter(|selection| selection.start == selection.end)
.filter_map(|selection| Some((selection.start.buffer_id?, selection.start)))
.filter_map(|(buffer_id, trigger_anchor)| {
let buffer = multibuffer.buffer(buffer_id)?;
let rust_language = buffer.read(cx).language_at(trigger_anchor.text_anchor)?;
if !is_rust_language(&rust_language) {
return None;
}
Some((trigger_anchor, rust_language, buffer))
})
.find_map(|(trigger_anchor, rust_language, buffer)| {
project
.read(cx)
.language_servers_for_buffer(buffer.read(cx), cx)
.find_map(|(adapter, server)| {
if adapter.name.0.as_ref() == "rust-analyzer" {
Some((
trigger_anchor,
Arc::clone(&rust_language),
server.server_id(),
buffer.clone(),
))
} else {
None
}
})
})
let Some((trigger_anchor, rust_language, server_to_query, buffer)) =
find_specific_language_server_in_selection(
&editor,
cx,
&is_rust_language,
RUST_ANALYZER_NAME,
)
else {
return;
};
@@ -120,7 +94,3 @@ pub fn expand_macro_recursively(
})
.detach_and_log_err(cx);
}
fn is_rust_language(language: &Language) -> bool {
language.name().as_ref() == "Rust"
}

View File

@@ -351,6 +351,16 @@ impl Fs for RealFs {
// invalid cross-device link error, and XDG_CACHE_DIR for fallback.
// See https://github.com/zed-industries/zed/pull/8437 for more details.
NamedTempFile::new_in(path.parent().unwrap_or(&paths::temp_dir()))
} else if cfg!(target_os = "windows") {
// If temp dir is set to a different drive than the destination,
// we receive error:
//
// failed to persist temporary file:
// The system cannot move the file to a different disk drive. (os error 17)
//
// So we use the directory of the destination as a temp dir to avoid it.
// https://github.com/zed-industries/zed/issues/16571
NamedTempFile::new_in(path.parent().unwrap_or(&paths::temp_dir()))
} else {
NamedTempFile::new()
}?;

View File

@@ -7,7 +7,7 @@
//!
//! # Element Basics
//!
//! Elements are constructed by calling [`Render::render()`] on the root view of the window, which
//! Elements are constructed by calling [`Render::render()`] on the root view of the window,
//! which recursively constructs the element tree from the current state of the application,.
//! These elements are then laid out by Taffy, and painted to the screen according to their own
//! implementation of [`Element::paint()`]. Before the start of the next frame, the entire element

View File

@@ -301,7 +301,7 @@ impl DirectWriteState {
continue;
};
if fonts.GetFontCount() == 0 {
log::error!("No mathcing font find for {}", family_name);
log::error!("No matching font found for {}", family_name);
continue;
}
let font = fonts.GetFontFaceReference(0)?.CreateFontFace()?;

View File

@@ -758,7 +758,7 @@ fn handle_dpi_changed_msg(
let new_dpi = wparam.loword() as f32;
let mut lock = state_ptr.state.borrow_mut();
lock.scale_factor = new_dpi / USER_DEFAULT_SCREEN_DPI as f32;
lock.border_offset.udpate(handle).log_err();
lock.border_offset.update(handle).log_err();
drop(lock);
let rect = unsafe { &*(lparam.0 as *const RECT) };
@@ -796,7 +796,7 @@ fn handle_dpi_changed_msg(
fn handle_display_change_msg(handle: HWND, state_ptr: Rc<WindowsWindowStatePtr>) -> Option<isize> {
// NOTE:
// Even the `lParam` holds the resolution of the screen, we just ignore it.
// Because WM_DPICHANGED, WM_MOVE, WM_SIEZ will come first, window reposition and resize
// Because WM_DPICHANGED, WM_MOVE, WM_SIZE will come first, window reposition and resize
// are handled there.
// So we only care about if monitor is disconnected.
let previous_monitor = state_ptr.as_ref().state.borrow().display;
@@ -1087,7 +1087,7 @@ fn handle_system_settings_changed(
// mouse double click
lock.click_state.system_update();
// window border offset
lock.border_offset.udpate(handle).log_err();
lock.border_offset.update(handle).log_err();
Some(0)
}

View File

@@ -166,7 +166,7 @@ impl WindowsWindowState {
/// get the logical size of the app's drawable area.
///
/// Currently, GPUI uses logical size of the app to handle mouse interactions (such as
/// Currently, GPUI uses the logical size of the app to handle mouse interactions (such as
/// whether the mouse collides with other elements of GPUI).
fn content_size(&self) -> Size<Pixels> {
self.logical_size
@@ -187,13 +187,13 @@ impl WindowsWindowState {
}
fn title_bar_height(&self) -> Pixels {
// todo(windows) this is hard set to match the ui title bar
// todo(windows) this is hardcoded to match the ui title bar
// in the future the ui title bar component will report the size
px(32.) + self.title_bar_top_offset()
}
pub(crate) fn caption_button_width(&self) -> Pixels {
// todo(windows) this is hard set to match the ui title bar
// todo(windows) this is hardcoded to match the ui title bar
// in the future the ui title bar component will report the size
px(36.)
}
@@ -343,8 +343,8 @@ impl WindowsWindow {
};
let mut lock = state_ptr.state.borrow_mut();
let bounds = bounds.to_device_pixels(lock.scale_factor);
lock.border_offset.udpate(raw_hwnd)?;
placement.rcNormalPosition = calcualte_window_rect(bounds, lock.border_offset);
lock.border_offset.update(raw_hwnd)?;
placement.rcNormalPosition = calculate_window_rect(bounds, lock.border_offset);
drop(lock);
SetWindowPlacement(raw_hwnd, &placement)?;
}
@@ -404,7 +404,7 @@ impl PlatformWindow for WindowsWindow {
/// get the logical size of the app's drawable area.
///
/// Currently, GPUI uses logical size of the app to handle mouse interactions (such as
/// Currently, GPUI uses the logical size of the app to handle mouse interactions (such as
/// whether the mouse collides with other elements of GPUI).
fn content_size(&self) -> Size<Pixels> {
self.0.state.borrow().content_size()
@@ -897,7 +897,7 @@ pub(crate) struct WindowBorderOffset {
}
impl WindowBorderOffset {
pub(crate) fn udpate(&mut self, hwnd: HWND) -> anyhow::Result<()> {
pub(crate) fn update(&mut self, hwnd: HWND) -> anyhow::Result<()> {
let window_rect = unsafe {
let mut rect = std::mem::zeroed();
GetWindowRect(hwnd, &mut rect)?;
@@ -1015,9 +1015,9 @@ fn register_drag_drop(state_ptr: Rc<WindowsWindowStatePtr>) -> Result<()> {
Ok(())
}
fn calcualte_window_rect(bounds: Bounds<DevicePixels>, border_offset: WindowBorderOffset) -> RECT {
fn calculate_window_rect(bounds: Bounds<DevicePixels>, border_offset: WindowBorderOffset) -> RECT {
// NOTE:
// The reason that not using `AdjustWindowRectEx()` here is
// The reason we're not using `AdjustWindowRectEx()` here is
// that the size reported by this function is incorrect.
// You can test it, and there are similar discussions online.
// See: https://stackoverflow.com/questions/12423584/how-to-set-exact-client-size-for-overlapped-window-winapi
@@ -1032,11 +1032,11 @@ fn calcualte_window_rect(bounds: Bounds<DevicePixels>, border_offset: WindowBord
let left_offset = border_offset.width_offset / 2;
let top_offset = border_offset.height_offset / 2;
let right_offset = border_offset.width_offset - left_offset;
let bottom_offet = border_offset.height_offset - top_offset;
let bottom_offset = border_offset.height_offset - top_offset;
rect.left -= left_offset;
rect.top -= top_offset;
rect.right += right_offset;
rect.bottom += bottom_offet;
rect.bottom += bottom_offset;
rect
}
@@ -1048,11 +1048,11 @@ fn calculate_client_rect(
let left_offset = border_offset.width_offset / 2;
let top_offset = border_offset.height_offset / 2;
let right_offset = border_offset.width_offset - left_offset;
let bottom_offet = border_offset.height_offset - top_offset;
let bottom_offset = border_offset.height_offset - top_offset;
let left = rect.left + left_offset;
let top = rect.top + top_offset;
let right = rect.right - right_offset;
let bottom = rect.bottom - bottom_offet;
let bottom = rect.bottom - bottom_offset;
let physical_size = size(DevicePixels(right - left), DevicePixels(bottom - top));
Bounds {
origin: logical_point(left as f32, top as f32, scale_factor),

View File

@@ -309,7 +309,7 @@ mod tests {
#[cfg(target_os = "macos")]
use crate as gpui;
// These seem to vary wildly based on the the text system.
// These seem to vary wildly based on the text system.
#[cfg(target_os = "macos")]
#[crate::test]
fn test_wrap_shaped_line(cx: &mut TestAppContext) {

View File

@@ -383,7 +383,7 @@ pub trait File: Send + Sync {
/// The file associated with a buffer, in the case where the file is on the local disk.
pub trait LocalFile: File {
/// Returns the absolute path of this file.
/// Returns the absolute path of this file
fn abs_path(&self, cx: &AppContext) -> PathBuf;
/// Loads the file's contents from disk.

View File

@@ -158,6 +158,24 @@ pub struct CachedLspAdapter {
cached_binary: futures::lock::Mutex<Option<LanguageServerBinary>>,
}
impl Debug for CachedLspAdapter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CachedLspAdapter")
.field("name", &self.name)
.field(
"disk_based_diagnostic_sources",
&self.disk_based_diagnostic_sources,
)
.field(
"disk_based_diagnostics_progress_token",
&self.disk_based_diagnostics_progress_token,
)
.field("language_ids", &self.language_ids)
.field("reinstall_attempt_count", &self.reinstall_attempt_count)
.finish_non_exhaustive()
}
}
impl CachedLspAdapter {
pub fn new(adapter: Arc<dyn LspAdapter>) -> Arc<Self> {
let name = adapter.name();

View File

@@ -379,10 +379,12 @@ pub enum SoftWrap {
None,
/// Prefer a single line generally, unless an overly long line is encountered.
PreferLine,
/// Soft wrap lines that overflow the editor
/// Soft wrap lines that exceed the editor width
EditorWidth,
/// Soft wrap lines at the preferred line length
PreferredLineLength,
/// Soft wrap line at the preferred line length or the editor width (whichever is smaller)
Bounded,
}
/// Controls the behavior of formatting files when they are saved.

View File

@@ -445,6 +445,21 @@ impl LanguageModel for CloudLanguageModel {
self.model.max_token_count()
}
fn cache_configuration(&self) -> Option<LanguageModelCacheConfiguration> {
match &self.model {
CloudModel::Anthropic(model) => {
model
.cache_configuration()
.map(|cache| LanguageModelCacheConfiguration {
max_cache_anchors: cache.max_cache_anchors,
should_speculate: cache.should_speculate,
min_total_token: cache.min_total_token,
})
}
CloudModel::OpenAi(_) | CloudModel::Google(_) | CloudModel::Zed(_) => None,
}
}
fn count_tokens(
&self,
request: LanguageModelRequest,

View File

@@ -8,7 +8,9 @@ use gpui::{
ViewContext, VisualContext, WeakModel, WindowContext,
};
use language::{LanguageServerId, LanguageServerName};
use lsp::{notification::SetTrace, IoKind, LanguageServer, SetTraceParams, TraceValue};
use lsp::{
notification::SetTrace, IoKind, LanguageServer, MessageType, SetTraceParams, TraceValue,
};
use project::{search::SearchQuery, Project};
use std::{borrow::Cow, sync::Arc};
use ui::{prelude::*, Button, Checkbox, ContextMenu, Label, PopoverMenu, Selection};
@@ -34,15 +36,76 @@ struct ProjectState {
_subscriptions: [gpui::Subscription; 2],
}
trait Message: AsRef<str> {
type Level: Copy + std::fmt::Debug;
fn should_include(&self, _: Self::Level) -> bool {
true
}
}
struct LogMessage {
message: String,
typ: MessageType,
}
impl AsRef<str> for LogMessage {
fn as_ref(&self) -> &str {
&self.message
}
}
impl Message for LogMessage {
type Level = MessageType;
fn should_include(&self, level: Self::Level) -> bool {
match (self.typ, level) {
(MessageType::ERROR, _) => true,
(_, MessageType::ERROR) => false,
(MessageType::WARNING, _) => true,
(_, MessageType::WARNING) => false,
(MessageType::INFO, _) => true,
(_, MessageType::INFO) => false,
_ => true,
}
}
}
struct TraceMessage {
message: String,
}
impl AsRef<str> for TraceMessage {
fn as_ref(&self) -> &str {
&self.message
}
}
impl Message for TraceMessage {
type Level = ();
}
struct RpcMessage {
message: String,
}
impl AsRef<str> for RpcMessage {
fn as_ref(&self) -> &str {
&self.message
}
}
impl Message for RpcMessage {
type Level = ();
}
struct LanguageServerState {
kind: LanguageServerKind,
log_messages: VecDeque<String>,
trace_messages: VecDeque<String>,
log_messages: VecDeque<LogMessage>,
trace_messages: VecDeque<TraceMessage>,
rpc_state: Option<LanguageServerRpcState>,
trace_level: TraceValue,
_io_logs_subscription: Option<lsp::Subscription>,
_lsp_logs_subscription: Option<lsp::Subscription>,
_lsp_trace_subscription: Option<lsp::Subscription>,
log_level: MessageType,
io_logs_subscription: Option<lsp::Subscription>,
}
enum LanguageServerKind {
@@ -60,7 +123,7 @@ impl LanguageServerKind {
}
struct LanguageServerRpcState {
rpc_messages: VecDeque<String>,
rpc_messages: VecDeque<RpcMessage>,
last_message_kind: Option<MessageKind>,
}
@@ -164,6 +227,7 @@ impl LogStore {
.update(&mut cx, |this, cx| {
this.add_language_server_log(
server_id,
MessageType::LOG,
&params.message,
cx,
);
@@ -175,7 +239,8 @@ impl LogStore {
LanguageServerKind::Global {
name: LanguageServerName(Arc::from("copilot")),
},
server.clone(),
server.server_id(),
Some(server.clone()),
cx,
);
}
@@ -226,7 +291,8 @@ impl LogStore {
LanguageServerKind::Local {
project: project.downgrade(),
},
server,
server.server_id(),
Some(server),
cx,
);
}
@@ -234,8 +300,23 @@ impl LogStore {
project::Event::LanguageServerRemoved(id) => {
this.remove_language_server(*id, cx);
}
project::Event::LanguageServerLog(id, message) => {
this.add_language_server_log(*id, message, cx);
project::Event::LanguageServerLog(id, typ, message) => {
this.add_language_server(
LanguageServerKind::Local {
project: project.downgrade(),
},
*id,
None,
cx,
);
match typ {
project::LanguageServerLogType::Log(typ) => {
this.add_language_server_log(*id, *typ, message, cx);
}
project::LanguageServerLogType::Trace(_) => {
this.add_language_server_trace(*id, message, cx);
}
}
}
_ => {}
}),
@@ -254,77 +335,56 @@ impl LogStore {
fn add_language_server(
&mut self,
kind: LanguageServerKind,
server: Arc<LanguageServer>,
server_id: LanguageServerId,
server: Option<Arc<LanguageServer>>,
cx: &mut ModelContext<Self>,
) -> Option<&mut LanguageServerState> {
let server_state = self
.language_servers
.entry(server.server_id())
.or_insert_with(|| {
cx.notify();
LanguageServerState {
kind,
rpc_state: None,
log_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES),
trace_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES),
trace_level: TraceValue::Off,
_io_logs_subscription: None,
_lsp_logs_subscription: None,
_lsp_trace_subscription: None,
}
});
let server_state = self.language_servers.entry(server_id).or_insert_with(|| {
cx.notify();
LanguageServerState {
kind,
rpc_state: None,
log_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES),
trace_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES),
trace_level: TraceValue::Off,
log_level: MessageType::LOG,
io_logs_subscription: None,
}
});
if server.has_notification_handler::<lsp::notification::LogMessage>()
|| server.has_notification_handler::<lsp::notification::LogTrace>()
{
// Another event wants to re-add the server that was already added and subscribed to, avoid doing it again.
return Some(server_state);
if let Some(server) = server.filter(|_| server_state.io_logs_subscription.is_none()) {
let io_tx = self.io_tx.clone();
let server_id = server.server_id();
server_state.io_logs_subscription = Some(server.on_io(move |io_kind, message| {
io_tx
.unbounded_send((server_id, io_kind, message.to_string()))
.ok();
}));
}
let io_tx = self.io_tx.clone();
let server_id = server.server_id();
server_state._io_logs_subscription = Some(server.on_io(move |io_kind, message| {
io_tx
.unbounded_send((server_id, io_kind, message.to_string()))
.ok();
}));
let this = cx.handle().downgrade();
server_state._lsp_logs_subscription =
Some(server.on_notification::<lsp::notification::LogMessage, _>({
let this = this.clone();
move |params, mut cx| {
if let Some(this) = this.upgrade() {
this.update(&mut cx, |this, cx| {
this.add_language_server_log(server_id, &params.message, cx);
})
.ok();
}
}
}));
server_state._lsp_trace_subscription =
Some(server.on_notification::<lsp::notification::LogTrace, _>({
move |params, mut cx| {
if let Some(this) = this.upgrade() {
this.update(&mut cx, |this, cx| {
this.add_language_server_trace(server_id, &params.message, cx);
})
.ok();
}
}
}));
Some(server_state)
}
fn add_language_server_log(
&mut self,
id: LanguageServerId,
typ: MessageType,
message: &str,
cx: &mut ModelContext<Self>,
) -> Option<()> {
let language_server_state = self.get_language_server_state(id)?;
let log_lines = &mut language_server_state.log_messages;
Self::add_language_server_message(log_lines, id, message, LogKind::Logs, cx);
Self::add_language_server_message(
log_lines,
id,
LogMessage {
message: message.trim_end().to_string(),
typ,
},
language_server_state.log_level,
LogKind::Logs,
cx,
);
Some(())
}
@@ -337,28 +397,39 @@ impl LogStore {
let language_server_state = self.get_language_server_state(id)?;
let log_lines = &mut language_server_state.trace_messages;
Self::add_language_server_message(log_lines, id, message, LogKind::Trace, cx);
Self::add_language_server_message(
log_lines,
id,
TraceMessage {
message: message.trim_end().to_string(),
},
(),
LogKind::Trace,
cx,
);
Some(())
}
fn add_language_server_message(
log_lines: &mut VecDeque<String>,
fn add_language_server_message<T: Message>(
log_lines: &mut VecDeque<T>,
id: LanguageServerId,
message: &str,
message: T,
current_severity: <T as Message>::Level,
kind: LogKind,
cx: &mut ModelContext<Self>,
) {
while log_lines.len() >= MAX_STORED_LOG_ENTRIES {
log_lines.pop_front();
}
let message = message.trim();
log_lines.push_back(message.to_string());
cx.emit(Event::NewServerLogEntry {
id,
entry: message.to_string(),
kind,
});
cx.notify();
let entry: &str = message.as_ref();
let entry = entry.to_string();
let visible = message.should_include(current_severity);
log_lines.push_back(message);
if visible {
cx.emit(Event::NewServerLogEntry { id, entry, kind });
cx.notify();
}
}
fn remove_language_server(&mut self, id: LanguageServerId, cx: &mut ModelContext<Self>) {
@@ -366,12 +437,14 @@ impl LogStore {
cx.notify();
}
fn server_logs(&self, server_id: LanguageServerId) -> Option<&VecDeque<String>> {
fn server_logs(&self, server_id: LanguageServerId) -> Option<&VecDeque<LogMessage>> {
Some(&self.language_servers.get(&server_id)?.log_messages)
}
fn server_trace(&self, server_id: LanguageServerId) -> Option<&VecDeque<String>> {
fn server_trace(&self, server_id: LanguageServerId) -> Option<&VecDeque<TraceMessage>> {
Some(&self.language_servers.get(&server_id)?.trace_messages)
}
fn server_ids_for_project<'a>(
&'a self,
lookup_project: &'a WeakModel<Project>,
@@ -425,7 +498,7 @@ impl LogStore {
IoKind::StdIn => false,
IoKind::StdErr => {
let message = format!("stderr: {}", message.trim());
self.add_language_server_log(language_server_id, &message, cx);
self.add_language_server_log(language_server_id, MessageType::LOG, &message, cx);
return Some(());
}
};
@@ -446,7 +519,9 @@ impl LogStore {
MessageKind::Send => SEND_LINE,
MessageKind::Receive => RECEIVE_LINE,
};
rpc_log_lines.push_back(line_before_message.to_string());
rpc_log_lines.push_back(RpcMessage {
message: line_before_message.to_string(),
});
cx.emit(Event::NewServerLogEntry {
id: language_server_id,
entry: line_before_message.to_string(),
@@ -458,7 +533,9 @@ impl LogStore {
rpc_log_lines.pop_front();
}
let message = message.trim();
rpc_log_lines.push_back(message.to_string());
rpc_log_lines.push_back(RpcMessage {
message: message.to_string(),
});
cx.emit(Event::NewServerLogEntry {
id: language_server_id,
entry: message.to_string(),
@@ -646,11 +723,17 @@ impl LspLogView {
}
fn show_logs_for_server(&mut self, server_id: LanguageServerId, cx: &mut ViewContext<Self>) {
let typ = self
.log_store
.read_with(cx, |v, _| {
v.language_servers.get(&server_id).map(|v| v.log_level)
})
.unwrap_or(MessageType::LOG);
let log_contents = self
.log_store
.read(cx)
.server_logs(server_id)
.map(log_contents);
.map(|v| log_contents(v, typ));
if let Some(log_contents) = log_contents {
self.current_server_id = Some(server_id);
self.active_entry_kind = LogKind::Logs;
@@ -661,12 +744,38 @@ impl LspLogView {
}
cx.focus(&self.focus_handle);
}
fn update_log_level(
&self,
server_id: LanguageServerId,
level: MessageType,
cx: &mut ViewContext<Self>,
) {
let log_contents = self.log_store.update(cx, |this, _| {
if let Some(state) = this.get_language_server_state(server_id) {
state.log_level = level;
}
this.server_logs(server_id).map(|v| log_contents(v, level))
});
if let Some(log_contents) = log_contents {
self.editor.update(cx, move |editor, cx| {
editor.set_text(log_contents, cx);
editor.move_to_end(&MoveToEnd, cx);
});
cx.notify();
}
cx.focus(&self.focus_handle);
}
fn show_trace_for_server(&mut self, server_id: LanguageServerId, cx: &mut ViewContext<Self>) {
let log_contents = self
.log_store
.read(cx)
.server_trace(server_id)
.map(log_contents);
.map(|v| log_contents(v, ()));
if let Some(log_contents) = log_contents {
self.current_server_id = Some(server_id);
self.active_entry_kind = LogKind::Trace;
@@ -686,7 +795,7 @@ impl LspLogView {
let rpc_log = self.log_store.update(cx, |log_store, _| {
log_store
.enable_rpc_trace_for_language_server(server_id)
.map(|state| log_contents(&state.rpc_messages))
.map(|state| log_contents(&state.rpc_messages, ()))
});
if let Some(rpc_log) = rpc_log {
self.current_server_id = Some(server_id);
@@ -758,16 +867,25 @@ impl LspLogView {
}
}
fn log_contents(lines: &VecDeque<String>) -> String {
let (a, b) = lines.as_slices();
let log_contents = a.join("\n");
if b.is_empty() {
log_contents
fn log_filter<T: Message>(line: &T, cmp: <T as Message>::Level) -> Option<&str> {
if line.should_include(cmp) {
Some(line.as_ref())
} else {
log_contents + "\n" + &b.join("\n")
None
}
}
fn log_contents<T: Message>(lines: &VecDeque<T>, cmp: <T as Message>::Level) -> String {
let (a, b) = lines.as_slices();
let a = a.into_iter().filter_map(move |v| log_filter(v, cmp));
let b = b.into_iter().filter_map(move |v| log_filter(v, cmp));
a.chain(b).fold(String::new(), |mut acc, el| {
acc.push_str(el);
acc.push('\n');
acc
})
}
impl Render for LspLogView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
self.editor
@@ -1044,8 +1162,8 @@ impl Render for LspLogToolbarItemView {
)
.ml_2(),
)
.child(log_view.update(cx, |this, _| {
if this.active_entry_kind == LogKind::Trace {
.child(log_view.update(cx, |this, _| match this.active_entry_kind {
LogKind::Trace => {
let log_view = log_view.clone();
div().child(
PopoverMenu::new("lsp-trace-level-menu")
@@ -1095,9 +1213,60 @@ impl Render for LspLogToolbarItemView {
}
}),
)
} else {
div()
}
LogKind::Logs => {
let log_view = log_view.clone();
div().child(
PopoverMenu::new("lsp-log-level-menu")
.anchor(AnchorCorner::TopLeft)
.trigger(Button::new(
"language_server_log_level_selector",
"Log level",
))
.menu({
let log_view = log_view.clone();
move |cx| {
let id = log_view.read(cx).current_server_id?;
let log_level = log_view.update(cx, |this, cx| {
this.log_store.update(cx, |this, _| {
Some(this.get_language_server_state(id)?.log_level)
})
})?;
ContextMenu::build(cx, |mut menu, _| {
let log_view = log_view.clone();
for (option, label) in [
(MessageType::LOG, "Log"),
(MessageType::INFO, "Info"),
(MessageType::WARNING, "Warning"),
(MessageType::ERROR, "Error"),
] {
menu = menu.entry(label, None, {
let log_view = log_view.clone();
move |cx| {
log_view.update(cx, |this, cx| {
if let Some(id) = this.current_server_id {
this.update_log_level(id, option, cx);
}
});
}
});
if option == log_level {
menu.select_last();
}
}
menu
})
.into()
}
}),
)
}
_ => div(),
}))
}
}

View File

@@ -135,3 +135,97 @@ impl LspCommand for ExpandMacro {
BufferId::new(message.buffer_id)
}
}
pub enum LspSwitchSourceHeader {}
impl lsp::request::Request for LspSwitchSourceHeader {
type Params = SwitchSourceHeaderParams;
type Result = Option<SwitchSourceHeaderResult>;
const METHOD: &'static str = "textDocument/switchSourceHeader";
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct SwitchSourceHeaderParams(lsp::TextDocumentIdentifier);
#[derive(Serialize, Deserialize, Debug, Default)]
#[serde(rename_all = "camelCase")]
pub struct SwitchSourceHeaderResult(pub String);
#[derive(Default, Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct SwitchSourceHeader;
#[async_trait(?Send)]
impl LspCommand for SwitchSourceHeader {
type Response = SwitchSourceHeaderResult;
type LspRequest = LspSwitchSourceHeader;
type ProtoRequest = proto::LspExtSwitchSourceHeader;
fn to_lsp(
&self,
path: &Path,
_: &Buffer,
_: &Arc<LanguageServer>,
_: &AppContext,
) -> SwitchSourceHeaderParams {
SwitchSourceHeaderParams(lsp::TextDocumentIdentifier {
uri: lsp::Url::from_file_path(path).unwrap(),
})
}
async fn response_from_lsp(
self,
message: Option<SwitchSourceHeaderResult>,
_: Model<Project>,
_: Model<Buffer>,
_: LanguageServerId,
_: AsyncAppContext,
) -> anyhow::Result<SwitchSourceHeaderResult> {
Ok(message
.map(|message| SwitchSourceHeaderResult(message.0))
.unwrap_or_default())
}
fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::LspExtSwitchSourceHeader {
proto::LspExtSwitchSourceHeader {
project_id,
buffer_id: buffer.remote_id().into(),
}
}
async fn from_proto(
_: Self::ProtoRequest,
_: Model<Project>,
_: Model<Buffer>,
_: AsyncAppContext,
) -> anyhow::Result<Self> {
Ok(Self {})
}
fn response_to_proto(
response: SwitchSourceHeaderResult,
_: &mut Project,
_: PeerId,
_: &clock::Global,
_: &mut AppContext,
) -> proto::LspExtSwitchSourceHeaderResponse {
proto::LspExtSwitchSourceHeaderResponse {
target_file: response.0,
}
}
async fn response_from_proto(
self,
message: proto::LspExtSwitchSourceHeaderResponse,
_: Model<Project>,
_: Model<Buffer>,
_: AsyncAppContext,
) -> anyhow::Result<SwitchSourceHeaderResult> {
Ok(SwitchSourceHeaderResult(message.target_file))
}
fn buffer_id_from_proto(message: &proto::LspExtSwitchSourceHeader) -> Result<BufferId> {
BufferId::new(message.buffer_id)
}
}

View File

@@ -62,8 +62,8 @@ use log::error;
use lsp::{
CompletionContext, DiagnosticSeverity, DiagnosticTag, DidChangeWatchedFilesRegistrationOptions,
DocumentHighlightKind, Edit, FileSystemWatcher, InsertTextFormat, LanguageServer,
LanguageServerBinary, LanguageServerId, LspRequestFuture, MessageActionItem, OneOf,
ServerHealthStatus, ServerStatus, TextEdit, WorkDoneProgressCancelParams,
LanguageServerBinary, LanguageServerId, LspRequestFuture, MessageActionItem, MessageType,
OneOf, ServerHealthStatus, ServerStatus, TextEdit, WorkDoneProgressCancelParams,
};
use lsp_command::*;
use node_runtime::NodeRuntime;
@@ -308,11 +308,17 @@ impl PartialEq for LanguageServerPromptRequest {
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum LanguageServerLogType {
Log(MessageType),
Trace(Option<String>),
}
#[derive(Clone, Debug, PartialEq)]
pub enum Event {
LanguageServerAdded(LanguageServerId),
LanguageServerRemoved(LanguageServerId),
LanguageServerLog(LanguageServerId, String),
LanguageServerLog(LanguageServerId, LanguageServerLogType, String),
Notification(String),
LanguageServerPrompt(LanguageServerPromptRequest),
LanguageNotFound(Model<Buffer>),
@@ -649,16 +655,17 @@ impl DirectoryLister {
};
"~/".to_string()
}
pub fn list_directory(&self, query: String, cx: &mut AppContext) -> Task<Result<Vec<PathBuf>>> {
pub fn list_directory(&self, path: String, cx: &mut AppContext) -> Task<Result<Vec<PathBuf>>> {
match self {
DirectoryLister::Project(project) => {
project.update(cx, |project, cx| project.list_directory(query, cx))
project.update(cx, |project, cx| project.list_directory(path, cx))
}
DirectoryLister::Local(fs) => {
let fs = fs.clone();
cx.background_executor().spawn(async move {
let mut results = vec![];
let expanded = shellexpand::tilde(&query);
let expanded = shellexpand::tilde(&path);
let query = Path::new(expanded.as_ref());
let mut response = fs.read_dir(query).await?;
while let Some(path) = response.next().await {
@@ -2300,6 +2307,7 @@ impl Project {
buffer.update(cx, |buffer, cx| {
let worktree_id = old_file.worktree_id(cx);
let ids = &self.language_server_ids;
if let Some(language) = buffer.language().cloned() {
@@ -2620,7 +2628,7 @@ impl Project {
let worktree_id = file.worktree_id(cx);
let abs_path = file.as_local()?.abs_path(cx);
let text_document = lsp::TextDocumentIdentifier {
uri: lsp::Url::from_file_path(abs_path).unwrap(),
uri: lsp::Url::from_file_path(abs_path).log_err()?,
};
for (_, _, server) in self.language_servers_for_worktree(worktree_id) {
@@ -3651,17 +3659,56 @@ impl Project {
})
.detach();
language_server
.on_notification::<lsp::notification::Progress, _>(move |params, mut cx| {
if let Some(this) = project.upgrade() {
this.update(&mut cx, |this, cx| {
this.on_lsp_progress(
params,
server_id,
disk_based_diagnostics_progress_token.clone(),
cx,
);
})
.ok();
.on_notification::<lsp::notification::Progress, _>({
let project = project.clone();
move |params, mut cx| {
if let Some(this) = project.upgrade() {
this.update(&mut cx, |this, cx| {
this.on_lsp_progress(
params,
server_id,
disk_based_diagnostics_progress_token.clone(),
cx,
);
})
.ok();
}
}
})
.detach();
language_server
.on_notification::<lsp::notification::LogMessage, _>({
let project = project.clone();
move |params, mut cx| {
if let Some(this) = project.upgrade() {
this.update(&mut cx, |_, cx| {
cx.emit(Event::LanguageServerLog(
server_id,
LanguageServerLogType::Log(params.typ),
params.message,
));
})
.ok();
}
}
})
.detach();
language_server
.on_notification::<lsp::notification::LogTrace, _>({
let project = project.clone();
move |params, mut cx| {
if let Some(this) = project.upgrade() {
this.update(&mut cx, |_, cx| {
cx.emit(Event::LanguageServerLog(
server_id,
LanguageServerLogType::Trace(params.verbose),
params.message,
));
})
.ok();
}
}
})
.detach();
@@ -3673,9 +3720,16 @@ impl Project {
(None, override_options) => initialization_options = override_options,
_ => {}
}
let language_server = cx
.update(|cx| language_server.initialize(initialization_options, cx))?
.await?;
.await
.inspect_err(|_| {
if let Some(this) = project.upgrade() {
this.update(cx, |_, cx| cx.emit(Event::LanguageServerRemoved(server_id)))
.ok();
}
})?;
language_server
.notify::<lsp::notification::DidChangeConfiguration>(
@@ -7768,6 +7822,88 @@ impl Project {
}
}
// Returns the resolved version of `path`, that was found in `buffer`, if it exists.
pub fn resolve_existing_file_path(
&self,
path: &str,
buffer: &Model<Buffer>,
cx: &mut ModelContext<Self>,
) -> Task<Option<ResolvedPath>> {
// TODO: ssh based remoting.
if self.ssh_session.is_some() {
return Task::ready(None);
}
if self.is_local() {
let expanded = PathBuf::from(shellexpand::tilde(&path).into_owned());
if expanded.is_absolute() {
let fs = self.fs.clone();
cx.background_executor().spawn(async move {
let path = expanded.as_path();
let exists = fs.is_file(path).await;
exists.then(|| ResolvedPath::AbsPath(expanded))
})
} else {
self.resolve_path_in_worktrees(expanded, buffer, cx)
}
} else {
let path = PathBuf::from(path);
if path.is_absolute() || path.starts_with("~") {
return Task::ready(None);
}
self.resolve_path_in_worktrees(path, buffer, cx)
}
}
fn resolve_path_in_worktrees(
&self,
path: PathBuf,
buffer: &Model<Buffer>,
cx: &mut ModelContext<Self>,
) -> Task<Option<ResolvedPath>> {
let mut candidates = vec![path.clone()];
if let Some(file) = buffer.read(cx).file() {
if let Some(dir) = file.path().parent() {
let joined = dir.to_path_buf().join(path);
candidates.push(joined);
}
}
let worktrees = self.worktrees(cx).collect::<Vec<_>>();
cx.spawn(|_, mut cx| async move {
for worktree in worktrees {
for candidate in candidates.iter() {
let path = worktree
.update(&mut cx, |worktree, _| {
let root_entry_path = &worktree.root_entry().unwrap().path;
let resolved = resolve_path(&root_entry_path, candidate);
let stripped =
resolved.strip_prefix(&root_entry_path).unwrap_or(&resolved);
worktree.entry_for_path(stripped).map(|entry| {
ResolvedPath::ProjectPath(ProjectPath {
worktree_id: worktree.id(),
path: entry.path.clone(),
})
})
})
.ok()?;
if path.is_some() {
return path;
}
}
}
None
})
}
pub fn list_directory(
&self,
query: String,
@@ -11229,6 +11365,14 @@ fn resolve_path(base: &Path, path: &Path) -> PathBuf {
result
}
/// ResolvedPath is a path that has been resolved to either a ProjectPath
/// or an AbsPath and that *exists*.
#[derive(Debug, Clone)]
pub enum ResolvedPath {
ProjectPath(ProjectPath),
AbsPath(PathBuf),
}
impl Item for Buffer {
fn try_open(
project: &Model<Project>,

View File

@@ -1356,22 +1356,47 @@ impl ProjectPanel {
}
fn copy_path(&mut self, _: &CopyPath, cx: &mut ViewContext<Self>) {
if let Some((worktree, entry)) = self.selected_entry(cx) {
cx.write_to_clipboard(ClipboardItem::new_string(
worktree
.abs_path()
.join(&entry.path)
.to_string_lossy()
.to_string(),
));
let abs_file_paths = {
let project = self.project.read(cx);
self.marked_entries()
.into_iter()
.filter_map(|entry| {
let entry_path = project.path_for_entry(entry.entry_id, cx)?.path;
Some(
project
.worktree_for_id(entry.worktree_id, cx)?
.read(cx)
.abs_path()
.join(entry_path)
.to_string_lossy()
.to_string(),
)
})
.collect::<Vec<_>>()
};
if !abs_file_paths.is_empty() {
cx.write_to_clipboard(ClipboardItem::new_string(abs_file_paths.join("\n")));
}
}
fn copy_relative_path(&mut self, _: &CopyRelativePath, cx: &mut ViewContext<Self>) {
if let Some((_, entry)) = self.selected_entry(cx) {
cx.write_to_clipboard(ClipboardItem::new_string(
entry.path.to_string_lossy().to_string(),
));
let file_paths = {
let project = self.project.read(cx);
self.marked_entries()
.into_iter()
.filter_map(|entry| {
Some(
project
.path_for_entry(entry.entry_id, cx)?
.path
.to_string_lossy()
.to_string(),
)
})
.collect::<Vec<_>>()
};
if !file_paths.is_empty() {
cx.write_to_clipboard(ClipboardItem::new_string(file_paths.join("\n")));
}
}

View File

@@ -131,7 +131,7 @@ message Envelope {
UpdateUserPlan update_user_plan = 234;
UpdateDiffBase update_diff_base = 104;
AcceptTermsOfService accept_terms_of_service = 239;
AcceptTermsOfServiceResponse accept_terms_of_service_response = 240; // current max
AcceptTermsOfServiceResponse accept_terms_of_service_response = 240;
OnTypeFormatting on_type_formatting = 105;
OnTypeFormattingResponse on_type_formatting_response = 106;
@@ -264,15 +264,18 @@ message Envelope {
GetSignatureHelp get_signature_help = 217;
GetSignatureHelpResponse get_signature_help_response = 218;
ListRemoteDirectory list_remote_directory = 219;
ListRemoteDirectoryResponse list_remote_directory_response = 220;
UpdateDevServerProject update_dev_server_project = 221;
AddWorktree add_worktree = 222;
AddWorktreeResponse add_worktree_response = 223;
GetLlmToken get_llm_token = 235;
GetLlmTokenResponse get_llm_token_response = 236;
LspExtSwitchSourceHeader lsp_ext_switch_source_header = 241;
LspExtSwitchSourceHeaderResponse lsp_ext_switch_source_header_response = 242; // current max
}
reserved 158 to 161;
@@ -2076,6 +2079,15 @@ message LspExtExpandMacroResponse {
string expansion = 2;
}
message LspExtSwitchSourceHeader {
uint64 project_id = 1;
uint64 buffer_id = 2;
}
message LspExtSwitchSourceHeaderResponse {
string target_file = 1;
}
message SetRoomParticipantRole {
uint64 room_id = 1;
uint64 user_id = 2;

View File

@@ -406,6 +406,8 @@ messages!(
(UpdateContext, Foreground),
(SynchronizeContexts, Foreground),
(SynchronizeContextsResponse, Foreground),
(LspExtSwitchSourceHeader, Background),
(LspExtSwitchSourceHeaderResponse, Background),
(AddWorktree, Foreground),
(AddWorktreeResponse, Foreground),
);
@@ -528,6 +530,7 @@ request_messages!(
(OpenContext, OpenContextResponse),
(CreateContext, CreateContextResponse),
(SynchronizeContexts, SynchronizeContextsResponse),
(LspExtSwitchSourceHeader, LspExtSwitchSourceHeaderResponse),
(AddWorktree, AddWorktreeResponse),
);
@@ -597,6 +600,7 @@ entity_messages!(
CreateContext,
UpdateContext,
SynchronizeContexts,
LspExtSwitchSourceHeader
);
entity_messages!(

View File

@@ -8,13 +8,14 @@ use editor::actions::{
use editor::{Editor, EditorSettings};
use gpui::{
anchored, deferred, Action, AnchorCorner, ClickEvent, DismissEvent, ElementId, EventEmitter,
InteractiveElement, ParentElement, Render, Styled, Subscription, View, ViewContext, WeakView,
Action, AnchorCorner, ClickEvent, ElementId, EventEmitter, InteractiveElement, ParentElement,
Render, Styled, Subscription, View, ViewContext, WeakView,
};
use search::{buffer_search, BufferSearchBar};
use settings::{Settings, SettingsStore};
use ui::{
prelude::*, ButtonStyle, ContextMenu, IconButton, IconButtonShape, IconName, IconSize, Tooltip,
prelude::*, ButtonStyle, ContextMenu, IconButton, IconButtonShape, IconName, IconSize,
PopoverMenu, PopoverMenuHandle, Tooltip,
};
use workspace::{
item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
@@ -27,10 +28,9 @@ pub struct QuickActionBar {
_inlay_hints_enabled_subscription: Option<Subscription>,
active_item: Option<Box<dyn ItemHandle>>,
buffer_search_bar: View<BufferSearchBar>,
repl_menu: Option<View<ContextMenu>>,
show: bool,
toggle_selections_menu: Option<View<ContextMenu>>,
toggle_settings_menu: Option<View<ContextMenu>>,
toggle_selections_handle: PopoverMenuHandle<ContextMenu>,
toggle_settings_handle: PopoverMenuHandle<ContextMenu>,
workspace: WeakView<Workspace>,
}
@@ -44,10 +44,9 @@ impl QuickActionBar {
_inlay_hints_enabled_subscription: None,
active_item: None,
buffer_search_bar,
repl_menu: None,
show: true,
toggle_selections_menu: None,
toggle_settings_menu: None,
toggle_selections_handle: Default::default(),
toggle_settings_handle: Default::default(),
workspace: workspace.weak_handle(),
};
this.apply_settings(cx);
@@ -79,17 +78,6 @@ impl QuickActionBar {
ToolbarItemLocation::Hidden
}
}
fn render_menu_overlay(menu: &View<ContextMenu>) -> Div {
div().absolute().bottom_0().right_0().size_0().child(
deferred(
anchored()
.anchor(AnchorCorner::TopRight)
.child(menu.clone()),
)
.with_priority(1),
)
}
}
impl Render for QuickActionBar {
@@ -158,150 +146,155 @@ impl Render for QuickActionBar {
);
let editor_selections_dropdown = selection_menu_enabled.then(|| {
IconButton::new("toggle_editor_selections_icon", IconName::TextCursor)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.style(ButtonStyle::Subtle)
.selected(self.toggle_selections_menu.is_some())
.on_click({
let focus = editor.focus_handle(cx);
cx.listener(move |quick_action_bar, _, cx| {
let focus = focus.clone();
let menu = ContextMenu::build(cx, move |menu, _| {
menu.context(focus.clone())
.action("Select All", Box::new(SelectAll))
.action(
"Select Next Occurrence",
Box::new(SelectNext {
replace_newest: false,
}),
)
.action("Expand Selection", Box::new(SelectLargerSyntaxNode))
.action("Shrink Selection", Box::new(SelectSmallerSyntaxNode))
.action("Add Cursor Above", Box::new(AddSelectionAbove))
.action("Add Cursor Below", Box::new(AddSelectionBelow))
.separator()
.action("Go to Symbol", Box::new(ToggleOutline))
.action("Go to Line/Column", Box::new(ToggleGoToLine))
.separator()
.action("Next Problem", Box::new(GoToDiagnostic))
.action("Previous Problem", Box::new(GoToPrevDiagnostic))
.separator()
.action("Next Hunk", Box::new(GoToHunk))
.action("Previous Hunk", Box::new(GoToPrevHunk))
.separator()
.action("Move Line Up", Box::new(MoveLineUp))
.action("Move Line Down", Box::new(MoveLineDown))
.action("Duplicate Selection", Box::new(DuplicateLineDown))
});
cx.subscribe(&menu, |quick_action_bar, _, _: &DismissEvent, _cx| {
quick_action_bar.toggle_selections_menu = None;
})
.detach();
quick_action_bar.toggle_selections_menu = Some(menu);
})
})
.when(self.toggle_selections_menu.is_none(), |this| {
this.tooltip(|cx| Tooltip::text("Selection Controls", cx))
let focus = editor.focus_handle(cx);
PopoverMenu::new("editor-selections-dropdown")
.trigger(
IconButton::new("toggle_editor_selections_icon", IconName::TextCursor)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.style(ButtonStyle::Subtle)
.selected(self.toggle_selections_handle.is_deployed())
.when(!self.toggle_selections_handle.is_deployed(), |this| {
this.tooltip(|cx| Tooltip::text("Selection Controls", cx))
}),
)
.with_handle(self.toggle_selections_handle.clone())
.anchor(AnchorCorner::TopRight)
.menu(move |cx| {
let focus = focus.clone();
let menu = ContextMenu::build(cx, move |menu, _| {
menu.context(focus.clone())
.action("Select All", Box::new(SelectAll))
.action(
"Select Next Occurrence",
Box::new(SelectNext {
replace_newest: false,
}),
)
.action("Expand Selection", Box::new(SelectLargerSyntaxNode))
.action("Shrink Selection", Box::new(SelectSmallerSyntaxNode))
.action("Add Cursor Above", Box::new(AddSelectionAbove))
.action("Add Cursor Below", Box::new(AddSelectionBelow))
.separator()
.action("Go to Symbol", Box::new(ToggleOutline))
.action("Go to Line/Column", Box::new(ToggleGoToLine))
.separator()
.action("Next Problem", Box::new(GoToDiagnostic))
.action("Previous Problem", Box::new(GoToPrevDiagnostic))
.separator()
.action("Next Hunk", Box::new(GoToHunk))
.action("Previous Hunk", Box::new(GoToPrevHunk))
.separator()
.action("Move Line Up", Box::new(MoveLineUp))
.action("Move Line Down", Box::new(MoveLineDown))
.action("Duplicate Selection", Box::new(DuplicateLineDown))
});
Some(menu)
})
});
let editor_settings_dropdown =
IconButton::new("toggle_editor_settings_icon", IconName::Sliders)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.style(ButtonStyle::Subtle)
.selected(self.toggle_settings_menu.is_some())
.on_click({
let editor = editor.clone();
cx.listener(move |quick_action_bar, _, cx| {
let menu = ContextMenu::build(cx, |mut menu, _| {
if supports_inlay_hints {
menu = menu.toggleable_entry(
"Inlay Hints",
inlay_hints_enabled,
IconPosition::Start,
Some(editor::actions::ToggleInlayHints.boxed_clone()),
{
let editor = editor.clone();
move |cx| {
editor.update(cx, |editor, cx| {
editor.toggle_inlay_hints(
&editor::actions::ToggleInlayHints,
cx,
);
});
}
},
);
}
menu = menu.toggleable_entry(
"Inline Git Blame",
git_blame_inline_enabled,
IconPosition::Start,
Some(editor::actions::ToggleGitBlameInline.boxed_clone()),
{
let editor = editor.clone();
move |cx| {
editor.update(cx, |editor, cx| {
editor.toggle_git_blame_inline(
&editor::actions::ToggleGitBlameInline,
cx,
)
});
}
},
);
menu = menu.toggleable_entry(
"Selection Menu",
selection_menu_enabled,
IconPosition::Start,
Some(editor::actions::ToggleSelectionMenu.boxed_clone()),
{
let editor = editor.clone();
move |cx| {
editor.update(cx, |editor, cx| {
editor.toggle_selection_menu(
&editor::actions::ToggleSelectionMenu,
cx,
)
});
}
},
);
menu = menu.toggleable_entry(
"Auto Signature Help",
auto_signature_help_enabled,
IconPosition::Start,
Some(editor::actions::ToggleAutoSignatureHelp.boxed_clone()),
{
let editor = editor.clone();
move |cx| {
editor.update(cx, |editor, cx| {
editor.toggle_auto_signature_help_menu(
&editor::actions::ToggleAutoSignatureHelp,
let editor = editor.downgrade();
let editor_settings_dropdown = PopoverMenu::new("editor-settings")
.trigger(
IconButton::new("toggle_editor_settings_icon", IconName::Sliders)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.style(ButtonStyle::Subtle)
.selected(self.toggle_settings_handle.is_deployed())
.when(!self.toggle_settings_handle.is_deployed(), |this| {
this.tooltip(|cx| Tooltip::text("Editor Controls", cx))
}),
)
.anchor(AnchorCorner::TopRight)
.with_handle(self.toggle_settings_handle.clone())
.menu(move |cx| {
let menu = ContextMenu::build(cx, |mut menu, _| {
if supports_inlay_hints {
menu = menu.toggleable_entry(
"Inlay Hints",
inlay_hints_enabled,
IconPosition::Start,
Some(editor::actions::ToggleInlayHints.boxed_clone()),
{
let editor = editor.clone();
move |cx| {
editor
.update(cx, |editor, cx| {
editor.toggle_inlay_hints(
&editor::actions::ToggleInlayHints,
cx,
);
});
}
},
);
})
.ok();
}
},
);
}
menu
});
cx.subscribe(&menu, |quick_action_bar, _, _: &DismissEvent, _cx| {
quick_action_bar.toggle_settings_menu = None;
})
.detach();
quick_action_bar.toggle_settings_menu = Some(menu);
})
})
.when(self.toggle_settings_menu.is_none(), |this| {
this.tooltip(|cx| Tooltip::text("Editor Controls", cx))
menu = menu.toggleable_entry(
"Inline Git Blame",
git_blame_inline_enabled,
IconPosition::Start,
Some(editor::actions::ToggleGitBlameInline.boxed_clone()),
{
let editor = editor.clone();
move |cx| {
editor
.update(cx, |editor, cx| {
editor.toggle_git_blame_inline(
&editor::actions::ToggleGitBlameInline,
cx,
)
})
.ok();
}
},
);
menu = menu.toggleable_entry(
"Selection Menu",
selection_menu_enabled,
IconPosition::Start,
Some(editor::actions::ToggleSelectionMenu.boxed_clone()),
{
let editor = editor.clone();
move |cx| {
editor
.update(cx, |editor, cx| {
editor.toggle_selection_menu(
&editor::actions::ToggleSelectionMenu,
cx,
)
})
.ok();
}
},
);
menu = menu.toggleable_entry(
"Auto Signature Help",
auto_signature_help_enabled,
IconPosition::Start,
Some(editor::actions::ToggleAutoSignatureHelp.boxed_clone()),
{
let editor = editor.clone();
move |cx| {
editor
.update(cx, |editor, cx| {
editor.toggle_auto_signature_help_menu(
&editor::actions::ToggleAutoSignatureHelp,
cx,
);
})
.ok();
}
},
);
menu
});
Some(menu)
});
h_flex()
.id("quick action bar")
@@ -316,21 +309,6 @@ impl Render for QuickActionBar {
)
.children(editor_selections_dropdown)
.child(editor_settings_dropdown)
.when_some(self.repl_menu.as_ref(), |el, repl_menu| {
el.child(Self::render_menu_overlay(repl_menu))
})
.when_some(
self.toggle_settings_menu.as_ref(),
|el, toggle_settings_menu| {
el.child(Self::render_menu_overlay(toggle_settings_menu))
},
)
.when_some(
self.toggle_selections_menu.as_ref(),
|el, toggle_selections_menu| {
el.child(Self::render_menu_overlay(toggle_selections_menu))
},
)
}
}

View File

@@ -173,8 +173,6 @@ impl QuickActionBar {
url: format!("{}#change-kernel", ZED_REPL_DOCUMENTATION),
}),
)
// TODO: Add Restart action
// .action("Restart", Box::new(gpui::NoAction))
.custom_entry(
move |_cx| {
Label::new("Shut Down Kernel")
@@ -189,6 +187,20 @@ impl QuickActionBar {
}
},
)
.custom_entry(
move |_cx| {
Label::new("Restart Kernel")
.size(LabelSize::Small)
.color(Color::Error)
.into_any_element()
},
{
let editor = editor.clone();
move |cx| {
repl::restart(editor.clone(), cx);
}
},
)
.separator()
.action("View Sessions", Box::new(repl::Sessions))
// TODO: Add shut down all kernels action
@@ -305,6 +317,15 @@ fn session_state(session: View<Session>, cx: &WindowContext) -> ReplMenuState {
};
let menu_state = match &session.kernel {
Kernel::Restarting => ReplMenuState {
tooltip: format!("Restarting {}", kernel_name).into(),
icon_is_animating: true,
popover_disabled: true,
icon_color: Color::Muted,
indicator: Some(Indicator::dot().color(Color::Muted)),
status: session.kernel.status(),
..fill_fields()
},
Kernel::RunningKernel(kernel) => match &kernel.execution_state {
ExecutionState::Idle => ReplMenuState {
tooltip: format!("Run code on {} ({})", kernel_name, kernel_language).into(),

View File

@@ -87,6 +87,7 @@ pub enum KernelStatus {
Error,
ShuttingDown,
Shutdown,
Restarting,
}
impl KernelStatus {
@@ -107,6 +108,7 @@ impl ToString for KernelStatus {
KernelStatus::Error => "Error".to_string(),
KernelStatus::ShuttingDown => "Shutting Down".to_string(),
KernelStatus::Shutdown => "Shutdown".to_string(),
KernelStatus::Restarting => "Restarting".to_string(),
}
}
}
@@ -122,6 +124,7 @@ impl From<&Kernel> for KernelStatus {
Kernel::ErroredLaunch(_) => KernelStatus::Error,
Kernel::ShuttingDown => KernelStatus::ShuttingDown,
Kernel::Shutdown => KernelStatus::Shutdown,
Kernel::Restarting => KernelStatus::Restarting,
}
}
}
@@ -133,6 +136,7 @@ pub enum Kernel {
ErroredLaunch(String),
ShuttingDown,
Shutdown,
Restarting,
}
impl Kernel {
@@ -160,7 +164,7 @@ impl Kernel {
pub fn is_shutting_down(&self) -> bool {
match self {
Kernel::ShuttingDown => true,
Kernel::Restarting | Kernel::ShuttingDown => true,
Kernel::RunningKernel(_)
| Kernel::StartingKernel(_)
| Kernel::ErroredLaunch(_)
@@ -324,7 +328,7 @@ impl RunningKernel {
_control_task: control_task,
_routing_task: routing_task,
connection_path,
execution_state: ExecutionState::Busy,
execution_state: ExecutionState::Idle,
kernel_info: None,
},
messages_rx,

View File

@@ -420,6 +420,7 @@ pub enum ExecutionStatus {
ShuttingDown,
Shutdown,
KernelErrored(String),
Restarting,
}
pub struct ExecutionView {
@@ -613,6 +614,9 @@ impl Render for ExecutionView {
ExecutionStatus::ShuttingDown => Label::new("Kernel shutting down...")
.color(Color::Muted)
.into_any_element(),
ExecutionStatus::Restarting => Label::new("Kernel restarting...")
.color(Color::Muted)
.into_any_element(),
ExecutionStatus::Shutdown => Label::new("Kernel shutdown")
.color(Color::Muted)
.into_any_element(),

View File

@@ -20,7 +20,7 @@ pub use crate::jupyter_settings::JupyterSettings;
pub use crate::kernels::{Kernel, KernelSpecification, KernelStatus};
pub use crate::repl_editor::*;
pub use crate::repl_sessions_ui::{
ClearOutputs, Interrupt, ReplSessionsPage, Run, Sessions, Shutdown,
ClearOutputs, Interrupt, ReplSessionsPage, Restart, Run, Sessions, Shutdown,
};
use crate::repl_store::ReplStore;
pub use crate::session::Session;

View File

@@ -168,6 +168,27 @@ pub fn shutdown(editor: WeakView<Editor>, cx: &mut WindowContext) {
});
}
pub fn restart(editor: WeakView<Editor>, cx: &mut WindowContext) {
let Some(editor) = editor.upgrade() else {
return;
};
let entity_id = editor.entity_id();
let Some(session) = ReplStore::global(cx)
.read(cx)
.get_session(entity_id)
.cloned()
else {
return;
};
session.update(cx, |session, cx| {
session.restart(cx);
cx.notify();
});
}
fn cell_range(buffer: &BufferSnapshot, start_row: u32, end_row: u32) -> Range<Point> {
let mut snippet_end_row = end_row;
while buffer.is_line_blank(snippet_end_row) && snippet_end_row > start_row {

View File

@@ -23,6 +23,7 @@ actions!(
Sessions,
Interrupt,
Shutdown,
Restart,
RefreshKernelspecs
]
);
@@ -126,6 +127,19 @@ pub fn init(cx: &mut AppContext) {
}
})
.detach();
editor
.register_action({
let editor_handle = editor_handle.clone();
move |_: &Restart, cx| {
if !JupyterSettings::enabled(cx) {
return;
}
crate::restart(editor_handle.clone(), cx);
}
})
.detach();
})
.detach();
}

View File

@@ -31,10 +31,12 @@ use theme::ActiveTheme;
use ui::{prelude::*, IconButtonShape, Tooltip};
pub struct Session {
fs: Arc<dyn Fs>,
editor: WeakView<Editor>,
pub kernel: Kernel,
blocks: HashMap<String, EditorBlock>,
messaging_task: Task<()>,
messaging_task: Option<Task<()>>,
process_status_task: Option<Task<()>>,
pub kernel_specification: KernelSpecification,
telemetry: Arc<Telemetry>,
_buffer_subscription: Subscription,
@@ -192,24 +194,50 @@ impl Session {
kernel_specification: KernelSpecification,
cx: &mut ViewContext<Self>,
) -> Self {
let kernel_language = kernel_specification.kernelspec.language.clone();
let subscription = match editor.upgrade() {
Some(editor) => {
let buffer = editor.read(cx).buffer().clone();
cx.subscribe(&buffer, Self::on_buffer_event)
}
None => Subscription::new(|| {}),
};
telemetry.report_repl_event(
let mut session = Self {
fs,
editor,
kernel: Kernel::StartingKernel(Task::ready(()).shared()),
messaging_task: None,
process_status_task: None,
blocks: HashMap::default(),
kernel_specification,
_buffer_subscription: subscription,
telemetry,
};
session.start_kernel(cx);
session
}
fn start_kernel(&mut self, cx: &mut ViewContext<Self>) {
let kernel_language = self.kernel_specification.kernelspec.language.clone();
let entity_id = self.editor.entity_id();
let working_directory = self
.editor
.upgrade()
.and_then(|editor| editor.read(cx).working_directory(cx))
.unwrap_or_else(temp_dir);
self.telemetry.report_repl_event(
kernel_language.clone(),
KernelStatus::Starting.to_string(),
cx.entity_id().to_string(),
);
let entity_id = editor.entity_id();
let working_directory = editor
.upgrade()
.and_then(|editor| editor.read(cx).working_directory(cx))
.unwrap_or_else(temp_dir);
let kernel = RunningKernel::new(
kernel_specification.clone(),
self.kernel_specification.clone(),
entity_id,
working_directory,
fs.clone(),
self.fs.clone(),
cx,
);
@@ -229,6 +257,7 @@ impl Session {
let reader = BufReader::new(stderr.unwrap());
let mut lines = reader.lines();
while let Some(Ok(line)) = lines.next().await {
// todo!(): Log stdout and stderr to something the session can show
log::error!("kernel: {}", line);
}
})
@@ -251,7 +280,7 @@ impl Session {
let status = kernel.process.status();
session.kernel(Kernel::RunningKernel(kernel), cx);
cx.spawn(|session, mut cx| async move {
let process_status_task = cx.spawn(|session, mut cx| async move {
let error_message = match status.await {
Ok(status) => {
if status.success() {
@@ -299,10 +328,11 @@ impl Session {
cx.notify();
})
.ok();
})
.detach();
});
session.messaging_task = cx.spawn(|session, mut cx| async move {
session.process_status_task = Some(process_status_task);
session.messaging_task = Some(cx.spawn(|session, mut cx| async move {
while let Some(message) = messages_rx.next().await {
session
.update(&mut cx, |session, cx| {
@@ -310,9 +340,9 @@ impl Session {
})
.ok();
}
});
}));
// todo!(@rgbkrk): send kernelinforequest once our shell channel read/writes are split
// todo!(@rgbkrk): send KernelInfoRequest once our shell channel read/writes are split
// cx.spawn(|this, mut cx| async move {
// cx.background_executor()
// .timer(Duration::from_millis(120))
@@ -336,23 +366,8 @@ impl Session {
})
.shared();
let subscription = match editor.upgrade() {
Some(editor) => {
let buffer = editor.read(cx).buffer().clone();
cx.subscribe(&buffer, Self::on_buffer_event)
}
None => Subscription::new(|| {}),
};
return Self {
editor,
kernel: Kernel::StartingKernel(pending_kernel),
messaging_task: Task::ready(()),
blocks: HashMap::default(),
kernel_specification,
_buffer_subscription: subscription,
telemetry,
};
self.kernel(Kernel::StartingKernel(pending_kernel), cx);
cx.notify();
}
fn on_buffer_event(
@@ -453,6 +468,7 @@ impl Session {
.ok();
let status = match &self.kernel {
Kernel::Restarting => ExecutionStatus::Restarting,
Kernel::RunningKernel(_) => ExecutionStatus::Queued,
Kernel::StartingKernel(_) => ExecutionStatus::ConnectingToKernel,
Kernel::ErroredLaunch(error) => ExecutionStatus::KernelErrored(error.clone()),
@@ -615,6 +631,12 @@ impl Session {
// Give the kernel a bit of time to clean up
cx.background_executor().timer(Duration::from_secs(3)).await;
this.update(&mut cx, |session, _cx| {
session.messaging_task.take();
session.process_status_task.take();
})
.ok();
kernel.process.kill().ok();
this.update(&mut cx, |session, cx| {
@@ -626,11 +648,59 @@ impl Session {
})
.detach();
}
Kernel::StartingKernel(_kernel) => {
self.kernel = Kernel::Shutdown;
_ => {
self.messaging_task.take();
self.process_status_task.take();
self.kernel(Kernel::Shutdown, cx);
}
}
cx.notify();
}
pub fn restart(&mut self, cx: &mut ViewContext<Self>) {
let kernel = std::mem::replace(&mut self.kernel, Kernel::Restarting);
match kernel {
Kernel::Restarting => {
// Do nothing if already restarting
}
Kernel::RunningKernel(mut kernel) => {
let mut request_tx = kernel.request_tx.clone();
cx.spawn(|this, mut cx| async move {
// Send shutdown request with restart flag
log::debug!("restarting kernel");
let message: JupyterMessage = ShutdownRequest { restart: true }.into();
request_tx.try_send(message).ok();
this.update(&mut cx, |session, _cx| {
session.messaging_task.take();
session.process_status_task.take();
})
.ok();
// Wait for kernel to shutdown
cx.background_executor().timer(Duration::from_secs(1)).await;
// Force kill the kernel if it hasn't shut down
kernel.process.kill().ok();
// Start a new kernel
this.update(&mut cx, |session, cx| {
// todo!(): Differentiate between restart and restart+clear-outputs
session.clear_outputs(cx);
session.start_kernel(cx);
})
.ok();
})
.detach();
}
_ => {
self.kernel = Kernel::Shutdown;
// If it's not already running, we can just clean up and start a new kernel
self.messaging_task.take();
self.process_status_task.take();
self.clear_outputs(cx);
self.start_kernel(cx);
}
}
cx.notify();
@@ -663,6 +733,7 @@ impl Render for Session {
Kernel::ErroredLaunch(err) => (Some(format!("Error: {err}")), None),
Kernel::ShuttingDown => (Some("Shutting Down".into()), None),
Kernel::Shutdown => (Some("Shutdown".into()), None),
Kernel::Restarting => (Some("Restarting".into()), None),
};
KernelListItem::new(self.kernel_specification.clone())
@@ -675,6 +746,7 @@ impl Render for Session {
Kernel::ErroredLaunch(_) => Color::Error,
Kernel::ShuttingDown => Color::Modified,
Kernel::Shutdown => Color::Disabled,
Kernel::Restarting => Color::Modified,
})
.child(Label::new(self.kernel_specification.name.clone()))
.children(status_text.map(|status_text| Label::new(format!("({status_text})"))))

View File

@@ -1,17 +1,24 @@
/// Please see: [Telemetry in Zed](https://zed.dev/docs/telemetry) for additional documentation.
use semantic_version::SemanticVersion;
use serde::{Deserialize, Serialize};
use std::{fmt::Display, sync::Arc, time::Duration};
#[derive(Serialize, Deserialize, Debug)]
pub struct EventRequestBody {
/// Identifier unique to each Zed installation (differs for stable, preview, dev)
pub installation_id: Option<String>,
/// Identifier unique to each logged in Zed user (randomly generated on first sign in)
pub metrics_id: Option<String>,
/// Identifier unique to each Zed session (differs for each time you open Zed)
pub session_id: Option<String>,
/// True for Zed staff, otherwise false
pub is_staff: Option<bool>,
/// Zed version number
pub app_version: String,
pub os_name: String,
pub os_version: Option<String>,
pub architecture: String,
/// Zed release channel (stable, preview, dev)
pub release_channel: Option<String>,
pub events: Vec<EventWrapper>,
}
@@ -25,6 +32,7 @@ impl EventRequestBody {
#[derive(Serialize, Deserialize, Debug)]
pub struct EventWrapper {
pub signed_in: bool,
/// Duration between this event's timestamp and the timestamp of the first event in the current batch
pub milliseconds_since_first_event: i64,
#[serde(flatten)]
pub event: Event,
@@ -70,13 +78,19 @@ pub enum Event {
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct EditorEvent {
/// The editor operation performed (open, save)
pub operation: String,
/// The extension of the file that was opened or saved
pub file_extension: Option<String>,
/// Whether the user is in vim mode or not
pub vim_mode: bool,
/// Whether the user has copilot enabled or not
pub copilot_enabled: bool,
/// Whether the user has copilot enabled for the language of the file opened or saved
pub copilot_enabled_for_language: bool,
}
/// Deprecated since Zed v0.137.0 (2024-05-29). Replaced by InlineCompletionEvent.
// Needed for clients sending old copilot_event types
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct CopilotEvent {
@@ -87,6 +101,7 @@ pub struct CopilotEvent {
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct InlineCompletionEvent {
/// Provider of the completion suggestion (e.g. copilot, supermaven)
pub provider: String,
pub suggestion_accepted: bool,
pub file_extension: Option<String>,
@@ -94,6 +109,7 @@ pub struct InlineCompletionEvent {
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct CallEvent {
/// Operation performed: invite/join call; begin/end screenshare; share/unshare project; etc
pub operation: String,
pub room_id: Option<u64>,
pub channel_id: Option<u64>,
@@ -101,8 +117,11 @@ pub struct CallEvent {
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct AssistantEvent {
/// Unique random identifier for each assistant tab (None for inline assist)
pub conversation_id: Option<String>,
/// The kind of assistant (Panel, Inline)
pub kind: AssistantKind,
/// Name of the AI model used (gpt-4o, claude-3-5-sonnet, etc)
pub model: String,
pub response_latency: Option<Duration>,
pub error_message: Option<String>,
@@ -171,6 +190,7 @@ pub struct HangReport {
pub os_name: String,
pub os_version: Option<String>,
pub architecture: String,
/// Identifier unique to each Zed installation (differs for stable, preview, dev)
pub installation_id: Option<String>,
}
@@ -182,19 +202,27 @@ pub struct LocationData {
#[derive(Serialize, Deserialize)]
pub struct Panic {
/// The name of the thread that panicked
pub thread: String,
/// The panic message
pub payload: String,
/// The location of the panic (file, line number)
#[serde(skip_serializing_if = "Option::is_none")]
pub location_data: Option<LocationData>,
pub backtrace: Vec<String>,
/// Zed version number
pub app_version: String,
/// Zed release channel (stable, preview, dev)
pub release_channel: String,
pub os_name: String,
pub os_version: Option<String>,
pub architecture: String,
/// The time the panic occurred (UNIX millisecond timestamp)
pub panicked_on: i64,
#[serde(skip_serializing_if = "Option::is_none")]
/// Identifier unique to each Zed installation (differs for stable, preview, dev)
pub installation_id: Option<String>,
/// Identifier unique to each Zed session (differs for each time you open Zed)
pub session_id: String,
}

View File

@@ -18,7 +18,7 @@ use alacritty_terminal::{
Config, RenderableCursor, TermMode,
},
tty::{self, setup_env},
vte::ansi::{ClearMode, Handler, NamedPrivateMode, PrivateMode, Rgb},
vte::ansi::{ClearMode, Handler, NamedPrivateMode, PrivateMode},
Term,
};
use anyhow::{bail, Result};
@@ -127,7 +127,6 @@ pub enum MaybeNavigationTarget {
#[derive(Clone)]
enum InternalEvent {
ColorRequest(usize, Arc<dyn Fn(Rgb) -> String + Sync + Send + 'static>),
Resize(TerminalSize),
Clear,
// FocusNextMatch,
@@ -688,9 +687,19 @@ impl Terminal {
cx.emit(Event::TitleChanged);
}
}
AlacTermEvent::ColorRequest(idx, fun_ptr) => {
self.events
.push_back(InternalEvent::ColorRequest(*idx, fun_ptr.clone()));
AlacTermEvent::ColorRequest(index, format) => {
// It's important that the color request is processed here to retain relative order
// with other PTY writes. Otherwise applications might witness out-of-order
// responses to requests. For example: An application sending `OSC 11 ; ? ST`
// (color request) followed by `CSI c` (request device attributes) would receive
// the response to `CSI c` first.
// Instead of locking, we could store the colors in `self.last_content`. But then
// we might respond with out of date value if a "set color" sequence is immediately
// followed by a color request sequence.
let color = self.term.lock().colors()[*index].unwrap_or_else(|| {
to_alac_rgb(get_color_at_index(*index, cx.theme().as_ref()))
});
self.write_to_pty(format(color));
}
AlacTermEvent::ChildExit(error_code) => {
self.register_task_finished(Some(*error_code), cx);
@@ -714,12 +723,6 @@ impl Terminal {
cx: &mut ModelContext<Self>,
) {
match event {
InternalEvent::ColorRequest(index, format) => {
let color = term.colors()[*index].unwrap_or_else(|| {
to_alac_rgb(get_color_at_index(*index, cx.theme().as_ref()))
});
self.write_to_pty(format(color))
}
InternalEvent::Resize(mut new_size) => {
new_size.size.height = cmp::max(new_size.line_height, new_size.height());
new_size.size.width = cmp::max(new_size.cell_width, new_size.width());

View File

@@ -5,7 +5,7 @@ use collections::{HashMap, HashSet};
use db::kvp::KEY_VALUE_STORE;
use futures::future::join_all;
use gpui::{
actions, Action, AnyView, AppContext, AsyncWindowContext, DismissEvent, Entity, EventEmitter,
actions, Action, AnchorCorner, AnyView, AppContext, AsyncWindowContext, Entity, EventEmitter,
ExternalPaths, FocusHandle, FocusableView, IntoElement, Model, ParentElement, Pixels, Render,
Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowContext,
};
@@ -20,7 +20,7 @@ use terminal::{
Terminal,
};
use ui::{
h_flex, ButtonCommon, Clickable, ContextMenu, FluentBuilder, IconButton, IconSize, Selectable,
h_flex, ButtonCommon, Clickable, ContextMenu, IconButton, IconSize, PopoverMenu, Selectable,
Tooltip,
};
use util::{ResultExt, TryFutureExt};
@@ -173,47 +173,42 @@ impl TerminalPanel {
let additional_buttons = self.additional_tab_bar_buttons.clone();
self.pane.update(cx, |pane, cx| {
pane.set_render_tab_bar_buttons(cx, move |pane, cx| {
if !pane.has_focus(cx) {
if !pane.has_focus(cx) && !pane.context_menu_focused(cx) {
return (None, None);
}
let focus_handle = pane.focus_handle(cx);
let right_children = h_flex()
.gap_2()
.children(additional_buttons.clone())
.child(
IconButton::new("plus", IconName::Plus)
.icon_size(IconSize::Small)
.on_click(cx.listener(|pane, _, cx| {
let focus_handle = pane.focus_handle(cx);
PopoverMenu::new("terminal-tab-bar-popover-menu")
.trigger(
IconButton::new("plus", IconName::Plus)
.icon_size(IconSize::Small)
.tooltip(|cx| Tooltip::text("New...", cx)),
)
.anchor(AnchorCorner::TopRight)
.with_handle(pane.new_item_context_menu_handle.clone())
.menu(move |cx| {
let focus_handle = focus_handle.clone();
let menu = ContextMenu::build(cx, |menu, _| {
menu.action(
"New Terminal",
workspace::NewTerminal.boxed_clone(),
)
.entry(
"Spawn task",
Some(tasks_ui::Spawn::modal().boxed_clone()),
move |cx| {
// We want the focus to go back to terminal panel once task modal is dismissed,
// hence we focus that first. Otherwise, we'd end up without a focused element, as
// context menu will be gone the moment we spawn the modal.
cx.focus(&focus_handle);
cx.dispatch_action(
tasks_ui::Spawn::modal().boxed_clone(),
);
},
)
menu.context(focus_handle.clone())
.action(
"New Terminal",
workspace::NewTerminal.boxed_clone(),
)
// We want the focus to go back to terminal panel once task modal is dismissed,
// hence we focus that first. Otherwise, we'd end up without a focused element, as
// context menu will be gone the moment we spawn the modal.
.action(
"Spawn task",
tasks_ui::Spawn::modal().boxed_clone(),
)
});
cx.subscribe(&menu, |pane, _, _: &DismissEvent, _| {
pane.new_item_menu = None;
})
.detach();
pane.new_item_menu = Some(menu);
}))
.tooltip(|cx| Tooltip::text("New...", cx)),
Some(menu)
}),
)
.when_some(pane.new_item_menu.as_ref(), |el, new_item_menu| {
el.child(Pane::render_menu_overlay(new_item_menu))
})
.child({
let zoomed = pane.is_zoomed();
IconButton::new("toggle_zoom", IconName::Maximize)
@@ -891,7 +886,11 @@ fn to_windows_shell_variable(shell_type: WindowsShellType, input: String) -> Str
#[cfg(target_os = "windows")]
fn to_windows_shell_type(shell: &str) -> WindowsShellType {
if shell == "powershell" || shell.ends_with("powershell.exe") {
if shell == "powershell"
|| shell.ends_with("powershell.exe")
|| shell == "pwsh"
|| shell.ends_with("pwsh.exe")
{
WindowsShellType::Powershell
} else if shell == "cmd" || shell.ends_with("cmd.exe") {
WindowsShellType::Cmd

View File

@@ -56,6 +56,23 @@ impl<M: ManagedView> PopoverMenuHandle<M> {
}
}
}
pub fn is_deployed(&self) -> bool {
self.0
.borrow()
.as_ref()
.map_or(false, |state| state.menu.borrow().as_ref().is_some())
}
pub fn is_focused(&self, cx: &mut WindowContext) -> bool {
self.0.borrow().as_ref().map_or(false, |state| {
state
.menu
.borrow()
.as_ref()
.map_or(false, |view| view.focus_handle(cx).is_focused(cx))
})
}
}
pub struct PopoverMenu<M: ManagedView> {
@@ -340,9 +357,12 @@ impl<M: ManagedView> Element for PopoverMenu<M> {
// want a click on the toggle to re-open it.
cx.on_mouse_event(move |_: &MouseDownEvent, phase, cx| {
if phase == DispatchPhase::Bubble && child_hitbox.is_hovered(cx) {
menu_handle.borrow_mut().take();
if let Some(menu) = menu_handle.borrow().as_ref() {
menu.update(cx, |_, cx| {
cx.emit(DismissEvent);
});
}
cx.stop_propagation();
cx.refresh();
}
})
}

View File

@@ -529,8 +529,8 @@ fn generate_commands(_: &AppContext) -> Vec<VimCommand> {
save_intent: Some(SaveIntent::Overwrite),
}),
VimCommand::new(("cq", "uit"), zed_actions::Quit),
VimCommand::new(("sp", "lit"), workspace::SplitUp),
VimCommand::new(("vs", "plit"), workspace::SplitLeft),
VimCommand::new(("sp", "lit"), workspace::SplitHorizontal),
VimCommand::new(("vs", "plit"), workspace::SplitVertical),
VimCommand::new(
("bd", "elete"),
workspace::CloseActiveItem {
@@ -546,14 +546,8 @@ fn generate_commands(_: &AppContext) -> Vec<VimCommand> {
VimCommand::new(("bf", "irst"), workspace::ActivateItem(0)),
VimCommand::new(("br", "ewind"), workspace::ActivateItem(0)),
VimCommand::new(("bl", "ast"), workspace::ActivateLastItem),
VimCommand::new(
("new", ""),
workspace::NewFileInDirection(workspace::SplitDirection::Up),
),
VimCommand::new(
("vne", "w"),
workspace::NewFileInDirection(workspace::SplitDirection::Left),
),
VimCommand::new(("new", ""), workspace::NewFileSplitHorizontal),
VimCommand::new(("vne", "w"), workspace::NewFileSplitVertical),
VimCommand::new(("tabe", "dit"), workspace::NewFile),
VimCommand::new(("tabnew", ""), workspace::NewFile),
VimCommand::new(("tabn", "ext"), workspace::ActivateNextItem).count(),
@@ -742,9 +736,15 @@ fn generate_positions(string: &str, query: &str) -> Vec<usize> {
mod test {
use std::path::Path;
use crate::test::{NeovimBackedTestContext, VimTestContext};
use crate::{
state::Mode,
test::{NeovimBackedTestContext, VimTestContext},
};
use editor::Editor;
use gpui::TestAppContext;
use indoc::indoc;
use ui::ViewContext;
use workspace::Workspace;
#[gpui::test]
async fn test_command_basics(cx: &mut TestAppContext) {
@@ -923,4 +923,55 @@ mod test {
.await;
cx.shared_state().await.assert_eq("k\nk\nˇk\n4\n4\n3\n2\n1");
}
fn assert_active_item(
workspace: &mut Workspace,
expected_path: &str,
expected_text: &str,
cx: &mut ViewContext<Workspace>,
) {
let active_editor = workspace.active_item_as::<Editor>(cx).unwrap();
let buffer = active_editor
.read(cx)
.buffer()
.read(cx)
.as_singleton()
.unwrap();
let text = buffer.read(cx).text();
let file = buffer.read(cx).file().unwrap();
let file_path = file.as_local().unwrap().abs_path(cx);
assert_eq!(text, expected_text);
assert_eq!(file_path.to_str().unwrap(), expected_path);
}
#[gpui::test]
async fn test_command_gf(cx: &mut TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
// Assert base state, that we're in /root/dir/file.rs
cx.workspace(|workspace, cx| {
assert_active_item(workspace, "/root/dir/file.rs", "", cx);
});
// Insert a new file
let fs = cx.workspace(|workspace, cx| workspace.project().read(cx).fs().clone());
fs.as_fake()
.insert_file("/root/dir/file2.rs", "This is file2.rs".as_bytes().to_vec())
.await;
// Put the path to the second file into the currently open buffer
cx.set_state(indoc! {"go to fiˇle2.rs"}, Mode::Normal);
// Go to file2.rs
cx.simulate_keystrokes("g f");
// We now have two items
cx.workspace(|workspace, cx| assert_eq!(workspace.items(cx).count(), 2));
cx.workspace(|workspace, cx| {
assert_active_item(workspace, "/root/dir/file2.rs", "This is file2.rs", cx);
});
}
}

View File

@@ -14,9 +14,9 @@ use collections::{BTreeSet, HashMap, HashSet, VecDeque};
use futures::{stream::FuturesUnordered, StreamExt};
use gpui::{
actions, anchored, deferred, impl_actions, prelude::*, Action, AnchorCorner, AnyElement,
AppContext, AsyncWindowContext, ClickEvent, ClipboardItem, DismissEvent, Div, DragMoveEvent,
EntityId, EventEmitter, ExternalPaths, FocusHandle, FocusOutEvent, FocusableView, KeyContext,
Model, MouseButton, MouseDownEvent, NavigationDirection, Pixels, Point, PromptLevel, Render,
AppContext, AsyncWindowContext, ClickEvent, ClipboardItem, Div, DragMoveEvent, EntityId,
EventEmitter, ExternalPaths, FocusHandle, FocusOutEvent, FocusableView, KeyContext, Model,
MouseButton, MouseDownEvent, NavigationDirection, Pixels, Point, PromptLevel, Render,
ScrollHandle, Subscription, Task, View, ViewContext, VisualContext, WeakFocusHandle, WeakView,
WindowContext,
};
@@ -40,7 +40,7 @@ use theme::ThemeSettings;
use ui::{
prelude::*, right_click_menu, ButtonSize, Color, IconButton, IconButtonShape, IconName,
IconSize, Indicator, Label, Tab, TabBar, TabPosition, Tooltip,
IconSize, Indicator, Label, PopoverMenu, PopoverMenuHandle, Tab, TabBar, TabPosition, Tooltip,
};
use ui::{v_flex, ContextMenu};
use util::{debug_panic, maybe, truncate_and_remove_front, ResultExt};
@@ -152,6 +152,8 @@ actions!(
SplitUp,
SplitRight,
SplitDown,
SplitHorizontal,
SplitVertical,
TogglePreviewTab,
]
);
@@ -245,8 +247,6 @@ pub struct Pane {
last_focus_handle_by_item: HashMap<EntityId, WeakFocusHandle>,
nav_history: NavHistory,
toolbar: View<Toolbar>,
pub new_item_menu: Option<View<ContextMenu>>,
split_item_menu: Option<View<ContextMenu>>,
pub(crate) workspace: WeakView<Workspace>,
project: Model<Project>,
drag_split_direction: Option<SplitDirection>,
@@ -264,6 +264,8 @@ pub struct Pane {
display_nav_history_buttons: Option<bool>,
double_click_dispatch_action: Box<dyn Action>,
save_modals_spawned: HashSet<EntityId>,
pub new_item_context_menu_handle: PopoverMenuHandle<ContextMenu>,
split_item_context_menu_handle: PopoverMenuHandle<ContextMenu>,
}
pub struct ActivationHistoryEntry {
@@ -364,8 +366,6 @@ impl Pane {
next_timestamp,
}))),
toolbar: cx.new_view(|_| Toolbar::new()),
new_item_menu: None,
split_item_menu: None,
tab_bar_scroll_handle: ScrollHandle::new(),
drag_split_direction: None,
workspace,
@@ -375,7 +375,7 @@ impl Pane {
can_split: true,
should_display_tab_bar: Rc::new(|cx| TabBarSettings::get_global(cx).show),
render_tab_bar_buttons: Rc::new(move |pane, cx| {
if !pane.has_focus(cx) {
if !pane.has_focus(cx) && !pane.context_menu_focused(cx) {
return (None, None);
}
// Ideally we would return a vec of elements here to pass directly to the [TabBar]'s
@@ -384,10 +384,16 @@ impl Pane {
// Instead we need to replicate the spacing from the [TabBar]'s `end_slot` here.
.gap(Spacing::Small.rems(cx))
.child(
IconButton::new("plus", IconName::Plus)
.icon_size(IconSize::Small)
.on_click(cx.listener(|pane, _, cx| {
let menu = ContextMenu::build(cx, |menu, _| {
PopoverMenu::new("pane-tab-bar-popover-menu")
.trigger(
IconButton::new("plus", IconName::Plus)
.icon_size(IconSize::Small)
.tooltip(|cx| Tooltip::text("New...", cx)),
)
.anchor(AnchorCorner::TopRight)
.with_handle(pane.new_item_context_menu_handle.clone())
.menu(move |cx| {
Some(ContextMenu::build(cx, |menu, _| {
menu.action("New File", NewFile.boxed_clone())
.action(
"Open File",
@@ -407,37 +413,27 @@ impl Pane {
)
.separator()
.action("New Terminal", NewTerminal.boxed_clone())
});
cx.subscribe(&menu, |pane, _, _: &DismissEvent, cx| {
pane.focus(cx);
pane.new_item_menu = None;
})
.detach();
pane.new_item_menu = Some(menu);
}))
.tooltip(|cx| Tooltip::text("New...", cx)),
}))
}),
)
.when_some(pane.new_item_menu.as_ref(), |el, new_item_menu| {
el.child(Self::render_menu_overlay(new_item_menu))
})
.child(
IconButton::new("split", IconName::Split)
.icon_size(IconSize::Small)
.on_click(cx.listener(|pane, _, cx| {
let menu = ContextMenu::build(cx, |menu, _| {
PopoverMenu::new("pane-tab-bar-split")
.trigger(
IconButton::new("split", IconName::Split)
.icon_size(IconSize::Small)
.tooltip(|cx| Tooltip::text("Split Pane", cx)),
)
.anchor(AnchorCorner::TopRight)
.with_handle(pane.split_item_context_menu_handle.clone())
.menu(move |cx| {
ContextMenu::build(cx, |menu, _| {
menu.action("Split Right", SplitRight.boxed_clone())
.action("Split Left", SplitLeft.boxed_clone())
.action("Split Up", SplitUp.boxed_clone())
.action("Split Down", SplitDown.boxed_clone())
});
cx.subscribe(&menu, |pane, _, _: &DismissEvent, cx| {
pane.focus(cx);
pane.split_item_menu = None;
})
.detach();
pane.split_item_menu = Some(menu);
}))
.tooltip(|cx| Tooltip::text("Split Pane", cx)),
.into()
}),
)
.child({
let zoomed = pane.is_zoomed();
@@ -456,9 +452,6 @@ impl Pane {
)
})
})
.when_some(pane.split_item_menu.as_ref(), |el, split_item_menu| {
el.child(Self::render_menu_overlay(split_item_menu))
})
.into_any_element()
.into();
(None, right_children)
@@ -469,6 +462,8 @@ impl Pane {
_subscriptions: subscriptions,
double_click_dispatch_action,
save_modals_spawned: HashSet::default(),
split_item_context_menu_handle: Default::default(),
new_item_context_menu_handle: Default::default(),
}
}
@@ -552,11 +547,9 @@ impl Pane {
}
}
fn context_menu_focused(&self, cx: &mut ViewContext<Self>) -> bool {
self.new_item_menu
.as_ref()
.or(self.split_item_menu.as_ref())
.map_or(false, |menu| menu.focus_handle(cx).is_focused(cx))
pub fn context_menu_focused(&self, cx: &mut ViewContext<Self>) -> bool {
self.new_item_context_menu_handle.is_focused(cx)
|| self.split_item_context_menu_handle.is_focused(cx)
}
fn focus_out(&mut self, _event: FocusOutEvent, cx: &mut ViewContext<Self>) {
@@ -2254,6 +2247,12 @@ impl Render for Pane {
}))
.on_action(cx.listener(|pane, _: &SplitLeft, cx| pane.split(SplitDirection::Left, cx)))
.on_action(cx.listener(|pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx)))
.on_action(cx.listener(|pane, _: &SplitHorizontal, cx| {
pane.split(SplitDirection::horizontal(cx), cx)
}))
.on_action(cx.listener(|pane, _: &SplitVertical, cx| {
pane.split(SplitDirection::vertical(cx), cx)
}))
.on_action(
cx.listener(|pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx)),
)

View File

@@ -1,4 +1,8 @@
use crate::{pane_group::element::pane_axis, AppState, FollowerState, Pane, Workspace};
use crate::{
pane_group::element::pane_axis,
workspace_settings::{PaneSplitDirectionHorizontal, PaneSplitDirectionVertical},
AppState, FollowerState, Pane, Workspace, WorkspaceSettings,
};
use anyhow::{anyhow, Result};
use call::{ActiveCall, ParticipantLocation};
use client::proto::PeerId;
@@ -10,6 +14,7 @@ use gpui::{
use parking_lot::Mutex;
use project::Project;
use serde::Deserialize;
use settings::Settings;
use std::sync::Arc;
use ui::prelude::*;
@@ -561,6 +566,20 @@ impl SplitDirection {
[Self::Up, Self::Down, Self::Left, Self::Right]
}
pub fn vertical(cx: &WindowContext) -> Self {
match WorkspaceSettings::get_global(cx).pane_split_direction_vertical {
PaneSplitDirectionVertical::Left => SplitDirection::Left,
PaneSplitDirectionVertical::Right => SplitDirection::Right,
}
}
pub fn horizontal(cx: &WindowContext) -> Self {
match WorkspaceSettings::get_global(cx).pane_split_direction_horizontal {
PaneSplitDirectionHorizontal::Down => SplitDirection::Down,
PaneSplitDirectionHorizontal::Up => SplitDirection::Up,
}
}
pub fn edge(&self, rect: Bounds<Pixels>) -> Pixels {
match self {
Self::Up => rect.origin.y,

View File

@@ -55,7 +55,9 @@ pub use persistence::{
WorkspaceDb, DB as WORKSPACE_DB,
};
use postage::stream::Stream;
use project::{DirectoryLister, Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
use project::{
DirectoryLister, Project, ProjectEntryId, ProjectPath, ResolvedPath, Worktree, WorktreeId,
};
use serde::Deserialize;
use session::AppSession;
use settings::Settings;
@@ -134,6 +136,8 @@ actions!(
FollowNextCollaborator,
NewCenterTerminal,
NewFile,
NewFileSplitVertical,
NewFileSplitHorizontal,
NewSearch,
NewTerminal,
NewWindow,
@@ -166,9 +170,6 @@ pub struct ActivatePaneInDirection(pub SplitDirection);
#[derive(Clone, Deserialize, PartialEq)]
pub struct SwapPaneInDirection(pub SplitDirection);
#[derive(Clone, Deserialize, PartialEq)]
pub struct NewFileInDirection(pub SplitDirection);
#[derive(Clone, PartialEq, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SaveAll {
@@ -218,7 +219,6 @@ impl_actions!(
ActivatePaneInDirection,
CloseAllItemsAndPanes,
CloseInactiveTabsAndPanes,
NewFileInDirection,
OpenTerminal,
Reload,
Save,
@@ -2015,6 +2015,17 @@ impl Workspace {
})
}
pub fn open_resolved_path(
&mut self,
path: ResolvedPath,
cx: &mut ViewContext<Self>,
) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
match path {
ResolvedPath::ProjectPath(project_path) => self.open_path(project_path, None, true, cx),
ResolvedPath::AbsPath(path) => self.open_abs_path(path, false, cx),
}
}
fn add_folder_to_project(&mut self, _: &AddFolderToProject, cx: &mut ViewContext<Self>) {
let project = self.project.read(cx);
if project.is_remote() && project.dev_server_project_id().is_none() {

View File

@@ -8,6 +8,8 @@ use settings::{Settings, SettingsSources};
#[derive(Deserialize)]
pub struct WorkspaceSettings {
pub active_pane_magnification: f32,
pub pane_split_direction_horizontal: PaneSplitDirectionHorizontal,
pub pane_split_direction_vertical: PaneSplitDirectionVertical,
pub centered_layout: CenteredLayoutSettings,
pub confirm_quit: bool,
pub show_call_status_icon: bool,
@@ -61,6 +63,14 @@ pub struct WorkspaceSettingsContent {
///
/// Default: `1.0`
pub active_pane_magnification: Option<f32>,
// Direction to split horizontally.
//
// Default: "up"
pub pane_split_direction_horizontal: Option<PaneSplitDirectionHorizontal>,
// Direction to split vertically.
//
// Default: "left"
pub pane_split_direction_vertical: Option<PaneSplitDirectionVertical>,
// Centered layout related settings.
pub centered_layout: Option<CenteredLayoutSettings>,
/// Whether or not to prompt the user to confirm before closing the application.
@@ -131,6 +141,20 @@ pub enum AutosaveSetting {
OnWindowChange,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum PaneSplitDirectionHorizontal {
Up,
Down,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum PaneSplitDirectionVertical {
Left,
Right,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub struct CenteredLayoutSettings {

View File

@@ -97,6 +97,7 @@ tab_switcher.workspace = true
supermaven.workspace = true
task.workspace = true
tasks_ui.workspace = true
time.workspace = true
telemetry_events.workspace = true
terminal_view.workspace = true
theme.workspace = true

View File

@@ -8,6 +8,7 @@ mod zed;
use anyhow::{anyhow, Context as _, Result};
use assistant::PromptBuilder;
use chrono::Offset;
use clap::{command, Parser};
use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
use client::{parse_zed_link, Client, DevServerToken, UserStore};
@@ -44,6 +45,7 @@ use std::{
sync::Arc,
};
use theme::{ActiveTheme, SystemAppearance, ThemeRegistry, ThemeSettings};
use time::UtcOffset;
use util::{maybe, parse_env_output, ResultExt, TryFutureExt};
use uuid::Uuid;
use welcome::{show_welcome_view, BaseKeymap, FIRST_OPEN};
@@ -886,7 +888,10 @@ fn init_logger() {
let mut config_builder = ConfigBuilder::new();
config_builder.set_time_format_rfc3339();
config_builder.set_time_offset_to_local().log_err();
let local_offset = chrono::Local::now().offset().fix().local_minus_utc();
if let Ok(offset) = UtcOffset::from_whole_seconds(local_offset) {
config_builder.set_time_offset(offset);
}
#[cfg(target_os = "linux")]
{

View File

@@ -21,3 +21,4 @@ enable = false
"/ruby.html" = "/docs/languages/ruby.html"
"/python.html" = "/docs/languages/python.html"
"/adding-new-languages.html" = "/docs/extensions/languages.html"
"/language-model-integration.html" = "/docs/assistant/assistant.html"

View File

@@ -6,10 +6,10 @@ Slash commands enhance the assistant's capabilities. Begin by typing a `/` at th
- `/default`: Inserts the default prompt into the context
- `/diagnostics`: Injects errors reported by the project's language server into the context
- `/fetch`: Inserts the content of a webpage and inserts it into the context
- `/fetch`: Fetches the content of a webpage and inserts it into the context
- `/file`: Inserts a single file or a directory of files into the context
- `/now`: Inserts the current date and time into the context
- `/prompt`: Adds a custom-configured prompt to the context (see Prompt Library)
- `/prompt`: Adds a custom-configured prompt to the context ([see Prompt Library](/docs/assistant/prompting#prompt-library))
- `/symbols`: Inserts the current tab's active symbols into the context
- `/tab`: Inserts the content of the active tab or all open tabs into the context
- `/terminal`: Inserts a select number of lines of output from the terminal

View File

@@ -13,7 +13,7 @@ The following providers are supported:
To configure different providers, run `assistant: show configuration` in the command palette, or click on the hamburger menu at the top-right of the assistant panel and select "Configure".
[^1]: This provider does not support [`/workflow`](./commands#workflow-not-generally-available) command.
[^1]: This provider does not support the [`/workflow`](./commands#workflow-not-generally-available) command.
To further customize providers, you can use `settings.json` to do that as follows:
@@ -29,7 +29,7 @@ A hosted service providing convenient and performant support for AI-enabled codi
You can use Claude 3.5 Sonnet via [Zed AI](#zed-ai) for free. To use other Anthropic models you will need to configure it by providing your own API key.
1. You can obtain an API key [here](https://console.anthropic.com/settings/keys).
1. Sign up for Anthropic and [create an API key](https://console.anthropic.com/settings/keys)
2. Make sure that your Anthropic account has credits
3. Open the configuration view (`assistant: show configuration`) and navigate to the Anthropic section
4. Enter your Anthropic API key
@@ -40,7 +40,7 @@ Zed will also use the `ANTHROPIC_API_KEY` environment variable if it's defined.
#### Anthropic Custom Models {#anthropic-custom-models}
You can add custom models to the Anthropic provider, by adding the following to your Zed `settings.json`:
You can add custom models to the Anthropic provider by adding the following to your Zed `settings.json`:
```json
{
@@ -75,8 +75,8 @@ You can use GitHub Copilot chat with the Zed assistant by choosing it via the mo
You can use Gemini 1.5 Pro/Flash with the Zed assistant by choosing it via the model dropdown in the assistant panel.
1. Create an API key [here](https://aistudio.google.com/app/apikey).
2. Open the configuration view (`assistant: show configuration`) and navigate to the OpenAI section
1. Go the Google AI Studio site and [create an API key](https://aistudio.google.com/app/apikey).
2. Open the configuration view (`assistant: show configuration`) and navigate to the Google AI section
3. Enter your Google AI API key
The Google AI API key will be saved in your keychain.
@@ -85,7 +85,7 @@ Zed will also use the `GOOGLE_AI_API_KEY` environment variable if it's defined.
#### Google AI custom models {#google-ai-custom-models}
You can add custom models to the GoogleAI provider, by adding the following to your Zed `settings.json`:
You can add custom models to the Google AI provider by adding the following to your Zed `settings.json`:
```json
{
@@ -137,7 +137,7 @@ You can use Ollama with the Zed assistant by making Ollama appear as an OpenAPI
### OpenAI {#openai}
1. Create an [OpenAI API key](https://platform.openai.com/account/api-keys)
1. Visit the OpenAI platform and [create an API key](https://platform.openai.com/account/api-keys)
2. Make sure that your OpenAI account has credits
3. Open the configuration view (`assistant: show configuration`) and navigate to the OpenAI section
4. Enter your OpenAI API key
@@ -244,7 +244,7 @@ You can also manually edit the `default_model` object in your settings:
| key | type | default | description |
| -------------- | ------- | ------- | ------------------------------------------------------------------------------------- |
| enabled | boolean | true | Setting this to `false` will completely disable the assistant |
| button | boolean | true | Show the assistant icon |
| button | boolean | true | Show the assistant icon in the status bar |
| dock | string | "right" | The default dock position for the assistant panel. Can be ["left", "right", "bottom"] |
| default_height | string | null | The pixel height of the assistant panel when docked to the bottom |
| default_width | string | null | The pixel width of the assistant panel when docked to the left or right |

View File

@@ -1264,6 +1264,7 @@ List of `integer` column numbers
"font_family": null,
"font_features": null,
"font_size": null,
"line_height": "comfortable",
"option_as_meta": true,
"button": false,
"shell": {},
@@ -1447,6 +1448,46 @@ See Buffer Font Features
}
```
### Terminal: Line Height
- Description: Set the terminal's line height.
- Setting: `line_height`
- Default: `comfortable`
**Options**
1. Use a line height that's `comfortable` for reading, 1.618. (default)
```jsonc
{
"terminal": {
"line_height": "comfortable",
},
}
```
2. Use a `standard` line height, 1.3. This option is useful for TUIs, particularly if they use box characters
```jsonc
{
"terminal": {
"line_height": "standard",
},
}
```
3. Use a custom line height.
```jsonc
{
"terminal": {
"line_height": {
"custom": 2,
},
},
}
```
### Terminal: Option As Meta
- Description: Re-interprets the option keys to act like a 'meta' key, like in Emacs.
@@ -1789,7 +1830,7 @@ Run the `theme selector: toggle` action in the command palette to see a current
- Description: Whether to fold directories automatically when directory has only one directory inside.
- Setting: `auto_fold_dirs`
- Default: `false`
- Default: `true`
**Options**

View File

@@ -15,7 +15,7 @@ For example, if you have Prettier installed and on your `PATH`, you can use it t
{
"languages": {
"JavaScript": {
"format_on_save": {
"formatter": {
"external": {
"command": "prettier",
"arguments": ["--stdin-filepath", "{buffer_path}"]

View File

@@ -1,7 +1,5 @@
# Telemetry in Zed
**Up to date with v0.112.0**
Zed collects anonymous telemetry data to help the team understand how people are using the application and to see what sort of issues they are experiencing.
## Configuring Telemetry Settings
@@ -31,118 +29,25 @@ Telemetry is sent from the application to our servers. Data is proxied through o
Diagnostic events include debug information (stack traces) from crash reports. Reports are sent on the first application launch after the crash occurred. We've built dashboards that allow us to visualize the frequency and severity of issues experienced by users. Having these reports sent automatically allows us to begin implementing fixes without the user needing to file a report in our issue tracker. The plots in the dashboards also give us an informal measurement of the stability of Zed.
When a panic occurs, the following data is sent:
You can see what data is sent when a panic occurs by inspecting the `Panic` struct in [crates/telemetry_events/src/telemetry_events.rs](https://github.com/zed-industries/zed/blob/main/crates/telemetry_events/src/telemetry_events.rs#L184) in the zed repo. You can find additional information in the [Debugging Crashes](https://zed.dev/docs/development/debugging-crashes) documentation.
#### PanicRequest
### Usage Data (Metrics) {#metrics}
- `panic`: The panic data
- `token`: An identifier that is used to authenticate the request on zed.dev
To improve Zed and understand how it is being used in the wild, Zed optionally collects usage data like the following:
#### Panic
- (a) file extensions of opened files;
- (b) features and tools You use within the Editor;
- (c) project statistics (e.g., number of files); and
- (d) frameworks detected in Your projects
- `thread`: The name of the thread that panicked
- `payload`: The panic message
- `location_data`: The location of the panic
- `file`
- `line`
- `backtrace`: The backtrace of the panic
- `app_version`: Zed's app version
- `release_channel`: Zed's release channel
- `stable`
- `preview`
- `dev`
- `os_name`: The name of your operating system
- `os_version`: The version of your operating system
- `architecture`: The architecture of your CPU
- `panicked_on`: The time that the panic occurred
- `installation_id`: An identifier that is unique to each installation of Zed (this differs for stable, preview, and dev builds)
- `session_id`: An identifier that is unique to each Zed session (this differs for each time you open Zed)
Usage Data does not include any of Your software code or sensitive project details. Metric events are reported over HTTPS, and requests are rate-limited to avoid using significant network bandwidth.
### Metrics
Zed also collects metric information based on user actions. Metric events are reported over HTTPS, and requests are rate-limited to avoid using significant network bandwidth. All data remains anonymous, and can't be related to specific Zed users.
The following data is sent:
#### ClickhouseEventRequestBody
- `token`: An identifier that is used to authenticate the request on zed.dev
- `installation_id`: An identifier that is unique to each installation of Zed (this differs for stable, preview, and dev builds)
- `session_id`: An identifier that is unique to each Zed session (this differs for each time you open Zed)
- `is_staff`: A boolean that indicates whether the user is a member of the Zed team or not
- `app_version`: Zed's app version
- `os_name`: The name of your operating system
- `os_version`: The version of your operating system
- `architecture`: The architecture of your CPU
- `release_channel`: Zed's release channel
- `stable`
- `preview`
- `dev`
- `events`: A vector of `ClickhouseEventWrapper`s
#### ClickhouseEventWrapper
- `signed_in`: A boolean that indicates whether the user is signed in or not
- `event`: An enum, where each variant can be one of the following `ClickhouseEvent` variants:
#### ClickhouseEvent
- `editor`
- `operation`: The editor operation that was performed
- `open`
- `save`
- `file_extension`: The extension of the file that was opened or saved
- `vim_mode`: A boolean that indicates whether the user is in vim mode or not
- `copilot_enabled`: A boolean that indicates whether the user has copilot enabled or not
- `copilot_enabled_for_language`: A boolean that indicates whether the user has copilot enabled for the language of the file that was opened or saved
- `milliseconds_since_first_event`: Duration of time between this event's timestamp and the timestamp of the first event in the current batch
- `copilot`
- `suggestion_accepted`: A boolean that indicates whether the suggestion was accepted or not
- `file_extension`: The file extension of the file that was opened or saved
- `milliseconds_since_first_event`: Same as above
- `call`
- `operation`: The call operation that was performed
- `accept incoming`
- `decline incoming`
- `disable microphone`
- `disable screen share`
- `enable microphone`
- `enable screen share`
- `hang up`
- `invite`
- `join channel`
- `open channel notes`
- `share project`
- `unshare project`
- `room_id`: The ID of the room
- `channel_id`: The ID of the channel
- `milliseconds_since_first_event`: Same as above
- `assistant`
- `conversation_id`: The ID of the conversation (for panel events only)
- `kind`: An enum with the following variants:
- `panel`
- `inline`
- `model`: The model that was used
- `milliseconds_since_first_event`: Same as above
- `cpu`
- `usage_as_percentage`: The CPU usage
- `core_count`: The number of cores on the CPU
- `milliseconds_since_first_event`: Same as above
- `memory`
- `memory_in_bytes`: The amount of memory used in bytes
- `virtual_memory_in_bytes`: The amount of virtual memory used in bytes
- `milliseconds_since_first_event`: Same as above
- `app`
- `operation`: The app operation that was performed
- `first open`
- `open`
- `close`
- `milliseconds_since_first_event`: Same as above
Usage Data is associated with a secure random telemetry ID which may be linked to Your email address. This linkage currently serves two purposes: (1) it allows Zed to analyze usage patterns over time while maintaining Your privacy; and (2) it enables Zed to reach out to specific user groups for feedback and improvement suggestions.
You can audit the metrics data that Zed has reported by running the command `zed: open telemetry log` from the command palette, or clicking `Help > View Telemetry Log` in the application menu.
The telemetry settings can also be configured via the `welcome` screen, which can be invoked via the `workspace: welcome` action in the command palette.
You can see the full list of the event types and exactly the data sent for each by inspecting the `Event` enum and the associated structs in [crates/telemetry_events/src/telemetry_events.rs](https://github.com/zed-industries/zed/blob/main/crates/telemetry_events/src/telemetry_events.rs#L63] in the zed repo.
### Concerns and Questions
## Concerns and Questions
If you have concerns about telemetry, please feel free to open issues in our [Zed repository](https://github.com/zed-industries/zed/issues/new/choose).

View File

@@ -1,6 +1,6 @@
[package]
name = "zed_zig"
version = "0.2.0"
version = "0.3.0"
edition = "2021"
publish = false
license = "Apache-2.0"

View File

@@ -1,7 +1,7 @@
id = "zig"
name = "Zig"
description = "Zig support."
version = "0.2.0"
version = "0.3.0"
schema_version = 1
authors = ["Allan Calix <contact@acx.dev>"]
repository = "https://github.com/zed-industries/zed"

View File

@@ -61,39 +61,40 @@ impl ZigExtension {
&language_server_id,
&zed::LanguageServerInstallationStatus::CheckingForUpdate,
);
// We're pinning ZLS to a release that has `.tar.gz` assets, since the latest release does not have
// them, at time of writing.
//
// ZLS tracking issue: https://github.com/zigtools/zls/issues/1879
let release = zed::github_release_by_tag_name("zigtools/zls", "0.11.0")?;
let asset_name = format!(
"zls-{arch}-{os}.{extension}",
arch = match arch {
zed::Architecture::Aarch64 => "aarch64",
zed::Architecture::X86 => "x86",
zed::Architecture::X8664 => "x86_64",
// Note that in github releases and on zlstools.org the tar.gz asset is not shown
// but is available at https://builds.zigtools.org/zls-{os}-{arch}-{version}.tar.gz
let release = zed::latest_github_release(
"zigtools/zls",
zed::GithubReleaseOptions {
require_assets: true,
pre_release: false,
},
os = match platform {
zed::Os::Mac => "macos",
zed::Os::Linux => "linux",
zed::Os::Windows => "windows",
},
extension = match platform {
zed::Os::Mac | zed::Os::Linux => "tar.gz",
zed::Os::Windows => "zip",
}
);
)?;
let asset = release
.assets
.iter()
.find(|asset| asset.name == asset_name)
.ok_or_else(|| format!("no asset found matching {:?}", asset_name))?;
let arch: &str = match arch {
zed::Architecture::Aarch64 => "aarch64",
zed::Architecture::X86 => "x86",
zed::Architecture::X8664 => "x86_64",
};
let os: &str = match platform {
zed::Os::Mac => "macos",
zed::Os::Linux => "linux",
zed::Os::Windows => "windows",
};
let extension: &str = match platform {
zed::Os::Mac | zed::Os::Linux => "tar.gz",
zed::Os::Windows => "zip",
};
let asset_name: String = format!("zls-{}-{}-{}.{}", os, arch, release.version, extension);
let download_url = format!("https://builds.zigtools.org/{}", asset_name);
let version_dir = format!("zls-{}", release.version);
let binary_path = match platform {
zed::Os::Mac | zed::Os::Linux => format!("{version_dir}/bin/zls"),
zed::Os::Mac | zed::Os::Linux => format!("{version_dir}/zls"),
zed::Os::Windows => format!("{version_dir}/zls.exe"),
};
@@ -104,7 +105,7 @@ impl ZigExtension {
);
zed::download_file(
&asset.download_url,
&download_url,
&version_dir,
match platform {
zed::Os::Mac | zed::Os::Linux => zed::DownloadedFileType::GzipTar,

View File

@@ -1,39 +0,0 @@
# SWE-Bench eval
## Zed "agent" flow
- Spin up Zed
- Open project with repo at `base_commit`
- Open assistant panel
- Add /workflow to context (e.g. in System message)
- (Out of band) LLM call to rephrase SWE-bench `problem_statement` into a Zed Assistant user query/prompt
- Trying to simulate user prompt here
- `user_query = rephrase(problem_statement)`
- Add `/auto {user_query}` to populate context
- Store benchmark outputs:
- `/file` calls + `/search` output
- Overlap of these files/snippets with `patch`
- [Stretch]: Overlap of these files/snippets within one-hop of `patch` (hop resolved via LSP go-to-impl call)
- Add `user_query` at end of assistant context
- Run assistant on context
- Apply workflow step resolution
- Apply inline-edits
- Store benchmark outputs:
- Success/failure of step resolution
- Success/failure of "proper" indentation of inline edit
- Success/failure of "overgeneration" of inline edit
- Finally, run tests from test_patch, observe results of `PASS_TO_PASS` + `FAIL_TO_PASS` tests
- Store benchmark outputs:
- Number of patch files modified: all/any/none
- Success/failure of `PASS_TO_PASS` + `FAIL_TO_PASS` tests
## Things to Report
- Rephrased user query (for test case validity)
### /workflow
- Step resolution: OK/fail
- Proper indents in inline edits: OK/fail per edit
- Overgeneration in inline edits: OK/fail per edit
- Number of patch files modified: all/any/none
- Success/failure of `PASS_TO_PASS` + `FAIL_TO_PASS` tests: OK/fail
### /auto {problem_statement}:
- Overlap of `/file` + `/search` outputs with `patch` snippets
- Overlap of these files/snippets within one-hop of `patch`

View File

@@ -1,30 +0,0 @@
# %%
import polars as pl
df = pl.read_parquet('hf://datasets/princeton-nlp/SWE-bench_Verified/data/test-00000-of-00001.parquet')
print(df.head())
print(df.columns)
print(len(df))
# Inspect the head of specific columns
df.select(['repo', 'problem_statement', 'test_patch', 'hints_text']).head()
full_row = df.head(1).to_dict(as_series=False)
import pprint
pp = pprint.PrettyPrinter(indent=4)
print("Repo:")
pp.pprint(full_row['repo'])
print("\nPatch:")
pp.pprint(full_row['patch'])
print("\nTest Patch:")
pp.pprint(full_row['test_patch'])
print("\nProblem Statement:")
pp.pprint(full_row['problem_statement'])
print("\nHints Text:")
pp.pprint(full_row['hints_text'])
print("\nPASS_TO_PASS:")
pp.pprint(full_row['PASS_TO_PASS'])
print("\nFAIL_TO_PASS:")
pp.pprint(full_row['FAIL_TO_PASS'])