Compare commits

...

54 Commits

Author SHA1 Message Date
Zed Bot
0000569e4a Bump to 0.159.7 for @ConradIrwin 2024-11-01 02:49:42 +00:00
Conrad Irwin
2353db0f47 Fix trigger release? 2024-10-31 20:31:12 -06:00
Conrad Irwin
061a8cf844 SSHHELL escaping.... (#20046)
Closes #20027
Closes #19976 (again)

Release Notes:

- Remoting: Fixed remotes with non-sh/bash/zsh default shells
- Remoting: Fixed remotes running busybox's version of gunzip
2024-10-31 16:15:50 -06:00
Peter Tripp
4d1030fd76 zed 0.159.6 2024-10-31 07:44:12 -04:00
Kyle Kelley
614f6ef04f Trim whitespace from base64 encoded image data before decoding it (#19977)
Closes #17956
Closes #16330

This fix is for both REPL (released) and notebook (unreleased)

<img width="1210" alt="image"
src="https://github.com/user-attachments/assets/bd046f0f-3ad1-4c25-b3cb-114e008c2a69">

Release Notes:

- Fixed image support in REPL for certain versions of matplotlib that
included preceding and/or trailing whitespace in the base64 image data
2024-10-31 07:43:50 -04:00
Conrad Irwin
b519fd5b2d Robustify download on remote (#19983)
Closes #19976
Closes #19972

We now prefer curl to wget (as it supports socks5:// proxies) and pass
-f to
curl so it fails; and use sh instead of bash, which should have more
consistent
behaviour across systems

Release Notes:

- SSH Remoting: make downloading binary on remote more reliable.

---------

Co-authored-by: Will <will@zed.dev>
2024-10-31 07:43:46 -04:00
Conrad Irwin
0376285213 Fail download if download fails (#19990)
Co-Authored-By: Mikayla <mikayla@zed.dev>

Release Notes:

- Remoting: Fixes a bug where we could cache an HTML error page as a
binary

Co-authored-by: Mikayla <mikayla@zed.dev>
2024-10-31 07:43:42 -04:00
Peter Tripp
bcf6806a3f v0.159.x stable 2024-10-30 11:06:24 -04:00
Peter Tripp
fe1f84152b zed 0.159.5 2024-10-30 10:14:42 -04:00
Thorsten Ball
e3b7e5e103 remote dev: Always upload binary in development mode (#19953)
Release Notes:

- N/A
2024-10-30 09:27:01 -04:00
Kirill Bulatov
ed4792d4fc Log prettier errors on failures (#19951)
Closes https://github.com/zed-industries/zed/issues/11987

Release Notes:

- Fixed prettier not reporting failures in the status panel on
formatting and installation errors
2024-10-30 09:22:44 -04:00
Thorsten Ball
8172bb38a8 remote dev: Allow canceling language server work in editor (#19946)
Release Notes:

- Added ability to cancel language server work in remote development.

Demo:



https://github.com/user-attachments/assets/c9ca91a5-617f-4886-a458-87c563c5a247
2024-10-30 09:22:00 -04:00
Mikayla Maki
161c14c9f2 Implement panic reporting saving and uploads (#19932)
TODO: 
- [x] check that the app version is well formatted for zed.dev

Release Notes:

- N/A

---------

Co-authored-by: Trace <violet.white.batt@gmail.com>
2024-10-30 09:21:26 -04:00
Max Brunsfeld
05f797f0c9 Fix missing diagnostic and text highlights after blocks (#19920)
Release Notes:

- Fixed an issue where diagnostic underlines and certain text highlights
were not rendered correctly below block decorations such as the inline
assistant prompt.

Co-authored-by: Antonio <antonio@zed.dev>
Co-authored-by: Richard <richard@zed.dev>
2024-10-29 13:48:57 -07:00
Conrad Irwin
a4b818ec6b Better handle interrupted connections for shared SSH (#19925)
Co-Authored-By: Mikayla <mikayla@zed.dev>
2024-10-29 16:44:11 -04:00
Peter Tripp
7dadb5b630 zed 0.159.4 2024-10-29 16:06:22 -04:00
Conrad Irwin
a8a690ebc1 SSH Remoting: Fix diagnostic summary syncing (#19923)
Co-Authored-By: Mikayla <mikayla@zed.dev>

Release Notes:

- SSH Remoting: Fix diagnostics summary over collab

Co-authored-by: Mikayla <mikayla@zed.dev>
2024-10-29 16:04:08 -04:00
Mikayla Maki
8e251e3507 Fix the log spam from the BlameBuffer request (#19921)
Release Notes:

- N/A
2024-10-29 16:03:33 -04:00
Mikayla Maki
a776c00753 Fix a rare crash on startup (#19922)
Release Notes:

- Fixed a rare crash that could happen when certain SQL statements are
prepared
2024-10-29 15:49:33 -04:00
Conrad Irwin
11d31e5b0e Fix quotes in Rust (#19914)
Release Notes:

- (preview only) Fixed quote-autoclose in Rust
2024-10-29 14:36:21 -04:00
Joseph T. Lyons
eb806b93c2 Fix Julia icon extension lookup (#19916)
Release Notes:

- Fixed a bug where the Julia icon was not displayed for Julia files.
2024-10-29 14:20:13 -04:00
Conrad Irwin
febd55d887 Fix wrong UpdateWorktree chunk size being used in release mode (#19912)
Release Notes:

- Fixed slowness when collaborating

Co-authored-by: Thorsten <thorsten@zed.dev>
2024-10-29 13:31:06 -04:00
Peter Tripp
a6a8e46062 zed 0.159.3 2024-10-29 12:05:52 -04:00
Kirill Bulatov
efc1840594 Ensure shared ssh project propagates buffer changes to all participants (#19907)
Fixed the bug when shared ssh project did not account for client
changing things in their buffers.
Also ensures Prettier formatting workflow works for both ssh project
owner and ssh project clients.

Release Notes:

- N/A

---------

Co-authored-by: Conrad Irwin <conrad@zed.dev>
2024-10-29 12:04:19 -04:00
Mikayla Maki
cd1eaa1971 Add more context to the save new file path picker (#19863)
Release Notes:

- N/A

Co-authored-by: Conrad <conrad@zed.dev>
2024-10-29 12:03:19 -04:00
Jen Stehlik
cfcd398e8d Add Gleam icon (#19887)
I took a shot at creating an icon version of the Gleam logo in response
to https://github.com/zed-industries/zed/pull/19529

Release Notes:

- Added an icon for Gleam files.


![image](https://github.com/user-attachments/assets/97432ded-342f-4d87-8eb2-dc9145513d8c)

<img width="231" alt="Screenshot 2024-10-29 at 9 46 33 AM"
src="https://github.com/user-attachments/assets/c957c98f-3da0-4b92-bc21-2a5adca1daa3">

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-10-29 10:12:23 -04:00
Thorsten Ball
cca65c85ac remote servers: Fix title from alpha to beta (#19889)
Discussed this in Slack yesterday. We use `beta` because that's what we
use in the docs as well.

Release Notes:

- N/A
2024-10-29 09:41:54 -04:00
Bennet Bo Fenner
ad6d20ec4f vsc menu: Fix issue when switching branch while non-visible worktree is open (#19888)
Fixes a regression introduced in #19755

<img width="935" alt="Screenshot 2024-10-29 at 12 13 04"
src="https://github.com/user-attachments/assets/7699b8da-631d-4932-89a8-bc5d7f2546f1">

Co-Authored-by: Thorsten <thorsten@zed.dev>

Release Notes:

- Fixed an issue where the branch switcher would show an error, when
opening a file outside of the project

Co-authored-by: Thorsten <thorsten@zed.dev>
2024-10-29 09:40:50 -04:00
Bennet Bo Fenner
1f44281933 ssh remoting: Hide share button while connecting to project (#19885)
Co-Authored-by: Thorsten <thorsten@zed.dev>

Release Notes:

- N/A

Co-authored-by: Thorsten <thorsten@zed.dev>
2024-10-29 09:40:05 -04:00
Bennet Bo Fenner
7d4a643b16 ssh remoting: Check nightly version correctly by comparing commit SHA (#19884)
This ensures that we detect if a new nightly version of the remote
server is available.
Previously we would always mark a version as matching if they had the
same semantic version.
However, for nightly versions we also need to check if they have the
same commit SHA.

Co-Authored-by: Thorsten <thorsten@zed.dev>

Release Notes:

- N/A

---------

Co-authored-by: Thorsten <thorsten@zed.dev>
2024-10-29 09:40:01 -04:00
Bennet Bo Fenner
91ee7f4000 ssh remoting: Show the host's GitHub name in the titlebar when sharing an SSH project (#19844)
The name (GitHub name) of the host was not displayed when sharing an ssh
project.

Previously we assumed that the a collaborator is a host if the
`replica_id` of the collaborator was `0`,
but for ssh project the `replica_id` is actually `1`.

<img width="329" alt="Screenshot 2024-10-28 at 18 16 30"
src="https://github.com/user-attachments/assets/c0151e12-a96f-4f38-aec1-4ed5475a9eaf">


Co-Authored-by: Thorsten <thorsten@zed.dev>

Release Notes:

- N/A

---------

Co-authored-by: Thorsten <thorsten@zed.dev>
2024-10-29 09:38:04 -04:00
Bennet Bo Fenner
874b4b8d55 Future-proof indent guides settings for panels (#19878)
This PR ensures that we do not have to break the indent guides settings
for the project/outline panel. In the future we might want to have a
more granular way to control when to show indent guides, or control
other indent guide properties, like its width.

Release Notes:

- N/A
2024-10-29 09:37:59 -04:00
Joseph T. Lyons
9cdccb2308 restore editor::UnfoldRecursive binding (#19865) 2024-10-29 09:36:04 -04:00
Piotr Osiewicz
70d6ad5a43 language_selector: Fix debug_assert firing off on context menu creation for LSP view (#19854)
Closes #ISSUE

Release Notes:

- N/A
2024-10-29 09:24:33 -04:00
Mikayla Maki
6a58d8c2b1 Fix mouse clicks on remote-open-folder UI (#19851)
Also change Zed's standard style to use
`.track_focus(&self.focus_handle(cx))`, instead of
`.track_focus(&self.focus_handle)`, to catch these kinds of errors more
easily in the future.

Release Notes:

- N/A

---------

Co-authored-by: Conrad <conrad@zed.dev>
2024-10-28 16:01:13 -04:00
Peter Tripp
d641115d19 zed 0.159.2 2024-10-28 15:50:42 -04:00
Peter Tripp
c300c1d600 Merge branch 'main' into v0.159.x 2024-10-28 15:49:46 -04:00
Kirill Bulatov
fd3b5ff0d6 Fix the tests
These tests were removed in Preview, but that commit was not yet cherry-picked here.
2024-10-28 07:54:56 +02:00
Kirill Bulatov
9517964a04 Fixed outline panel panicking on filtering (#19811)
Closes https://github.com/zed-industries/zed/issues/19732

Release Notes:

- Fixed outline panel panicking on filtering
([#19732](https://github.com/zed-industries/zed/issues/19732))
2024-10-28 02:31:18 +02:00
Kirill Bulatov
0798c642d4 Restore horizontal scrollbar checks (#19767)
Closes https://github.com/zed-industries/zed/issues/19637

Follow-up of https://github.com/zed-industries/zed/pull/18927 , restores
the condition that removed the horizontal scrollbar when panel's items
are not long enough.

Release Notes:

- Fixed horizontal scrollbar not being hidden
([#19637](https://github.com/zed-industries/zed/issues/19637))
2024-10-26 21:58:40 +03:00
Kirill Bulatov
6281df900c Update outline panel representation when a theme is changed (#19747)
Release Notes:

- N/A
2024-10-26 01:40:29 +03:00
Kirill Bulatov
c248d15d13 Properly deserialize active pane in the workspace (#19744)
Without setting the active pane metadata, no center pane events are
emitted on start before the pane is focused manually, which breaks
deserialization of other components like outline panel, which should
show the active pane's active item outlines on start.

Release Notes:

- N/A

Co-authored-by: Thorsten Ball <thorsten@zed.dev>
2024-10-26 01:40:29 +03:00
Conrad Irwin
07612cf0a3 Revert "Show invisibles in editor (#19298)"
This reverts commit 6dcec47235.
2024-10-25 13:57:27 -06:00
Conrad Irwin
7ad9f1b908 Fix partial downloads of ssh remote server (#19700)
Release Notes:

- SSH Remoting: fix a bug where inerrrupting ssh connecting could leave
your local binary cached in an invalid state
2024-10-24 14:37:36 -06:00
Kirill Bulatov
c1fedde6f5 Fix ssh project history (#19683)
Use `Fs` instead of `std::fs` and do entry existence checks better:
* first, check the worktree entry existence without any FS checks
* then, only for local cases, use `Fs` to check for abs_path existence
of items, in case those came from single-filed worktrees that got closed
and removed.

Remote entries do not get file existence checks, so might try opening
previously removed buffers for now.

Release Notes:

- N/A
2024-10-24 21:52:02 +03:00
Marshall Bowers
2f030e3b9a assistant: Add implementation for /delta argument completion (#19693)
This PR fixes a panic that could occur when trying to complete arguments
for the `/delta` slash command.

We were using `unimplemented!()` instead of providing a default no-op
implementation like we do for other slash commands that do not support
completing arguments.

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

Release Notes:

- Fixed a panic that could occur when trying to complete arguments with
the `/delta` command.
2024-10-24 13:40:31 -04:00
Peter Tripp
391243da0a zed 0.159.1 2024-10-24 13:13:02 -04:00
Danilo Leal
80da78b450 ssh: Capitalize error and connection strings (#19675)
Another tiny PR for the sake of consistency :)

Release Notes:

- N/A
2024-10-24 11:00:05 -04:00
Thorsten Ball
3b3477c5a1 ssh remoting: Fix wrong working directory for SSH terminals (#19672)
Before this change, we would save the working directory *on the client*
of each shell that was running in a terminal.

While it's technically right, it's wrong in all of these cases where
`working_directory` was used:

- in inline assistant
- when resolving file paths in the terminal output
- when serializing the current working dir and deserializing it on
restart

Release Notes:

- Fixed terminals opened on remote hosts failing to deserialize with an
error message after restarting Zed.
2024-10-24 10:59:59 -04:00
Thorsten Ball
8e80ce8430 ssh remoting: Fix version check (#19668)
This snuck in when Bennet and I were debugging why our connection to the
SSH host would break. We suspected that somewhere something was logging
to STDOUT and, I guess, we changed all `println!` to `eprintln!`.

Now, two weeks later, I'm sitting here, wondering why the version check
doesn't work anymore. The server always reports a version of `""`.

Turns out we take the command's STDOUT and not STDERR, which is correct.

But it also turns out we started to print the version to STDERR, which
breaks the version check.

One-character bug & one-character fix.

Release Notes:

- N/A
2024-10-24 10:59:53 -04:00
Kirill Bulatov
1b1872666c Use zstd without dynamic linking due to musl usage (#19627)
Due to leaning towards `musl` builds, unit features for `zstd` and link
it statically too for Zed.


bfe1e34f59/zstd-safe/zstd-sys/build.rs (L260)
shows that `ZSTD_SYS_USE_PKG_CONFIG` env var can be used to return this
behavior.

Release Notes:

- N/A
2024-10-24 10:59:47 -04:00
Peter Tripp
ac5cb8b969 Switch to Anthropic -latest tags (#19615)
- Closes: https://github.com/zed-industries/zed/issues/19609

Switches us to using `-latest` tags with Anthropic models instead of
pinning to a specific date version.
See: [Anthropic Model
Docs](https://docs.anthropic.com/en/docs/about-claude/models)

This is a no-op for:
- Claude 3 Opus (`claude-3-opus-20240229`)
- Claude 3 Sonnet (`claude-3-sonnet-20240229`)
- Claude 3 Haiku (`claude-3-haiku-20240307`)

For Claude 3.5 Sonnet this will update us from
`claude-3-5-sonnet-20240620` to `claude-3-5-sonnet-20241022`. We will
also pickup any subsequent model updates automatically when Anthropic
updates the `latest` tag.

This matches the behavior for OpenAI where use `gpt-4o` as the
model_name and not `gpt-4o-2024-08-06`.
2024-10-24 10:52:39 -04:00
Antonio Scandurra
c4060fe075 Fix crash in collab when sending worktree updates (#19678)
This pull request does a couple of things:

- In 29c2df73e1, we introduced a safety
guard that prevents this crash from happening again in the future by
returning an error instead of panicking when the payload is too large.
- In 3e7a2e5c30, we introduced chunking
for updates coming from SSH servers (previously, we were sending the
whole changeset and initial set of paths in their entirety).
- In 122b5b4, we introduced a panic hook that sends panics to Axiom.

For posterity, this is how we figured out what the panic was:

```
kubectl logs current-pod-name --previous --namespace=production
```

Release Notes:

- N/A

---------

Co-authored-by: Thorsten <thorsten@zed.dev>
Co-authored-by: Bennet <bennet@zed.dev>
Co-authored-by: Kirill <kirill@zed.dev>
2024-10-24 15:58:58 +02:00
Joseph T. Lyons
ca336567ef v0.159.x preview 2024-10-23 13:12:16 -04:00
79 changed files with 1740 additions and 502 deletions

View File

@@ -43,6 +43,8 @@ jobs:
esac
which cargo-set-version > /dev/null || cargo install cargo-edit
output=$(cargo set-version -p zed --bump patch 2>&1 | sed 's/.* //')
export GIT_COMMITTER_NAME="Zed Bot"
export GIT_COMMITTER_EMAIL="hi@zed.dev"
git commit -am "Bump to $output for @$GITHUB_ACTOR" --author "Zed Bot <hi@zed.dev>"
git tag v${output}${tag_suffix}
git push origin HEAD v${output}${tag_suffix}

43
Cargo.lock generated
View File

@@ -16,6 +16,7 @@ dependencies = [
"project",
"smallvec",
"ui",
"util",
"workspace",
]
@@ -853,7 +854,7 @@ dependencies = [
"chrono",
"futures-util",
"http-types",
"hyper 0.14.30",
"hyper 0.14.31",
"hyper-rustls 0.24.2",
"serde",
"serde_json",
@@ -1349,7 +1350,7 @@ dependencies = [
"http-body 0.4.6",
"http-body 1.0.1",
"httparse",
"hyper 0.14.30",
"hyper 0.14.31",
"hyper-rustls 0.24.2",
"once_cell",
"pin-project-lite",
@@ -1440,7 +1441,7 @@ dependencies = [
"headers",
"http 0.2.12",
"http-body 0.4.6",
"hyper 0.14.30",
"hyper 0.14.31",
"itoa",
"matchit",
"memchr",
@@ -1586,7 +1587,7 @@ dependencies = [
"bitflags 2.6.0",
"cexpr",
"clang-sys",
"itertools 0.10.5",
"itertools 0.12.1",
"lazy_static",
"lazycell",
"proc-macro2",
@@ -2365,7 +2366,7 @@ dependencies = [
"clickhouse-derive",
"clickhouse-rs-cityhash-sys",
"futures 0.3.30",
"hyper 0.14.30",
"hyper 0.14.31",
"hyper-tls",
"lz4",
"sealed",
@@ -2568,7 +2569,7 @@ dependencies = [
"gpui",
"hex",
"http_client",
"hyper 0.14.30",
"hyper 0.14.31",
"indoc",
"jsonwebtoken",
"language",
@@ -5567,9 +5568,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]]
name = "hyper"
version = "0.14.30"
version = "0.14.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a152ddd61dfaec7273fe8419ab357f33aee0d914c5f4efbf0d96fa749eea5ec9"
checksum = "8c08302e8fa335b151b788c775ff56e7a03ae64ff85c548ee820fecb70356e85"
dependencies = [
"bytes 1.7.2",
"futures-channel",
@@ -5582,7 +5583,7 @@ dependencies = [
"httpdate",
"itoa",
"pin-project-lite",
"socket2 0.4.10",
"socket2 0.5.7",
"tokio",
"tower-service",
"tracing",
@@ -5617,7 +5618,7 @@ checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590"
dependencies = [
"futures-util",
"http 0.2.12",
"hyper 0.14.30",
"hyper 0.14.31",
"log",
"rustls 0.21.12",
"rustls-native-certs 0.6.3",
@@ -5650,7 +5651,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
dependencies = [
"bytes 1.7.2",
"hyper 0.14.30",
"hyper 0.14.31",
"native-tls",
"tokio",
"tokio-native-tls",
@@ -6472,7 +6473,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4"
dependencies = [
"cfg-if",
"windows-targets 0.48.5",
"windows-targets 0.52.6",
]
[[package]]
@@ -9501,6 +9502,7 @@ dependencies = [
"fs",
"futures 0.3.30",
"gpui",
"itertools 0.13.0",
"log",
"parking_lot",
"prost",
@@ -9522,6 +9524,7 @@ dependencies = [
"async-watch",
"backtrace",
"cargo_toml",
"chrono",
"clap",
"client",
"clock",
@@ -9541,6 +9544,8 @@ dependencies = [
"node_runtime",
"paths",
"project",
"proto",
"release_channel",
"remote",
"reqwest_client",
"rpc",
@@ -9550,6 +9555,7 @@ dependencies = [
"settings",
"shellexpand 2.1.2",
"smol",
"telemetry_events",
"toml 0.8.19",
"util",
"worktree",
@@ -9621,7 +9627,7 @@ dependencies = [
"h2 0.3.26",
"http 0.2.12",
"http-body 0.4.6",
"hyper 0.14.30",
"hyper 0.14.31",
"hyper-tls",
"ipnet",
"js-sys",
@@ -9925,9 +9931,9 @@ dependencies = [
[[package]]
name = "runtimelib"
version = "0.15.0"
version = "0.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7d76d28b882a7b889ebb04e79bc2b160b3061821ea596ff0f4a838fc7a76db0"
checksum = "43075bcdb843dc90af086586895247681fb79ed9dc24c62e5455995a807d3d85"
dependencies = [
"anyhow",
"async-dispatcher",
@@ -13411,7 +13417,7 @@ dependencies = [
"futures-util",
"headers",
"http 0.2.12",
"hyper 0.14.30",
"hyper 0.14.31",
"log",
"mime",
"mime_guess",
@@ -14124,7 +14130,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.48.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -14991,7 +14997,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.160.0"
version = "0.159.7"
dependencies = [
"activity_indicator",
"anyhow",
@@ -15056,6 +15062,7 @@ dependencies = [
"project",
"project_panel",
"project_symbols",
"proto",
"quick_action_bar",
"recent_projects",
"release_channel",

View File

@@ -58,6 +58,7 @@
"gitignore": "vcs",
"gitkeep": "vcs",
"gitmodules": "vcs",
"gleam": "gleam",
"go": "go",
"gql": "graphql",
"graphql": "graphql",
@@ -83,6 +84,7 @@
"j2k": "image",
"java": "java",
"jfif": "image",
"jl": "julia",
"jp2": "image",
"jpeg": "image",
"jpg": "image",
@@ -90,7 +92,6 @@
"json": "storage",
"jsonc": "storage",
"jsx": "react",
"julia": "julia",
"jxl": "image",
"kt": "kotlin",
"ldf": "storage",
@@ -264,6 +265,9 @@
"fsharp": {
"icon": "icons/file_icons/fsharp.svg"
},
"gleam": {
"icon": "icons/file_icons/gleam.svg"
},
"go": {
"icon": "icons/file_icons/go.svg"
},

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
<path fill-rule="evenodd" fill="black" d="M 3.828125 14.601562 C 3.894531 15.726562 5.183594 16.375 6.132812 15.785156 L 6.136719 15.785156 L 8.988281 13.824219 C 8.996094 13.816406 9.007812 13.8125 9.015625 13.804688 C 9.203125 13.675781 9.4375 13.636719 9.65625 13.691406 L 12.988281 14.550781 C 14.105469 14.839844 15.140625 13.769531 14.8125 12.667969 L 13.832031 9.386719 C 13.769531 9.167969 13.800781 8.9375 13.921875 8.75 C 13.921875 8.746094 13.925781 8.746094 13.925781 8.746094 L 15.777344 5.863281 L 15.777344 5.859375 C 15.78125 5.851562 15.785156 5.84375 15.789062 5.835938 L 15.792969 5.835938 C 16.382812 4.871094 15.6875 3.582031 14.542969 3.554688 L 11.109375 3.472656 C 10.878906 3.464844 10.664062 3.359375 10.519531 3.183594 L 8.339844 0.542969 C 8.019531 0.152344 7.550781 -0.015625 7.105469 0.0078125 L 7.101562 0.0078125 C 7.039062 0.0117188 6.976562 0.0195312 6.914062 0.0273438 C 6.414062 0.117188 5.945312 0.453125 5.75 1 L 4.609375 4.222656 C 4.535156 4.4375 4.367188 4.613281 4.152344 4.695312 L 0.957031 5.945312 C -0.121094 6.363281 -0.328125 7.835938 0.589844 8.535156 L 3.316406 10.609375 C 3.5 10.75 3.609375 10.960938 3.625 11.191406 Z M 7.515625 1.847656 C 7.421875 1.730469 7.296875 1.695312 7.183594 1.714844 C 7.066406 1.734375 6.960938 1.8125 6.914062 1.953125 L 5.867188 4.902344 C 5.699219 5.382812 5.328125 5.765625 4.851562 5.949219 L 1.925781 7.09375 C 1.785156 7.148438 1.710938 7.253906 1.695312 7.371094 C 1.679688 7.484375 1.71875 7.605469 1.839844 7.695312 L 4.335938 9.597656 C 4.742188 9.90625 4.992188 10.375 5.023438 10.882812 L 5.207031 14.003906 C 5.214844 14.152344 5.296875 14.253906 5.398438 14.304688 C 5.503906 14.355469 5.632812 14.355469 5.757812 14.269531 L 8.347656 12.492188 C 8.765625 12.207031 9.292969 12.113281 9.785156 12.242188 L 12.824219 13.027344 C 12.972656 13.066406 13.09375 13.023438 13.175781 12.9375 C 13.257812 12.855469 13.296875 12.734375 13.253906 12.589844 L 12.355469 9.589844 C 12.210938 9.105469 12.285156 8.578125 12.558594 8.148438 L 14.253906 5.511719 C 14.335938 5.386719 14.332031 5.257812 14.277344 5.15625 C 14.222656 5.054688 14.117188 4.980469 13.964844 4.976562 L 10.824219 4.902344 C 10.316406 4.886719 9.835938 4.65625 9.511719 4.261719 Z M 7.515625 1.847656 "/>
<path fill="black" d="M 5.71875 7.257812 C 5.671875 7.25 5.628906 7.246094 5.582031 7.246094 C 5.09375 7.246094 4.695312 7.644531 4.695312 8.128906 C 4.695312 8.613281 5.09375 9.011719 5.582031 9.011719 C 6.070312 9.011719 6.46875 8.613281 6.46875 8.128906 C 6.46875 7.6875 6.140625 7.320312 5.71875 7.257812 Z M 5.71875 7.257812 "/>
<path fill="black" d="M 11.019531 7.953125 C 10.976562 7.957031 10.929688 7.960938 10.886719 7.960938 C 10.398438 7.960938 10 7.5625 10 7.078125 C 10 6.59375 10.398438 6.195312 10.886719 6.195312 C 11.371094 6.195312 11.773438 6.59375 11.773438 7.078125 C 11.773438 7.519531 11.445312 7.886719 11.019531 7.953125 Z M 11.019531 7.953125 "/>
<path fill="black" d="M 7.269531 9.089844 C 7.53125 8.988281 7.828125 9.113281 7.933594 9.375 C 8.125 9.859375 8.503906 9.996094 8.796875 9.949219 C 9.082031 9.898438 9.378906 9.664062 9.378906 9.136719 C 9.378906 8.855469 9.605469 8.628906 9.886719 8.628906 C 10.167969 8.628906 10.398438 8.855469 10.398438 9.136719 C 10.398438 10.140625 9.757812 10.816406 8.96875 10.949219 C 8.1875 11.078125 7.351562 10.664062 6.988281 9.75 C 6.882812 9.488281 7.011719 9.195312 7.269531 9.089844 Z M 7.269531 9.089844 "/>
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@@ -349,6 +349,7 @@
"alt-cmd-]": "editor::UnfoldLines",
"cmd-k cmd-l": "editor::ToggleFold",
"cmd-k cmd-[": "editor::FoldRecursive",
"cmd-k cmd-]": "editor::UnfoldRecursive",
"cmd-k cmd-1": ["editor::FoldAtLevel", { "level": 1 }],
"cmd-k cmd-2": ["editor::FoldAtLevel", { "level": 2 }],
"cmd-k cmd-3": ["editor::FoldAtLevel", { "level": 3 }],

View File

@@ -346,8 +346,6 @@
"git_status": true,
// Amount of indentation for nested items.
"indent_size": 20,
// Whether to show indent guides in the project panel.
"indent_guides": true,
// Whether to reveal it in the project panel automatically,
// when a corresponding project entry becomes active.
// Gitignored entries are never auto revealed.
@@ -371,6 +369,17 @@
/// 5. Never show the scrollbar:
/// "never"
"show": null
},
// Settings related to indent guides in the project panel.
"indent_guides": {
// When to show indent guides in the project panel.
// This setting can take two values:
//
// 1. Always show indent guides:
// "always"
// 2. Never show indent guides:
// "never"
"show": "always"
}
},
"outline_panel": {
@@ -388,15 +397,24 @@
"git_status": true,
// Amount of indentation for nested items.
"indent_size": 20,
// Whether to show indent guides in the outline panel.
"indent_guides": true,
// Whether to reveal it in the outline panel automatically,
// when a corresponding outline entry becomes active.
// Gitignored entries are never auto revealed.
"auto_reveal_entries": true,
/// Whether to fold directories automatically
/// when a directory has only one directory inside.
"auto_fold_dirs": true
"auto_fold_dirs": true,
// Settings related to indent guides in the outline panel.
"indent_guides": {
// When to show indent guides in the outline panel.
// This setting can take two values:
//
// 1. Always show indent guides:
// "always"
// 2. Never show indent guides:
// "never"
"show": "always"
}
},
"collaboration_panel": {
// Whether to show the collaboration panel button in the status bar.

View File

@@ -23,6 +23,7 @@ language.workspace = true
project.workspace = true
smallvec.workspace = true
ui.workspace = true
util.workspace = true
workspace.workspace = true
[dev-dependencies]

View File

@@ -13,7 +13,8 @@ use language::{
use project::{EnvironmentErrorMessage, LanguageServerProgress, Project, WorktreeId};
use smallvec::SmallVec;
use std::{cmp::Reverse, fmt::Write, sync::Arc, time::Duration};
use ui::{prelude::*, ButtonLike, ContextMenu, PopoverMenu, PopoverMenuHandle};
use ui::{prelude::*, ButtonLike, ContextMenu, PopoverMenu, PopoverMenuHandle, Tooltip};
use util::truncate_and_trailoff;
use workspace::{item::ItemHandle, StatusItemView, Workspace};
actions!(activity_indicator, [ShowErrorMessage]);
@@ -446,6 +447,8 @@ impl ActivityIndicator {
impl EventEmitter<Event> for ActivityIndicator {}
const MAX_MESSAGE_LEN: usize = 50;
impl Render for ActivityIndicator {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let result = h_flex()
@@ -456,6 +459,7 @@ impl Render for ActivityIndicator {
return result;
};
let this = cx.view().downgrade();
let truncate_content = content.message.len() > MAX_MESSAGE_LEN;
result.gap_2().child(
PopoverMenu::new("activity-indicator-popover")
.trigger(
@@ -464,7 +468,21 @@ impl Render for ActivityIndicator {
.id("activity-indicator-status")
.gap_2()
.children(content.icon)
.child(Label::new(content.message).size(LabelSize::Small))
.map(|button| {
if truncate_content {
button
.child(
Label::new(truncate_and_trailoff(
&content.message,
MAX_MESSAGE_LEN,
))
.size(LabelSize::Small),
)
.tooltip(move |cx| Tooltip::text(&content.message, cx))
} else {
button.child(Label::new(content.message).size(LabelSize::Small))
}
})
.when_some(content.on_click, |this, handler| {
this.on_click(cx.listener(move |this, _, cx| {
handler(this, cx);

View File

@@ -4707,7 +4707,7 @@ impl Render for ConfigurationView {
let mut element = v_flex()
.id("assistant-configuration-view")
.track_focus(&self.focus_handle)
.track_focus(&self.focus_handle(cx))
.bg(cx.theme().colors().editor_background)
.size_full()
.overflow_y_scroll()

View File

@@ -84,9 +84,9 @@ pub struct AutoUpdater {
}
#[derive(Deserialize)]
struct JsonRelease {
version: String,
url: String,
pub struct JsonRelease {
pub version: String,
pub url: String,
}
struct MacOsUnmounter {
@@ -482,7 +482,7 @@ impl AutoUpdater {
release_channel: ReleaseChannel,
version: Option<SemanticVersion>,
cx: &mut AsyncAppContext,
) -> Result<(String, String)> {
) -> Result<(JsonRelease, String)> {
let this = cx.update(|cx| {
cx.default_global::<GlobalAutoUpdate>()
.0
@@ -504,7 +504,7 @@ impl AutoUpdater {
let update_request_body = build_remote_server_update_request_body(cx)?;
let body = serde_json::to_string(&update_request_body)?;
Ok((release.url, body))
Ok((release, body))
}
async fn get_release(
@@ -686,6 +686,12 @@ async fn download_remote_server_binary(
let request_body = AsyncBody::from(serde_json::to_string(&update_request_body)?);
let mut response = client.get(&release.url, request_body, true).await?;
if !response.status().is_success() {
return Err(anyhow!(
"failed to download remote server release: {:?}",
response.status()
));
}
smol::io::copy(response.body_mut(), &mut temp_file).await?;
smol::fs::rename(&temp, &target_path).await?;

View File

@@ -48,6 +48,7 @@ pub struct Collaborator {
pub peer_id: proto::PeerId,
pub replica_id: ReplicaId,
pub user_id: UserId,
pub is_host: bool,
}
impl PartialOrd for User {
@@ -824,6 +825,7 @@ impl Collaborator {
peer_id: message.peer_id.ok_or_else(|| anyhow!("invalid peer id"))?,
replica_id: message.replica_id as ReplicaId,
user_id: message.user_id as UserId,
is_host: message.is_host,
})
}
}

View File

@@ -740,6 +740,7 @@ impl ProjectCollaborator {
peer_id: Some(self.connection_id.into()),
replica_id: self.replica_id.0 as u32,
user_id: self.user_id.to_proto(),
is_host: self.is_host,
}
}
}

View File

@@ -116,6 +116,7 @@ impl Database {
peer_id: Some(collaborator.connection().into()),
user_id: collaborator.user_id.to_proto(),
replica_id: collaborator.replica_id.0 as u32,
is_host: false,
})
.collect(),
})
@@ -222,6 +223,7 @@ impl Database {
peer_id: Some(collaborator.connection().into()),
user_id: collaborator.user_id.to_proto(),
replica_id: collaborator.replica_id.0 as u32,
is_host: false,
})
.collect(),
},
@@ -257,6 +259,7 @@ impl Database {
peer_id: Some(db_collaborator.connection().into()),
replica_id: db_collaborator.replica_id.0 as u32,
user_id: db_collaborator.user_id.to_proto(),
is_host: false,
})
} else {
collaborator_ids_to_remove.push(db_collaborator.id);
@@ -385,6 +388,7 @@ impl Database {
peer_id: Some(connection.into()),
replica_id: row.replica_id.0 as u32,
user_id: row.user_id.to_proto(),
is_host: false,
});
}

View File

@@ -121,11 +121,13 @@ async fn test_channel_buffers(db: &Arc<Database>) {
user_id: a_id.to_proto(),
peer_id: Some(rpc::proto::PeerId { id: 1, owner_id }),
replica_id: 0,
is_host: false,
},
rpc::proto::Collaborator {
user_id: b_id.to_proto(),
peer_id: Some(rpc::proto::PeerId { id: 2, owner_id }),
replica_id: 1,
is_host: false,
}
]
);

View File

@@ -1827,6 +1827,7 @@ fn join_project_internal(
peer_id: Some(session.connection_id.into()),
replica_id: replica_id.0 as u32,
user_id: guest_user_id.to_proto(),
is_host: false,
}),
};

View File

@@ -21,8 +21,8 @@ use language::{
language_settings::{
AllLanguageSettings, Formatter, FormatterList, PrettierSettings, SelectedFormatter,
},
tree_sitter_rust, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language, LanguageConfig,
LanguageMatcher, LineEnding, OffsetRangeExt, Point, Rope,
tree_sitter_rust, tree_sitter_typescript, Diagnostic, DiagnosticEntry, FakeLspAdapter,
Language, LanguageConfig, LanguageMatcher, LineEnding, OffsetRangeExt, Point, Rope,
};
use live_kit_client::MacOSDisplay;
use lsp::LanguageServerId;
@@ -4461,7 +4461,7 @@ async fn test_prettier_formatting_buffer(
},
..Default::default()
},
Some(tree_sitter_rust::LANGUAGE.into()),
Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
)));
let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
"TypeScript",

View File

@@ -1,14 +1,27 @@
use crate::tests::TestServer;
use call::ActiveCall;
use collections::HashSet;
use fs::{FakeFs, Fs as _};
use gpui::{BackgroundExecutor, Context as _, TestAppContext};
use futures::StreamExt as _;
use gpui::{BackgroundExecutor, Context as _, TestAppContext, UpdateGlobal as _};
use http_client::BlockedHttpClient;
use language::{language_settings::language_settings, LanguageRegistry};
use language::{
language_settings::{
language_settings, AllLanguageSettings, Formatter, FormatterList, PrettierSettings,
SelectedFormatter,
},
tree_sitter_typescript, FakeLspAdapter, Language, LanguageConfig, LanguageMatcher,
LanguageRegistry,
};
use node_runtime::NodeRuntime;
use project::ProjectPath;
use project::{
lsp_store::{FormatTarget, FormatTrigger},
ProjectPath,
};
use remote::SshRemoteClient;
use remote_server::{HeadlessAppState, HeadlessProject};
use serde_json::json;
use settings::SettingsStore;
use std::{path::Path, sync::Arc};
#[gpui::test(iterations = 10)]
@@ -304,3 +317,181 @@ async fn test_ssh_collaboration_git_branches(
assert_eq!(server_branch.as_ref(), "totally-new-branch");
}
#[gpui::test]
async fn test_ssh_collaboration_formatting_with_prettier(
executor: BackgroundExecutor,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
server_cx: &mut TestAppContext,
) {
cx_a.set_name("a");
cx_b.set_name("b");
server_cx.set_name("server");
let mut server = TestServer::start(executor.clone()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
server
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
.await;
let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx);
let remote_fs = FakeFs::new(server_cx.executor());
let buffer_text = "let one = \"two\"";
let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
remote_fs
.insert_tree("/project", serde_json::json!({ "a.ts": buffer_text }))
.await;
let test_plugin = "test_plugin";
let ts_lang = Arc::new(Language::new(
LanguageConfig {
name: "TypeScript".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["ts".to_string()],
..LanguageMatcher::default()
},
..LanguageConfig::default()
},
Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
));
client_a.language_registry().add(ts_lang.clone());
client_b.language_registry().add(ts_lang.clone());
let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
let mut fake_language_servers = languages.register_fake_lsp(
"TypeScript",
FakeLspAdapter {
prettier_plugins: vec![test_plugin],
..Default::default()
},
);
// User A connects to the remote project via SSH.
server_cx.update(HeadlessProject::init);
let remote_http_client = Arc::new(BlockedHttpClient);
let _headless_project = server_cx.new_model(|cx| {
client::init_settings(cx);
HeadlessProject::new(
HeadlessAppState {
session: server_ssh,
fs: remote_fs.clone(),
http_client: remote_http_client,
node_runtime: NodeRuntime::unavailable(),
languages,
},
cx,
)
});
let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await;
let (project_a, worktree_id) = client_a
.build_ssh_project("/project", client_ssh, cx_a)
.await;
// While the SSH worktree is being scanned, user A shares the remote project.
let active_call_a = cx_a.read(ActiveCall::global);
let project_id = active_call_a
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
.unwrap();
// User B joins the project.
let project_b = client_b.join_remote_project(project_id, cx_b).await;
executor.run_until_parked();
// Opens the buffer and formats it
let buffer_b = project_b
.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.ts"), cx))
.await
.expect("user B opens buffer for formatting");
cx_a.update(|cx| {
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings::<AllLanguageSettings>(cx, |file| {
file.defaults.formatter = Some(SelectedFormatter::Auto);
file.defaults.prettier = Some(PrettierSettings {
allowed: true,
..PrettierSettings::default()
});
});
});
});
cx_b.update(|cx| {
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings::<AllLanguageSettings>(cx, |file| {
file.defaults.formatter = Some(SelectedFormatter::List(FormatterList(
vec![Formatter::LanguageServer { name: None }].into(),
)));
file.defaults.prettier = Some(PrettierSettings {
allowed: true,
..PrettierSettings::default()
});
});
});
});
let fake_language_server = fake_language_servers.next().await.unwrap();
fake_language_server.handle_request::<lsp::request::Formatting, _, _>(|_, _| async move {
panic!(
"Unexpected: prettier should be preferred since it's enabled and language supports it"
)
});
project_b
.update(cx_b, |project, cx| {
project.format(
HashSet::from_iter([buffer_b.clone()]),
true,
FormatTrigger::Save,
FormatTarget::Buffer,
cx,
)
})
.await
.unwrap();
executor.run_until_parked();
assert_eq!(
buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
buffer_text.to_string() + "\n" + prettier_format_suffix,
"Prettier formatting was not applied to client buffer after client's request"
);
// User A opens and formats the same buffer too
let buffer_a = project_a
.update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.ts"), cx))
.await
.expect("user A opens buffer for formatting");
cx_a.update(|cx| {
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings::<AllLanguageSettings>(cx, |file| {
file.defaults.formatter = Some(SelectedFormatter::Auto);
file.defaults.prettier = Some(PrettierSettings {
allowed: true,
..PrettierSettings::default()
});
});
});
});
project_a
.update(cx_a, |project, cx| {
project.format(
HashSet::from_iter([buffer_a.clone()]),
true,
FormatTrigger::Manual,
FormatTarget::Buffer,
cx,
)
})
.await
.unwrap();
executor.run_until_parked();
assert_eq!(
buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
buffer_text.to_string() + "\n" + prettier_format_suffix + "\n" + prettier_format_suffix,
"Prettier formatting was not applied to client buffer after host's request"
);
}

View File

@@ -2726,7 +2726,7 @@ impl Render for CollabPanel {
.on_action(cx.listener(CollabPanel::collapse_selected_channel))
.on_action(cx.listener(CollabPanel::expand_selected_channel))
.on_action(cx.listener(CollabPanel::start_move_selected_channel))
.track_focus(&self.focus_handle)
.track_focus(&self.focus_handle(cx))
.size_full()
.child(if self.user_store.read(cx).current_user().is_none() {
self.render_signed_out(cx)

View File

@@ -185,7 +185,7 @@ impl Render for CopilotCodeVerification {
v_flex()
.id("copilot code verification")
.track_focus(&self.focus_handle)
.track_focus(&self.focus_handle(cx))
.elevation_3(cx)
.w_96()
.items_center()

View File

@@ -101,7 +101,7 @@ impl Render for ProjectDiagnosticsEditor {
};
div()
.track_focus(&self.focus_handle)
.track_focus(&self.focus_handle(cx))
.when(self.path_states.is_empty(), |el| {
el.key_context("EmptyPane")
})

View File

@@ -1157,16 +1157,21 @@ pub mod tests {
use super::*;
use crate::{movement, test::marked_display_snapshot};
use block_map::BlockPlacement;
use gpui::{div, font, observe, px, AppContext, BorrowAppContext, Context, Element, Hsla};
use gpui::{
div, font, observe, px, AppContext, BorrowAppContext, Context, Element, Hsla, Rgba,
};
use language::{
language_settings::{AllLanguageSettings, AllLanguageSettingsContent},
Buffer, Language, LanguageConfig, LanguageMatcher,
Buffer, Diagnostic, DiagnosticEntry, DiagnosticSet, Language, LanguageConfig,
LanguageMatcher,
};
use lsp::LanguageServerId;
use project::Project;
use rand::{prelude::*, Rng};
use settings::SettingsStore;
use smol::stream::StreamExt;
use std::{env, sync::Arc};
use text::PointUtf16;
use theme::{LoadThemes, SyntaxTheme};
use unindent::Unindent as _;
use util::test::{marked_text_ranges, sample_text};
@@ -1821,6 +1826,125 @@ pub mod tests {
);
}
#[gpui::test]
async fn test_chunks_with_diagnostics_across_blocks(cx: &mut gpui::TestAppContext) {
cx.background_executor
.set_block_on_ticks(usize::MAX..=usize::MAX);
let text = r#"
struct A {
b: usize;
}
const c: usize = 1;
"#
.unindent();
cx.update(|cx| init_test(cx, |_| {}));
let buffer = cx.new_model(|cx| Buffer::local(text, cx));
buffer.update(cx, |buffer, cx| {
buffer.update_diagnostics(
LanguageServerId(0),
DiagnosticSet::new(
[DiagnosticEntry {
range: PointUtf16::new(0, 0)..PointUtf16::new(2, 1),
diagnostic: Diagnostic {
severity: DiagnosticSeverity::ERROR,
group_id: 1,
message: "hi".into(),
..Default::default()
},
}],
buffer,
),
cx,
)
});
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
let buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
let map = cx.new_model(|cx| {
DisplayMap::new(
buffer,
font("Courier"),
px(16.0),
None,
true,
1,
1,
0,
FoldPlaceholder::test(),
cx,
)
});
let black = gpui::black().to_rgb();
let red = gpui::red().to_rgb();
// Insert a block in the middle of a multi-line diagnostic.
map.update(cx, |map, cx| {
map.highlight_text(
TypeId::of::<usize>(),
vec![
buffer_snapshot.anchor_before(Point::new(3, 9))
..buffer_snapshot.anchor_after(Point::new(3, 14)),
buffer_snapshot.anchor_before(Point::new(3, 17))
..buffer_snapshot.anchor_after(Point::new(3, 18)),
],
red.into(),
);
map.insert_blocks(
[BlockProperties {
placement: BlockPlacement::Below(
buffer_snapshot.anchor_before(Point::new(1, 0)),
),
height: 1,
style: BlockStyle::Sticky,
render: Box::new(|_| div().into_any()),
priority: 0,
}],
cx,
)
});
let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
let mut chunks = Vec::<(String, Option<DiagnosticSeverity>, Rgba)>::new();
for chunk in snapshot.chunks(DisplayRow(0)..DisplayRow(5), true, Default::default()) {
let color = chunk
.highlight_style
.and_then(|style| style.color)
.map_or(black, |color| color.to_rgb());
if let Some((last_chunk, last_severity, last_color)) = chunks.last_mut() {
if *last_severity == chunk.diagnostic_severity && *last_color == color {
last_chunk.push_str(chunk.text);
continue;
}
}
chunks.push((chunk.text.to_string(), chunk.diagnostic_severity, color));
}
assert_eq!(
chunks,
[
(
"struct A {\n b: usize;\n".into(),
Some(DiagnosticSeverity::ERROR),
black
),
("\n".into(), None, black),
("}".into(), Some(DiagnosticSeverity::ERROR), black),
("\nconst c: ".into(), None, black),
("usize".into(), None, red),
(" = ".into(), None, black),
("1".into(), None, red),
(";\n".into(), None, black),
]
);
}
// todo(linux) fails due to pixel differences in text rendering
#[cfg(target_os = "macos")]
#[gpui::test]

View File

@@ -255,6 +255,22 @@ impl<'a> InlayChunks<'a> {
self.buffer_chunk = None;
self.output_offset = new_range.start;
self.max_output_offset = new_range.end;
let mut highlight_endpoints = Vec::new();
if let Some(text_highlights) = self.highlights.text_highlights {
if !text_highlights.is_empty() {
self.snapshot.apply_text_highlights(
&mut self.transforms,
&new_range,
text_highlights,
&mut highlight_endpoints,
);
self.transforms.seek(&new_range.start, Bias::Right, &());
highlight_endpoints.sort();
}
}
self.highlight_endpoints = highlight_endpoints.into_iter().peekable();
self.active_highlights.clear();
}
pub fn offset(&self) -> InlayOffset {

View File

@@ -3244,9 +3244,21 @@ impl Editor {
}
if enabled && pair.start.ends_with(text.as_ref()) {
bracket_pair = Some(pair.clone());
is_bracket_pair_start = true;
break;
let prefix_len = pair.start.len() - text.len();
let preceding_text_matches_prefix = prefix_len == 0
|| (selection.start.column >= (prefix_len as u32)
&& snapshot.contains_str_at(
Point::new(
selection.start.row,
selection.start.column - (prefix_len as u32),
),
&pair.start[..prefix_len],
));
if preceding_text_matches_prefix {
bracket_pair = Some(pair.clone());
is_bracket_pair_start = true;
break;
}
}
if pair.end.as_str() == text.as_ref() {
bracket_pair = Some(pair.clone());
@@ -3263,8 +3275,6 @@ impl Editor {
self.use_auto_surround && snapshot_settings.use_auto_surround;
if selection.is_empty() {
if is_bracket_pair_start {
let prefix_len = bracket_pair.start.len() - text.len();
// If the inserted text is a suffix of an opening bracket and the
// selection is preceded by the rest of the opening bracket, then
// insert the closing bracket.
@@ -3272,15 +3282,6 @@ impl Editor {
.chars_at(selection.start)
.next()
.map_or(true, |c| scope.should_autoclose_before(c));
let preceding_text_matches_prefix = prefix_len == 0
|| (selection.start.column >= (prefix_len as u32)
&& snapshot.contains_str_at(
Point::new(
selection.start.row,
selection.start.column - (prefix_len as u32),
),
&bracket_pair.start[..prefix_len],
));
let is_closing_quote = if bracket_pair.end == bracket_pair.start
&& bracket_pair.start.len() == 1
@@ -3299,7 +3300,6 @@ impl Editor {
if autoclose
&& bracket_pair.close
&& following_text_allows_autoclose
&& preceding_text_matches_prefix
&& !is_closing_quote
{
let anchor = snapshot.anchor_before(selection.end);
@@ -10460,7 +10460,7 @@ impl Editor {
fn cancel_language_server_work(
&mut self,
_: &CancelLanguageServerWork,
_: &actions::CancelLanguageServerWork,
cx: &mut ViewContext<Self>,
) {
if let Some(project) = self.project.clone() {

View File

@@ -368,12 +368,15 @@ impl GitBlame {
.spawn({
let snapshot = snapshot.clone();
async move {
let Blame {
let Some(Blame {
entries,
permalinks,
messages,
remote_url,
} = blame.await?;
}) = blame.await?
else {
return Ok(None);
};
let entries = build_blame_entry_sum_tree(entries, snapshot.max_point().row);
let commit_details = parse_commit_messages(
@@ -385,13 +388,16 @@ impl GitBlame {
)
.await;
anyhow::Ok((entries, commit_details))
anyhow::Ok(Some((entries, commit_details)))
}
})
.await;
this.update(&mut cx, |this, cx| match result {
Ok((entries, commit_details)) => {
Ok(None) => {
// Nothing to do, e.g. no repository found
}
Ok(Some((entries, commit_details))) => {
this.buffer_edits = buffer_edits;
this.buffer_snapshot = snapshot;
this.entries = entries;
@@ -410,11 +416,7 @@ impl GitBlame {
} else {
// If we weren't triggered by a user, we just log errors in the background, instead of sending
// notifications.
// Except for `NoRepositoryError`, which can happen often if a user has inline-blame turned on
// and opens a non-git file.
if error.downcast_ref::<project::NoRepositoryError>().is_none() {
log::error!("failed to get git blame data: {error:?}");
}
log::error!("failed to get git blame data: {error:?}");
}
}),
})

View File

@@ -4,7 +4,7 @@ use gpui::{HighlightStyle, Model, StyledText};
use picker::{Picker, PickerDelegate};
use project::{Entry, PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
use std::{
path::PathBuf,
path::{Path, PathBuf},
sync::{
atomic::{self, AtomicBool},
Arc,
@@ -254,6 +254,7 @@ impl PickerDelegate for NewPathDelegate {
.trim()
.trim_start_matches("./")
.trim_start_matches('/');
let (dir, suffix) = if let Some(index) = query.rfind('/') {
let suffix = if index + 1 < query.len() {
Some(query[index + 1..].to_string())
@@ -317,6 +318,14 @@ impl PickerDelegate for NewPathDelegate {
})
}
fn confirm_completion(
&mut self,
_: String,
cx: &mut ViewContext<Picker<Self>>,
) -> Option<String> {
self.confirm_update_query(cx)
}
fn confirm_update_query(&mut self, cx: &mut ViewContext<Picker<Self>>) -> Option<String> {
let m = self.matches.get(self.selected_index)?;
if m.is_dir(self.project.read(cx), cx) {
@@ -422,7 +431,32 @@ impl NewPathDelegate {
) {
cx.notify();
if query.is_empty() {
self.matches = vec![];
self.matches = self
.project
.read(cx)
.worktrees(cx)
.flat_map(|worktree| {
let worktree_id = worktree.read(cx).id();
worktree
.read(cx)
.child_entries(Path::new(""))
.filter_map(move |entry| {
entry.is_dir().then(|| Match {
path_match: Some(PathMatch {
score: 1.0,
positions: Default::default(),
worktree_id: worktree_id.to_usize(),
path: entry.path.clone(),
path_prefix: "".into(),
is_dir: entry.is_dir(),
distance_to_relative_ancestor: 0,
}),
suffix: None,
})
})
})
.collect();
return;
}

View File

@@ -220,7 +220,11 @@ impl PickerDelegate for OpenPathDelegate {
})
}
fn confirm_completion(&self, query: String) -> Option<String> {
fn confirm_completion(
&mut self,
query: String,
_: &mut ViewContext<Picker<Self>>,
) -> Option<String> {
Some(
maybe!({
let m = self.matches.get(self.selected_index)?;

View File

@@ -485,7 +485,7 @@ impl Render for TextInput {
div()
.flex()
.key_context("TextInput")
.track_focus(&self.focus_handle)
.track_focus(&self.focus_handle(cx))
.cursor(CursorStyle::IBeam)
.on_action(cx.listener(Self::backspace))
.on_action(cx.listener(Self::delete))
@@ -549,7 +549,7 @@ impl Render for InputExample {
let num_keystrokes = self.recent_keystrokes.len();
div()
.bg(rgb(0xaaaaaa))
.track_focus(&self.focus_handle)
.track_focus(&self.focus_handle(cx))
.flex()
.flex_col()
.size_full()

View File

@@ -217,6 +217,7 @@ pub(crate) type KeystrokeObserver =
type QuitHandler = Box<dyn FnOnce(&mut AppContext) -> LocalBoxFuture<'static, ()> + 'static>;
type ReleaseListener = Box<dyn FnOnce(&mut dyn Any, &mut AppContext) + 'static>;
type NewViewListener = Box<dyn FnMut(AnyView, &mut WindowContext) + 'static>;
type NewModelListener = Box<dyn FnMut(AnyModel, &mut AppContext) + 'static>;
/// Contains the state of the full application, and passed as a reference to a variety of callbacks.
/// Other contexts such as [ModelContext], [WindowContext], and [ViewContext] deref to this type, making it the most general context type.
@@ -237,6 +238,7 @@ pub struct AppContext {
http_client: Arc<dyn HttpClient>,
pub(crate) globals_by_type: FxHashMap<TypeId, Box<dyn Any>>,
pub(crate) entities: EntityMap,
pub(crate) new_model_observers: SubscriberSet<TypeId, NewModelListener>,
pub(crate) new_view_observers: SubscriberSet<TypeId, NewViewListener>,
pub(crate) windows: SlotMap<WindowId, Option<Window>>,
pub(crate) window_handles: FxHashMap<WindowId, AnyWindowHandle>,
@@ -296,6 +298,7 @@ impl AppContext {
globals_by_type: FxHashMap::default(),
entities,
new_view_observers: SubscriberSet::new(),
new_model_observers: SubscriberSet::new(),
window_handles: FxHashMap::default(),
windows: SlotMap::with_key(),
keymap: Rc::new(RefCell::new(Keymap::default())),
@@ -1016,6 +1019,7 @@ impl AppContext {
activate();
subscription
}
/// Arrange for the given function to be invoked whenever a view of the specified type is created.
/// The function will be passed a mutable reference to the view along with an appropriate context.
pub fn observe_new_views<V: 'static>(
@@ -1035,6 +1039,31 @@ impl AppContext {
)
}
pub(crate) fn new_model_observer(&self, key: TypeId, value: NewModelListener) -> Subscription {
let (subscription, activate) = self.new_model_observers.insert(key, value);
activate();
subscription
}
/// Arrange for the given function to be invoked whenever a view of the specified type is created.
/// The function will be passed a mutable reference to the view along with an appropriate context.
pub fn observe_new_models<T: 'static>(
&self,
on_new: impl 'static + Fn(&mut T, &mut ModelContext<T>),
) -> Subscription {
self.new_model_observer(
TypeId::of::<T>(),
Box::new(move |any_model: AnyModel, cx: &mut AppContext| {
any_model
.downcast::<T>()
.unwrap()
.update(cx, |model_state, cx| {
on_new(model_state, cx);
})
}),
)
}
/// Observe the release of a model or view. The callback is invoked after the model or view
/// has no more strong references but before it has been dropped.
pub fn observe_release<E, T>(
@@ -1346,8 +1375,21 @@ impl Context for AppContext {
) -> Model<T> {
self.update(|cx| {
let slot = cx.entities.reserve();
let model = slot.clone();
let entity = build_model(&mut ModelContext::new(cx, slot.downgrade()));
cx.entities.insert(slot, entity)
cx.entities.insert(slot, entity);
// Non-generic part to avoid leaking SubscriberSet to invokers of `new_view`.
fn notify_observers(cx: &mut AppContext, tid: TypeId, model: AnyModel) {
cx.new_model_observers.clone().retain(&tid, |observer| {
let any_model = model.clone();
(observer)(any_model, cx);
true
});
}
notify_observers(cx, TypeId::of::<T>(), AnyModel::from(model.clone()));
model
})
}

View File

@@ -16,7 +16,7 @@
/// impl Render for Editor {
/// fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
/// div()
/// .track_focus(&self.focus_handle)
/// .track_focus(&self.focus_handle(cx))
/// .keymap_context("Editor")
/// .on_action(cx.listener(Editor::undo))
/// .on_action(cx.listener(Editor::redo))

View File

@@ -271,7 +271,7 @@ impl Render for ImageView {
.left_0();
div()
.track_focus(&self.focus_handle)
.track_focus(&self.focus_handle(cx))
.size_full()
.child(checkered_background)
.child(

View File

@@ -4103,6 +4103,10 @@ impl<'a> BufferChunks<'a> {
diagnostic_endpoints
.sort_unstable_by_key(|endpoint| (endpoint.offset, !endpoint.is_start));
*diagnostics = diagnostic_endpoints.into_iter().peekable();
self.hint_depth = 0;
self.error_depth = 0;
self.warning_depth = 0;
self.information_depth = 0;
}
}
}

View File

@@ -38,7 +38,7 @@ menu.workspace = true
ollama = { workspace = true, features = ["schemars"] }
open_ai = { workspace = true, features = ["schemars"] }
parking_lot.workspace = true
proto = { workspace = true, features = ["test-support"] }
proto.workspace = true
project.workspace = true
schemars.workspace = true
serde.workspace = true
@@ -62,6 +62,7 @@ env_logger.workspace = true
language = { workspace = true, features = ["test-support"] }
log.workspace = true
project = { workspace = true, features = ["test-support"] }
proto = { workspace = true, features = ["test-support"] }
rand.workspace = true
text = { workspace = true, features = ["test-support"] }
unindent.workspace = true

View File

@@ -1237,6 +1237,22 @@ impl Render for LspLogToolbarItemView {
view.show_rpc_trace_for_server(row.server_id, cx);
}),
);
if server_selected && row.selected_entry == LogKind::Rpc {
let selected_ix = menu.select_last();
// Each language server has:
// 1. A title.
// 2. Server logs.
// 3. Server trace.
// 4. RPC messages.
// 5. Server capabilities
// Thus, if nth server's RPC is selected, the index of selected entry should match this formula
let _expected_index = ix * 5 + 3;
debug_assert_eq!(
Some(_expected_index),
selected_ix,
"Could not scroll to a just added LSP menu item"
);
}
menu = menu.entry(
SERVER_CAPABILITIES,
None,
@@ -1244,14 +1260,6 @@ impl Render for LspLogToolbarItemView {
view.show_capabilities_for_server(row.server_id, cx);
}),
);
if server_selected && row.selected_entry == LogKind::Rpc {
let selected_ix = menu.select_last();
debug_assert_eq!(
Some(ix * 4 + 3),
selected_ix,
"Could not scroll to a just added LSP menu item"
);
}
}
menu
})

View File

@@ -5,9 +5,9 @@ line_comments = ["// ", "/// ", "//! "]
autoclose_before = ";:.,=}])>"
brackets = [
{ start = "{", end = "}", close = true, newline = true },
{ start = "r#\"", end = "\"#", close = true, newline = true },
{ start = "r##\"", end = "\"##", close = true, newline = true },
{ start = "r###\"", end = "\"###", close = true, newline = true },
{ start = "r#\"", end = "\"#", close = true, newline = true, not_in = ["string", "comment"] },
{ start = "r##\"", end = "\"##", close = true, newline = true, not_in = ["string", "comment"] },
{ start = "r###\"", end = "\"###", close = true, newline = true, not_in = ["string", "comment"] },
{ start = "[", end = "]", close = true, newline = true },
{ start = "(", end = ")", close = true, newline = true },
{ start = "<", end = ">", close = false, newline = true, not_in = ["string", "comment"] },

View File

@@ -479,7 +479,7 @@ impl Render for MarkdownPreviewView {
v_flex()
.id("MarkdownPreview")
.key_context("MarkdownPreview")
.track_focus(&self.focus_handle)
.track_focus(&self.focus_handle(cx))
.size_full()
.bg(cx.theme().colors().editor_background)
.p_4()

View File

@@ -35,7 +35,7 @@ use itertools::Itertools;
use language::{BufferId, BufferSnapshot, OffsetRangeExt, OutlineItem};
use menu::{Cancel, SelectFirst, SelectLast, SelectNext, SelectPrev};
use outline_panel_settings::{OutlinePanelDockPosition, OutlinePanelSettings};
use outline_panel_settings::{OutlinePanelDockPosition, OutlinePanelSettings, ShowIndentGuides};
use project::{File, Fs, Item, Project};
use search::{BufferSearchBar, ProjectSearchView};
use serde::{Deserialize, Serialize};
@@ -3748,7 +3748,7 @@ impl Render for OutlinePanel {
let pinned = self.pinned;
let settings = OutlinePanelSettings::get_global(cx);
let indent_size = settings.indent_size;
let show_indent_guides = settings.indent_guides;
let show_indent_guides = settings.indent_guides.show == ShowIndentGuides::Always;
let outline_panel = v_flex()
.id("outline-panel")
@@ -3787,7 +3787,7 @@ impl Render for OutlinePanel {
}
}),
)
.track_focus(&self.focus_handle);
.track_focus(&self.focus_handle(cx));
if self.cached_entries.is_empty() {
let header = if self.updating_fs_entries {

View File

@@ -10,6 +10,13 @@ pub enum OutlinePanelDockPosition {
Right,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ShowIndentGuides {
Always,
Never,
}
#[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
pub struct OutlinePanelSettings {
pub button: bool,
@@ -19,11 +26,22 @@ pub struct OutlinePanelSettings {
pub folder_icons: bool,
pub git_status: bool,
pub indent_size: f32,
pub indent_guides: bool,
pub indent_guides: IndentGuidesSettings,
pub auto_reveal_entries: bool,
pub auto_fold_dirs: bool,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
pub struct IndentGuidesSettings {
pub show: ShowIndentGuides,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
pub struct IndentGuidesSettingsContent {
/// When to show the scrollbar in the outline panel.
pub show: Option<ShowIndentGuides>,
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
pub struct OutlinePanelSettingsContent {
/// Whether to show the outline panel button in the status bar.
@@ -54,10 +72,6 @@ pub struct OutlinePanelSettingsContent {
///
/// Default: 20
pub indent_size: Option<f32>,
/// Whether to show indent guides in the outline panel.
///
/// Default: true
pub indent_guides: Option<bool>,
/// Whether to reveal it in the outline panel automatically,
/// when a corresponding project entry becomes active.
/// Gitignored entries are never auto revealed.
@@ -69,6 +83,8 @@ pub struct OutlinePanelSettingsContent {
///
/// Default: true
pub auto_fold_dirs: Option<bool>,
/// Settings related to indent guides in the outline panel.
pub indent_guides: Option<IndentGuidesSettingsContent>,
}
impl Settings for OutlinePanelSettings {

View File

@@ -52,8 +52,8 @@ impl EmptyHead {
}
impl Render for EmptyHead {
fn render(&mut self, _: &mut ViewContext<Self>) -> impl IntoElement {
div().track_focus(&self.focus_handle)
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
div().track_focus(&self.focus_handle(cx))
}
}

View File

@@ -108,7 +108,11 @@ pub trait PickerDelegate: Sized + 'static {
fn should_dismiss(&self) -> bool {
true
}
fn confirm_completion(&self, _query: String) -> Option<String> {
fn confirm_completion(
&mut self,
_query: String,
_: &mut ViewContext<Picker<Self>>,
) -> Option<String> {
None
}
@@ -370,7 +374,7 @@ impl<D: PickerDelegate> Picker<D> {
}
fn confirm_completion(&mut self, _: &ConfirmCompletion, cx: &mut ViewContext<Self>) {
if let Some(new_query) = self.delegate.confirm_completion(self.query(cx)) {
if let Some(new_query) = self.delegate.confirm_completion(self.query(cx), cx) {
self.set_query(new_query, cx);
} else {
cx.propagate()

View File

@@ -14,14 +14,14 @@ use std::{
};
use util::paths::PathMatcher;
#[derive(Clone)]
#[derive(Debug, Clone)]
pub enum Prettier {
Real(RealPrettier),
#[cfg(any(test, feature = "test-support"))]
Test(TestPrettier),
}
#[derive(Clone)]
#[derive(Debug, Clone)]
pub struct RealPrettier {
default: bool,
prettier_dir: PathBuf,
@@ -29,7 +29,7 @@ pub struct RealPrettier {
}
#[cfg(any(test, feature = "test-support"))]
#[derive(Clone)]
#[derive(Debug, Clone)]
pub struct TestPrettier {
prettier_dir: PathBuf,
default: bool,
@@ -329,11 +329,7 @@ impl Prettier {
})?
.context("prettier params calculation")?;
let response = local
.server
.request::<Format>(params)
.await
.context("prettier format request")?;
let response = local.server.request::<Format>(params).await?;
let diff_task = buffer.update(cx, |buffer, cx| buffer.diff(response.text, cx))?;
Ok(diff_task.await)
}

View File

@@ -1,7 +1,7 @@
use crate::{
search::SearchQuery,
worktree_store::{WorktreeStore, WorktreeStoreEvent},
Item, NoRepositoryError, ProjectPath,
Item, ProjectPath,
};
use ::git::{parse_git_remote_url, BuildPermalinkParams, GitHostingProviderRegistry};
use anyhow::{anyhow, Context as _, Result};
@@ -1118,7 +1118,7 @@ impl BufferStore {
buffer: &Model<Buffer>,
version: Option<clock::Global>,
cx: &AppContext,
) -> Task<Result<Blame>> {
) -> Task<Result<Option<Blame>>> {
let buffer = buffer.read(cx);
let Some(file) = File::from_dyn(buffer.file()) else {
return Task::ready(Err(anyhow!("buffer has no file")));
@@ -1130,7 +1130,7 @@ impl BufferStore {
let blame_params = maybe!({
let (repo_entry, local_repo_entry) = match worktree.repo_for_path(&file.path) {
Some(repo_for_path) => repo_for_path,
None => anyhow::bail!(NoRepositoryError {}),
None => return Ok(None),
};
let relative_path = repo_entry
@@ -1144,13 +1144,16 @@ impl BufferStore {
None => buffer.as_rope().clone(),
};
anyhow::Ok((repo, relative_path, content))
anyhow::Ok(Some((repo, relative_path, content)))
});
cx.background_executor().spawn(async move {
let (repo, relative_path, content) = blame_params?;
let Some((repo, relative_path, content)) = blame_params? else {
return Ok(None);
};
repo.blame(&relative_path, content)
.with_context(|| format!("Failed to blame {:?}", relative_path.0))
.map(Some)
})
}
Worktree::Remote(worktree) => {
@@ -2112,7 +2115,13 @@ fn is_not_found_error(error: &anyhow::Error) -> bool {
.is_some_and(|err| err.kind() == io::ErrorKind::NotFound)
}
fn serialize_blame_buffer_response(blame: git::blame::Blame) -> proto::BlameBufferResponse {
fn serialize_blame_buffer_response(blame: Option<git::blame::Blame>) -> proto::BlameBufferResponse {
let Some(blame) = blame else {
return proto::BlameBufferResponse {
blame_response: None,
};
};
let entries = blame
.entries
.into_iter()
@@ -2154,14 +2163,19 @@ fn serialize_blame_buffer_response(blame: git::blame::Blame) -> proto::BlameBuff
.collect::<Vec<_>>();
proto::BlameBufferResponse {
entries,
messages,
permalinks,
remote_url: blame.remote_url,
blame_response: Some(proto::blame_buffer_response::BlameResponse {
entries,
messages,
permalinks,
remote_url: blame.remote_url,
}),
}
}
fn deserialize_blame_buffer_response(response: proto::BlameBufferResponse) -> git::blame::Blame {
fn deserialize_blame_buffer_response(
response: proto::BlameBufferResponse,
) -> Option<git::blame::Blame> {
let response = response.blame_response?;
let entries = response
.entries
.into_iter()
@@ -2202,10 +2216,10 @@ fn deserialize_blame_buffer_response(response: proto::BlameBufferResponse) -> gi
})
.collect::<HashMap<_, _>>();
Blame {
Some(Blame {
entries,
permalinks,
messages,
remote_url: response.remote_url,
}
})
}

View File

@@ -29,6 +29,7 @@ use gpui::{
Task, WeakModel,
};
use http_client::HttpClient;
use itertools::Itertools as _;
use language::{
language_settings::{
language_settings, FormatOnSave, Formatter, LanguageSettings, SelectedFormatter,
@@ -144,7 +145,6 @@ pub struct LocalLspStore {
HashMap<LanguageServerId, (LanguageServerName, Arc<LanguageServer>)>,
prettier_store: Model<PrettierStore>,
current_lsp_settings: HashMap<LanguageServerName, LspSettings>,
last_formatting_failure: Option<String>,
_subscription: gpui::Subscription,
}
@@ -563,9 +563,7 @@ impl LocalLspStore {
})?;
prettier_store::format_with_prettier(&prettier, &buffer.handle, cx)
.await
.transpose()
.ok()
.flatten()
.transpose()?
}
Formatter::External { command, arguments } => {
Self::format_via_external_command(buffer, command, arguments.as_deref(), cx)
@@ -675,6 +673,7 @@ impl LocalLspStore {
}
}
#[derive(Debug)]
pub struct FormattableBuffer {
handle: Model<Buffer>,
abs_path: Option<PathBuf>,
@@ -704,6 +703,7 @@ impl LspStoreMode {
pub struct LspStore {
mode: LspStoreMode,
last_formatting_failure: Option<String>,
downstream_client: Option<(AnyProtoClient, u64)>,
nonce: u128,
buffer_store: Model<BufferStore>,
@@ -786,6 +786,7 @@ impl LspStore {
pub fn init(client: &AnyProtoClient) {
client.add_model_request_handler(Self::handle_multi_lsp_query);
client.add_model_request_handler(Self::handle_restart_language_servers);
client.add_model_request_handler(Self::handle_cancel_language_server_work);
client.add_model_message_handler(Self::handle_start_language_server);
client.add_model_message_handler(Self::handle_update_language_server);
client.add_model_message_handler(Self::handle_language_server_log);
@@ -905,7 +906,6 @@ impl LspStore {
language_server_watcher_registrations: Default::default(),
current_lsp_settings: ProjectSettings::get_global(cx).lsp.clone(),
buffers_being_formatted: Default::default(),
last_formatting_failure: None,
prettier_store,
environment,
http_client,
@@ -915,6 +915,7 @@ impl LspStore {
this.as_local_mut().unwrap().shutdown_language_servers(cx)
}),
}),
last_formatting_failure: None,
downstream_client: None,
buffer_store,
worktree_store,
@@ -975,6 +976,7 @@ impl LspStore {
upstream_project_id: project_id,
}),
downstream_client: None,
last_formatting_failure: None,
buffer_store,
worktree_store,
languages: languages.clone(),
@@ -4043,6 +4045,20 @@ impl LspStore {
.or_default()
.insert(server_id, summary);
}
if let Some((downstream_client, project_id)) = &this.downstream_client {
downstream_client
.send(proto::UpdateDiagnosticSummary {
project_id: *project_id,
worktree_id: worktree_id.to_proto(),
summary: Some(proto::DiagnosticSummary {
path: project_path.path.to_string_lossy().to_string(),
language_server_id: server_id.0 as u64,
error_count: summary.error_count as u32,
warning_count: summary.warning_count as u32,
}),
})
.log_err();
}
cx.emit(LspStoreEvent::DiagnosticsUpdated {
language_server_id: LanguageServerId(message.language_server_id as usize),
path: project_path,
@@ -4103,7 +4119,7 @@ impl LspStore {
LanguageServerProgress {
title: payload.title,
is_disk_based_diagnostics_progress: false,
is_cancellable: false,
is_cancellable: payload.is_cancellable.unwrap_or(false),
message: payload.message,
percentage: payload.percentage.map(|p| p as usize),
last_update_at: cx.background_executor().now(),
@@ -4119,7 +4135,7 @@ impl LspStore {
LanguageServerProgress {
title: None,
is_disk_based_diagnostics_progress: false,
is_cancellable: false,
is_cancellable: payload.is_cancellable.unwrap_or(false),
message: payload.message,
percentage: payload.percentage.map(|p| p as usize),
last_update_at: cx.background_executor().now(),
@@ -4620,6 +4636,7 @@ impl LspStore {
token,
message: report.message,
percentage: report.percentage,
is_cancellable: report.cancellable,
},
),
})
@@ -4653,6 +4670,7 @@ impl LspStore {
title: progress.title,
message: progress.message,
percentage: progress.percentage.map(|p| p as u32),
is_cancellable: Some(progress.is_cancellable),
}),
})
}
@@ -4683,6 +4701,9 @@ impl LspStore {
if progress.percentage.is_some() {
entry.percentage = progress.percentage;
}
if progress.is_cancellable != entry.is_cancellable {
entry.is_cancellable = progress.is_cancellable;
}
cx.notify();
return true;
}
@@ -5153,22 +5174,52 @@ impl LspStore {
mut cx: AsyncAppContext,
) -> Result<proto::Ack> {
this.update(&mut cx, |this, cx| {
let buffers: Vec<_> = envelope
.payload
.buffer_ids
.into_iter()
.flat_map(|buffer_id| {
this.buffer_store
.read(cx)
.get(BufferId::new(buffer_id).log_err()?)
})
.collect();
this.restart_language_servers_for_buffers(buffers, cx)
let buffers = this.buffer_ids_to_buffers(envelope.payload.buffer_ids.into_iter(), cx);
this.restart_language_servers_for_buffers(buffers, cx);
})?;
Ok(proto::Ack {})
}
pub async fn handle_cancel_language_server_work(
this: Model<Self>,
envelope: TypedEnvelope<proto::CancelLanguageServerWork>,
mut cx: AsyncAppContext,
) -> Result<proto::Ack> {
this.update(&mut cx, |this, cx| {
if let Some(work) = envelope.payload.work {
match work {
proto::cancel_language_server_work::Work::Buffers(buffers) => {
let buffers =
this.buffer_ids_to_buffers(buffers.buffer_ids.into_iter(), cx);
this.cancel_language_server_work_for_buffers(buffers, cx);
}
proto::cancel_language_server_work::Work::LanguageServerWork(work) => {
let server_id = LanguageServerId::from_proto(work.language_server_id);
this.cancel_language_server_work(server_id, work.token, cx);
}
}
}
})?;
Ok(proto::Ack {})
}
fn buffer_ids_to_buffers(
&mut self,
buffer_ids: impl Iterator<Item = u64>,
cx: &mut ModelContext<Self>,
) -> Vec<Model<Buffer>> {
buffer_ids
.into_iter()
.flat_map(|buffer_id| {
self.buffer_store
.read(cx)
.get(BufferId::new(buffer_id).log_err()?)
})
.collect::<Vec<_>>()
}
async fn handle_apply_additional_edits_for_completion(
this: Model<Self>,
envelope: TypedEnvelope<proto::ApplyCompletionAdditionalEdits>,
@@ -5214,9 +5265,9 @@ impl LspStore {
.map(language::proto::serialize_transaction),
})
}
pub fn last_formatting_failure(&self) -> Option<&str> {
self.as_local()
.and_then(|local| local.last_formatting_failure.as_deref())
self.last_formatting_failure.as_deref()
}
pub fn environment_for_buffer(
@@ -5287,23 +5338,16 @@ impl LspStore {
cx.clone(),
)
.await;
lsp_store.update(&mut cx, |lsp_store, _| {
let local = lsp_store.as_local_mut().unwrap();
match &result {
Ok(_) => local.last_formatting_failure = None,
Err(error) => {
local.last_formatting_failure.replace(error.to_string());
}
}
lsp_store.update_last_formatting_failure(&result);
})?;
result
})
} else if let Some((client, project_id)) = self.upstream_client() {
let buffer_store = self.buffer_store();
cx.spawn(move |_, mut cx| async move {
let response = client
cx.spawn(move |lsp_store, mut cx| async move {
let result = client
.request(proto::FormatBuffers {
project_id,
trigger: trigger as i32,
@@ -5314,13 +5358,21 @@ impl LspStore {
})
.collect::<Result<_>>()?,
})
.await?
.transaction
.ok_or_else(|| anyhow!("missing transaction"))?;
.await
.and_then(|result| result.transaction.context("missing transaction"));
lsp_store.update(&mut cx, |lsp_store, _| {
lsp_store.update_last_formatting_failure(&result);
})?;
let transaction_response = result?;
buffer_store
.update(&mut cx, |buffer_store, cx| {
buffer_store.deserialize_project_transaction(response, push_to_history, cx)
buffer_store.deserialize_project_transaction(
transaction_response,
push_to_history,
cx,
)
})?
.await
})
@@ -5342,7 +5394,7 @@ impl LspStore {
buffers.insert(this.buffer_store.read(cx).get_existing(buffer_id)?);
}
let trigger = FormatTrigger::from_proto(envelope.payload.trigger);
Ok::<_, anyhow::Error>(this.format(buffers, false, trigger, FormatTarget::Buffer, cx))
anyhow::Ok(this.format(buffers, false, trigger, FormatTarget::Buffer, cx))
})??;
let project_transaction = format.await?;
@@ -5914,7 +5966,6 @@ impl LspStore {
let adapter = adapter.clone();
if let Some(this) = this.upgrade() {
adapter.process_diagnostics(&mut params);
// Everything else has to be on the server, Can we make it on the client?
this.update(&mut cx, |this, cx| {
this.update_diagnostics(
server_id,
@@ -6714,16 +6765,89 @@ impl LspStore {
buffers: impl IntoIterator<Item = Model<Buffer>>,
cx: &mut ModelContext<Self>,
) {
let servers = buffers
.into_iter()
.flat_map(|buffer| {
self.language_server_ids_for_buffer(buffer.read(cx), cx)
.into_iter()
})
.collect::<HashSet<_>>();
if let Some((client, project_id)) = self.upstream_client() {
let request = client.request(proto::CancelLanguageServerWork {
project_id,
work: Some(proto::cancel_language_server_work::Work::Buffers(
proto::cancel_language_server_work::Buffers {
buffer_ids: buffers
.into_iter()
.map(|b| b.read(cx).remote_id().to_proto())
.collect(),
},
)),
});
cx.background_executor()
.spawn(request)
.detach_and_log_err(cx);
} else {
let servers = buffers
.into_iter()
.flat_map(|buffer| {
self.language_server_ids_for_buffer(buffer.read(cx), cx)
.into_iter()
})
.collect::<HashSet<_>>();
for server_id in servers {
self.cancel_language_server_work(server_id, None, cx);
for server_id in servers {
self.cancel_language_server_work(server_id, None, cx);
}
}
}
pub(crate) fn cancel_language_server_work(
&mut self,
server_id: LanguageServerId,
token_to_cancel: Option<String>,
cx: &mut ModelContext<Self>,
) {
if let Some(local) = self.as_local() {
let status = self.language_server_statuses.get(&server_id);
let server = local.language_servers.get(&server_id);
if let Some((LanguageServerState::Running { server, .. }, status)) = server.zip(status)
{
for (token, progress) in &status.pending_work {
if let Some(token_to_cancel) = token_to_cancel.as_ref() {
if token != token_to_cancel {
continue;
}
}
if progress.is_cancellable {
server
.notify::<lsp::notification::WorkDoneProgressCancel>(
WorkDoneProgressCancelParams {
token: lsp::NumberOrString::String(token.clone()),
},
)
.ok();
}
if progress.is_cancellable {
server
.notify::<lsp::notification::WorkDoneProgressCancel>(
WorkDoneProgressCancelParams {
token: lsp::NumberOrString::String(token.clone()),
},
)
.ok();
}
}
}
} else if let Some((client, project_id)) = self.upstream_client() {
let request = client.request(proto::CancelLanguageServerWork {
project_id,
work: Some(
proto::cancel_language_server_work::Work::LanguageServerWork(
proto::cancel_language_server_work::LanguageServerWork {
language_server_id: server_id.to_proto(),
token: token_to_cancel,
},
),
),
});
cx.background_executor()
.spawn(request)
.detach_and_log_err(cx);
}
}
@@ -6854,47 +6978,6 @@ impl LspStore {
}
}
pub(crate) fn cancel_language_server_work(
&mut self,
server_id: LanguageServerId,
token_to_cancel: Option<String>,
_cx: &mut ModelContext<Self>,
) {
let Some(local) = self.as_local() else {
return;
};
let status = self.language_server_statuses.get(&server_id);
let server = local.language_servers.get(&server_id);
if let Some((LanguageServerState::Running { server, .. }, status)) = server.zip(status) {
for (token, progress) in &status.pending_work {
if let Some(token_to_cancel) = token_to_cancel.as_ref() {
if token != token_to_cancel {
continue;
}
}
if progress.is_cancellable {
server
.notify::<lsp::notification::WorkDoneProgressCancel>(
WorkDoneProgressCancelParams {
token: lsp::NumberOrString::String(token.clone()),
},
)
.ok();
}
if progress.is_cancellable {
server
.notify::<lsp::notification::WorkDoneProgressCancel>(
WorkDoneProgressCancelParams {
token: lsp::NumberOrString::String(token.clone()),
},
)
.ok();
}
}
}
}
pub fn wait_for_remote_buffer(
&mut self,
id: BufferId,
@@ -7284,6 +7367,18 @@ impl LspStore {
lsp_action,
})
}
fn update_last_formatting_failure<T>(&mut self, formatting_result: &anyhow::Result<T>) {
match &formatting_result {
Ok(_) => self.last_formatting_failure = None,
Err(error) => {
let error_string = format!("{error:#}");
log::error!("Formatting failed: {error_string}");
self.last_formatting_failure
.replace(error_string.lines().join(" "));
}
}
}
}
impl EventEmitter<LspStoreEvent> for LspStore {}

View File

@@ -827,7 +827,7 @@ impl Project {
ssh_proto.add_model_message_handler(Self::handle_toast);
ssh_proto.add_model_request_handler(Self::handle_language_server_prompt_request);
ssh_proto.add_model_message_handler(Self::handle_hide_toast);
ssh_proto.add_model_request_handler(BufferStore::handle_update_buffer);
ssh_proto.add_model_request_handler(Self::handle_update_buffer_from_ssh);
BufferStore::init(&ssh_proto);
LspStore::init(&ssh_proto);
SettingsObserver::init(&ssh_proto);
@@ -1333,7 +1333,7 @@ impl Project {
}
pub fn host(&self) -> Option<&Collaborator> {
self.collaborators.values().find(|c| c.replica_id == 0)
self.collaborators.values().find(|c| c.is_host)
}
pub fn set_worktrees_reordered(&mut self, worktrees_reordered: bool, cx: &mut AppContext) {
@@ -3420,7 +3420,7 @@ impl Project {
buffer: &Model<Buffer>,
version: Option<clock::Global>,
cx: &AppContext,
) -> Task<Result<Blame>> {
) -> Task<Result<Option<Blame>>> {
self.buffer_store.read(cx).blame_buffer(buffer, version, cx)
}
@@ -3495,7 +3495,7 @@ impl Project {
.collaborators
.remove(&old_peer_id)
.ok_or_else(|| anyhow!("received UpdateProjectCollaborator for unknown peer"))?;
let is_host = collaborator.replica_id == 0;
let is_host = collaborator.is_host;
this.collaborators.insert(new_peer_id, collaborator);
log::info!("peer {} became {}", old_peer_id, new_peer_id,);
@@ -3653,6 +3653,24 @@ impl Project {
})?
}
async fn handle_update_buffer_from_ssh(
this: Model<Self>,
envelope: TypedEnvelope<proto::UpdateBuffer>,
cx: AsyncAppContext,
) -> Result<proto::Ack> {
let buffer_store = this.read_with(&cx, |this, cx| {
if let Some(remote_id) = this.remote_id() {
let mut payload = envelope.payload.clone();
payload.project_id = remote_id;
cx.background_executor()
.spawn(this.client.request(payload))
.detach_and_log_err(cx);
}
this.buffer_store.clone()
})?;
BufferStore::handle_update_buffer(buffer_store, envelope, cx).await
}
async fn handle_update_buffer(
this: Model<Self>,
envelope: TypedEnvelope<proto::UpdateBuffer>,
@@ -4255,17 +4273,6 @@ impl Completion {
}
}
#[derive(Debug)]
pub struct NoRepositoryError {}
impl std::fmt::Display for NoRepositoryError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "no git repository for worktree found")
}
}
impl std::error::Error for NoRepositoryError {}
pub fn sort_worktree_entries(entries: &mut [Entry]) {
entries.sort_by(|entry_a, entry_b| {
compare_paths(

View File

@@ -30,7 +30,7 @@ use project::{
relativize_path, Entry, EntryKind, Fs, Project, ProjectEntryId, ProjectPath, Worktree,
WorktreeId,
};
use project_panel_settings::{ProjectPanelDockPosition, ProjectPanelSettings};
use project_panel_settings::{ProjectPanelDockPosition, ProjectPanelSettings, ShowIndentGuides};
use serde::{Deserialize, Serialize};
use smallvec::SmallVec;
use std::{
@@ -3043,7 +3043,8 @@ impl Render for ProjectPanel {
let has_worktree = !self.visible_entries.is_empty();
let project = self.project.read(cx);
let indent_size = ProjectPanelSettings::get_global(cx).indent_size;
let indent_guides = ProjectPanelSettings::get_global(cx).indent_guides;
let show_indent_guides =
ProjectPanelSettings::get_global(cx).indent_guides.show == ShowIndentGuides::Always;
let is_local = project.is_local();
if has_worktree {
@@ -3136,7 +3137,7 @@ impl Render for ProjectPanel {
}
}),
)
.track_focus(&self.focus_handle)
.track_focus(&self.focus_handle(cx))
.child(
uniform_list(cx.view().clone(), "entries", item_count, {
|this, range, cx| {
@@ -3147,7 +3148,7 @@ impl Render for ProjectPanel {
items
}
})
.when(indent_guides, |list| {
.when(show_indent_guides, |list| {
list.with_decoration(
ui::indent_guides(
cx.view().clone(),
@@ -3268,7 +3269,7 @@ impl Render for ProjectPanel {
.id("empty-project_panel")
.size_full()
.p_4()
.track_focus(&self.focus_handle)
.track_focus(&self.focus_handle(cx))
.child(
Button::new("open_project", "Open a project")
.full_width()

View File

@@ -11,6 +11,13 @@ pub enum ProjectPanelDockPosition {
Right,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ShowIndentGuides {
Always,
Never,
}
#[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
pub struct ProjectPanelSettings {
pub button: bool,
@@ -20,12 +27,23 @@ pub struct ProjectPanelSettings {
pub folder_icons: bool,
pub git_status: bool,
pub indent_size: f32,
pub indent_guides: bool,
pub indent_guides: IndentGuidesSettings,
pub auto_reveal_entries: bool,
pub auto_fold_dirs: bool,
pub scrollbar: ScrollbarSettings,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
pub struct IndentGuidesSettings {
pub show: ShowIndentGuides,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
pub struct IndentGuidesSettingsContent {
/// When to show the scrollbar in the project panel.
pub show: Option<ShowIndentGuides>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
pub struct ScrollbarSettings {
/// When to show the scrollbar in the project panel.
@@ -72,10 +90,6 @@ pub struct ProjectPanelSettingsContent {
///
/// Default: 20
pub indent_size: Option<f32>,
/// Whether to show indent guides in the project panel.
///
/// Default: true
pub indent_guides: Option<bool>,
/// Whether to reveal it in the project panel automatically,
/// when a corresponding project entry becomes active.
/// Gitignored entries are never auto revealed.
@@ -89,6 +103,8 @@ pub struct ProjectPanelSettingsContent {
pub auto_fold_dirs: Option<bool>,
/// Scrollbar-related settings
pub scrollbar: Option<ScrollbarSettingsContent>,
/// Settings related to indent guides in the project panel.
pub indent_guides: Option<IndentGuidesSettingsContent>,
}
impl Settings for ProjectPanelSettings {

View File

@@ -289,7 +289,12 @@ message Envelope {
ActiveToolchainResponse active_toolchain_response = 277;
GetPathMetadata get_path_metadata = 278;
GetPathMetadataResponse get_path_metadata_response = 279; // current max
GetPathMetadataResponse get_path_metadata_response = 279;
GetPanicFiles get_panic_files = 280;
GetPanicFilesResponse get_panic_files_response = 281;
CancelLanguageServerWork cancel_language_server_work = 282; // current max
}
reserved 87 to 88;
@@ -1254,12 +1259,14 @@ message LspWorkStart {
optional string title = 4;
optional string message = 2;
optional uint32 percentage = 3;
optional bool is_cancellable = 5;
}
message LspWorkProgress {
string token = 1;
optional string message = 2;
optional uint32 percentage = 3;
optional bool is_cancellable = 4;
}
message LspWorkEnd {
@@ -1721,6 +1728,7 @@ message Collaborator {
PeerId peer_id = 1;
uint32 replica_id = 2;
uint64 user_id = 3;
bool is_host = 4;
}
message User {
@@ -2116,10 +2124,16 @@ message CommitPermalink {
}
message BlameBufferResponse {
repeated BlameEntry entries = 1;
repeated CommitMessage messages = 2;
repeated CommitPermalink permalinks = 3;
optional string remote_url = 4;
message BlameResponse {
repeated BlameEntry entries = 1;
repeated CommitMessage messages = 2;
repeated CommitPermalink permalinks = 3;
optional string remote_url = 4;
}
optional BlameResponse blame_response = 5;
reserved 1 to 4;
}
message MultiLspQuery {
@@ -2482,5 +2496,29 @@ message UpdateGitBranch {
uint64 project_id = 1;
string branch_name = 2;
ProjectPath repository = 3;
}
message GetPanicFiles {
}
message GetPanicFilesResponse {
repeated string file_contents = 2;
}
message CancelLanguageServerWork {
uint64 project_id = 1;
oneof work {
Buffers buffers = 2;
LanguageServerWork language_server_work = 3;
}
message Buffers {
repeated uint64 buffer_ids = 2;
}
message LanguageServerWork {
uint64 language_server_id = 1;
optional string token = 2;
}
}

View File

@@ -104,7 +104,19 @@ impl ErrorExt for anyhow::Error {
if let Some(rpc_error) = self.downcast_ref::<RpcError>() {
rpc_error.to_proto()
} else {
ErrorCode::Internal.message(format!("{}", self)).to_proto()
ErrorCode::Internal
.message(
format!("{self:#}")
.lines()
.fold(String::new(), |mut message, line| {
if !message.is_empty() {
message.push(' ');
}
message.push_str(line);
message
}),
)
.to_proto()
}
}

View File

@@ -363,7 +363,10 @@ messages!(
(ActiveToolchain, Foreground),
(ActiveToolchainResponse, Foreground),
(GetPathMetadata, Background),
(GetPathMetadataResponse, Background)
(GetPathMetadataResponse, Background),
(GetPanicFiles, Background),
(GetPanicFilesResponse, Background),
(CancelLanguageServerWork, Foreground),
);
request_messages!(
@@ -483,7 +486,9 @@ request_messages!(
(ListToolchains, ListToolchainsResponse),
(ActivateToolchain, Ack),
(ActiveToolchain, ActiveToolchainResponse),
(GetPathMetadata, GetPathMetadataResponse)
(GetPathMetadata, GetPathMetadataResponse),
(GetPanicFiles, GetPanicFilesResponse),
(CancelLanguageServerWork, Ack),
);
entity_messages!(
@@ -566,7 +571,8 @@ entity_messages!(
ListToolchains,
ActivateToolchain,
ActiveToolchain,
GetPathMetadata
GetPathMetadata,
CancelLanguageServerWork,
);
entity_messages!(

View File

@@ -149,7 +149,7 @@ impl Render for DisconnectedOverlay {
};
div()
.track_focus(&self.focus_handle)
.track_focus(&self.focus_handle(cx))
.elevation_3(cx)
.on_action(cx.listener(Self::cancel))
.occlude()

View File

@@ -1,7 +1,7 @@
pub mod disconnected_overlay;
mod remote_servers;
mod ssh_connections;
pub use ssh_connections::open_ssh_project;
pub use ssh_connections::{is_connecting_over_ssh, open_ssh_project};
use disconnected_overlay::DisconnectedOverlay;
use fuzzy::{StringMatch, StringMatchCandidate};

View File

@@ -1204,7 +1204,7 @@ impl RemoteServerProjects {
Modal::new("remote-projects", Some(self.scroll_handle.clone()))
.header(
ModalHeader::new()
.child(Headline::new("Remote Projects (alpha)").size(HeadlineSize::XSmall)),
.child(Headline::new("Remote Projects (beta)").size(HeadlineSize::XSmall)),
)
.section(
Section::new().padded(false).child(
@@ -1266,7 +1266,7 @@ impl Render for RemoteServerProjects {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
self.selectable_items.reset();
div()
.track_focus(&self.focus_handle)
.track_focus(&self.focus_handle(cx))
.elevation_3(cx)
.w(rems(34.))
.key_context("RemoteServerModal")

View File

@@ -14,7 +14,7 @@ use gpui::{AppContext, Model};
use language::CursorShape;
use markdown::{Markdown, MarkdownStyle};
use release_channel::{AppVersion, ReleaseChannel};
use remote::ssh_session::ServerBinary;
use remote::ssh_session::{ServerBinary, ServerVersion};
use remote::{SshConnectionOptions, SshPlatform, SshRemoteClient};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -446,7 +446,7 @@ impl remote::SshClientDelegate for SshClientDelegate {
platform: SshPlatform,
upload_binary_over_ssh: bool,
cx: &mut AsyncAppContext,
) -> oneshot::Receiver<Result<(ServerBinary, SemanticVersion)>> {
) -> oneshot::Receiver<Result<(ServerBinary, ServerVersion)>> {
let (tx, rx) = oneshot::channel();
let this = self.clone();
cx.spawn(|mut cx| async move {
@@ -491,7 +491,7 @@ impl SshClientDelegate {
platform: SshPlatform,
upload_binary_via_ssh: bool,
cx: &mut AsyncAppContext,
) -> Result<(ServerBinary, SemanticVersion)> {
) -> Result<(ServerBinary, ServerVersion)> {
let (version, release_channel) = cx.update(|cx| {
let version = AppVersion::global(cx);
let channel = ReleaseChannel::global(cx);
@@ -505,7 +505,10 @@ impl SshClientDelegate {
let result = self.build_local(cx, platform, version).await?;
// Fall through to a remote binary if we're not able to compile a local binary
if let Some((path, version)) = result {
return Ok((ServerBinary::LocalBinary(path), version));
return Ok((
ServerBinary::LocalBinary(path),
ServerVersion::Semantic(version),
));
}
}
@@ -540,9 +543,12 @@ impl SshClientDelegate {
)
})?;
Ok((ServerBinary::LocalBinary(binary_path), version))
Ok((
ServerBinary::LocalBinary(binary_path),
ServerVersion::Semantic(version),
))
} else {
let (request_url, request_body) = AutoUpdater::get_remote_server_release_url(
let (release, request_body) = AutoUpdater::get_remote_server_release_url(
platform.os,
platform.arch,
release_channel,
@@ -560,9 +566,14 @@ impl SshClientDelegate {
)
})?;
let version = release
.version
.parse::<SemanticVersion>()
.map(ServerVersion::Semantic)
.unwrap_or_else(|_| ServerVersion::Commit(release.version));
Ok((
ServerBinary::ReleaseUrl {
url: request_url,
url: release.url,
body: request_body,
},
version,
@@ -678,6 +689,10 @@ impl SshClientDelegate {
}
}
pub fn is_connecting_over_ssh(workspace: &Workspace, cx: &AppContext) -> bool {
workspace.active_modal::<SshConnectionModal>(cx).is_some()
}
pub fn connect_over_ssh(
unique_identifier: String,
connection_options: SshConnectionOptions,

View File

@@ -24,6 +24,7 @@ collections.workspace = true
fs.workspace = true
futures.workspace = true
gpui.workspace = true
itertools.workspace = true
log.workspace = true
parking_lot.workspace = true
prost.workspace = true

View File

@@ -20,6 +20,7 @@ use gpui::{
AppContext, AsyncAppContext, BorrowAppContext, Context, EventEmitter, Global, Model,
ModelContext, SemanticVersion, Task, WeakModel,
};
use itertools::Itertools;
use parking_lot::Mutex;
use rpc::{
proto::{self, build_typed_envelope, Envelope, EnvelopedMessage, PeerId, RequestMessage},
@@ -33,8 +34,7 @@ use smol::{
use std::{
any::TypeId,
collections::VecDeque,
ffi::OsStr,
fmt,
fmt, iter,
ops::ControlFlow,
path::{Path, PathBuf},
sync::{
@@ -69,6 +69,18 @@ pub struct SshConnectionOptions {
pub upload_binary_over_ssh: bool,
}
#[macro_export]
macro_rules! shell_script {
($fmt:expr, $($name:ident = $arg:expr),+ $(,)?) => {{
format!(
$fmt,
$(
$name = shlex::try_quote($arg).unwrap()
),+
)
}};
}
impl SshConnectionOptions {
pub fn parse_command_line(input: &str) -> Result<Self> {
let input = input.trim_start_matches("ssh ");
@@ -227,6 +239,20 @@ pub enum ServerBinary {
ReleaseUrl { url: String, body: String },
}
pub enum ServerVersion {
Semantic(SemanticVersion),
Commit(String),
}
impl std::fmt::Display for ServerVersion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Semantic(version) => write!(f, "{}", version),
Self::Commit(commit) => write!(f, "{}", commit),
}
}
}
pub trait SshClientDelegate: Send + Sync {
fn ask_password(
&self,
@@ -243,19 +269,31 @@ pub trait SshClientDelegate: Send + Sync {
platform: SshPlatform,
upload_binary_over_ssh: bool,
cx: &mut AsyncAppContext,
) -> oneshot::Receiver<Result<(ServerBinary, SemanticVersion)>>;
) -> oneshot::Receiver<Result<(ServerBinary, ServerVersion)>>;
fn set_status(&self, status: Option<&str>, cx: &mut AsyncAppContext);
}
impl SshSocket {
fn ssh_command<S: AsRef<OsStr>>(&self, program: S) -> process::Command {
// :WARNING: ssh unquotes arguments when executing on the remote :WARNING:
// e.g. $ ssh host sh -c 'ls -l' is equivalent to $ ssh host sh -c ls -l
// and passes -l as an argument to sh, not to ls.
// You need to do it like this: $ ssh host "sh -c 'ls -l /tmp'"
fn ssh_command(&self, program: &str, args: &[&str]) -> process::Command {
let mut command = process::Command::new("ssh");
let to_run = iter::once(&program)
.chain(args.iter())
.map(|token| shlex::try_quote(token).unwrap())
.join(" ");
self.ssh_options(&mut command)
.arg(self.connection_options.ssh_url())
.arg(program);
.arg(to_run);
command
}
fn shell_script(&self, script: impl AsRef<str>) -> process::Command {
return self.ssh_command("sh", &["-c", script.as_ref()]);
}
fn ssh_options<'a>(&self, command: &'a mut process::Command) -> &'a mut process::Command {
command
.stdin(Stdio::piped())
@@ -276,7 +314,7 @@ impl SshSocket {
}
}
async fn run_cmd(command: &mut process::Command) -> Result<String> {
async fn run_cmd(mut command: process::Command) -> Result<String> {
let output = command.output().await?;
if output.status.success() {
Ok(String::from_utf8_lossy(&output.stdout).to_string())
@@ -1203,7 +1241,7 @@ impl RemoteConnection for SshRemoteConnection {
}
let socket = self.socket.clone();
run_cmd(socket.ssh_command(&remote_binary_path).arg("version")).await?;
run_cmd(socket.ssh_command(&remote_binary_path.to_string_lossy(), &["version"])).await?;
Ok(remote_binary_path)
}
@@ -1220,22 +1258,33 @@ impl RemoteConnection for SshRemoteConnection {
) -> Task<Result<i32>> {
delegate.set_status(Some("Starting proxy"), cx);
let mut start_proxy_command = format!(
"RUST_LOG={} {} {:?} proxy --identifier {}",
std::env::var("RUST_LOG").unwrap_or_default(),
std::env::var("RUST_BACKTRACE")
.map(|b| { format!("RUST_BACKTRACE={}", b) })
.unwrap_or_default(),
remote_binary_path,
unique_identifier,
let mut start_proxy_command = shell_script!(
"exec {binary_path} proxy --identifier {identifier}",
binary_path = &remote_binary_path.to_string_lossy(),
identifier = &unique_identifier,
);
if let Some(rust_log) = std::env::var("RUST_LOG").ok() {
start_proxy_command = format!(
"RUST_LOG={} {}",
shlex::try_quote(&rust_log).unwrap(),
start_proxy_command
)
}
if let Some(rust_backtrace) = std::env::var("RUST_BACKTRACE").ok() {
start_proxy_command = format!(
"RUST_BACKTRACE={} {}",
shlex::try_quote(&rust_backtrace).unwrap(),
start_proxy_command
)
}
if reconnect {
start_proxy_command.push_str(" --reconnect");
}
let ssh_proxy_process = match self
.socket
.ssh_command(start_proxy_command)
.shell_script(start_proxy_command)
// IMPORTANT: we kill this process when we drop the task that uses it.
.kill_on_drop(true)
.spawn()
@@ -1274,6 +1323,7 @@ impl SshRemoteConnection {
) -> Result<Self> {
use futures::AsyncWriteExt as _;
use futures::{io::BufReader, AsyncBufReadExt as _};
use smol::net::unix::UnixStream;
use smol::{fs::unix::PermissionsExt as _, net::unix::UnixListener};
use util::ResultExt as _;
@@ -1290,6 +1340,9 @@ impl SshRemoteConnection {
let listener =
UnixListener::bind(&askpass_socket).context("failed to create askpass socket")?;
let (askpass_kill_master_tx, askpass_kill_master_rx) = oneshot::channel::<UnixStream>();
let mut kill_tx = Some(askpass_kill_master_tx);
let askpass_task = cx.spawn({
let delegate = delegate.clone();
|mut cx| async move {
@@ -1313,6 +1366,11 @@ impl SshRemoteConnection {
.log_err()
{
stream.write_all(password.as_bytes()).await.log_err();
} else {
if let Some(kill_tx) = kill_tx.take() {
kill_tx.send(stream).log_err();
break;
}
}
}
}
@@ -1333,6 +1391,7 @@ impl SshRemoteConnection {
// the connection and keep it open, allowing other ssh commands to reuse it
// via a control socket.
let socket_path = temp_dir.path().join("ssh.sock");
let mut master_process = process::Command::new("ssh")
.stdin(Stdio::null())
.stdout(Stdio::piped())
@@ -1355,20 +1414,28 @@ impl SshRemoteConnection {
// Wait for this ssh process to close its stdout, indicating that authentication
// has completed.
let stdout = master_process.stdout.as_mut().unwrap();
let mut stdout = master_process.stdout.take().unwrap();
let mut output = Vec::new();
let connection_timeout = Duration::from_secs(10);
let result = select_biased! {
_ = askpass_opened_rx.fuse() => {
// If the askpass script has opened, that means the user is typing
// their password, in which case we don't want to timeout anymore,
// since we know a connection has been established.
stdout.read_to_end(&mut output).await?;
Ok(())
select_biased! {
stream = askpass_kill_master_rx.fuse() => {
master_process.kill().ok();
drop(stream);
Err(anyhow!("SSH connection canceled"))
}
// If the askpass script has opened, that means the user is typing
// their password, in which case we don't want to timeout anymore,
// since we know a connection has been established.
result = stdout.read_to_end(&mut output).fuse() => {
result?;
Ok(())
}
}
}
result = stdout.read_to_end(&mut output).fuse() => {
result?;
_ = stdout.read_to_end(&mut output).fuse() => {
Ok(())
}
_ = futures::FutureExt::fuse(smol::Timer::after(connection_timeout)) => {
@@ -1399,8 +1466,8 @@ impl SshRemoteConnection {
socket_path,
};
let os = run_cmd(socket.ssh_command("uname").arg("-s")).await?;
let arch = run_cmd(socket.ssh_command("uname").arg("-m")).await?;
let os = run_cmd(socket.ssh_command("uname", &["-s"])).await?;
let arch = run_cmd(socket.ssh_command("uname", &["-m"])).await?;
let os = match os.trim() {
"Darwin" => "macos",
@@ -1598,14 +1665,9 @@ impl SshRemoteConnection {
}
async fn get_ssh_source_port(&self) -> Result<String> {
let output = run_cmd(
self.socket
.ssh_command("sh")
.arg("-c")
.arg(r#""echo $SSH_CLIENT | cut -d' ' -f2""#),
)
.await
.context("failed to get source port from SSH_CLIENT on host")?;
let output = run_cmd(self.socket.shell_script("echo $SSH_CLIENT | cut -d' ' -f2"))
.await
.context("failed to get source port from SSH_CLIENT on host")?;
Ok(output.trim().to_string())
}
@@ -1616,13 +1678,13 @@ impl SshRemoteConnection {
.ok_or_else(|| anyhow!("Lock file path has no parent directory"))?;
let script = format!(
r#"'mkdir -p "{parent_dir}" && [ ! -f "{lock_file}" ] && echo "{content}" > "{lock_file}" && echo "created" || echo "exists"'"#,
r#"mkdir -p "{parent_dir}" && [ ! -f "{lock_file}" ] && echo "{content}" > "{lock_file}" && echo "created" || echo "exists""#,
parent_dir = parent_dir.display(),
lock_file = lock_file.display(),
content = content,
);
let output = run_cmd(self.socket.ssh_command("sh").arg("-c").arg(&script))
let output = run_cmd(self.socket.shell_script(&script))
.await
.with_context(|| format!("failed to create a lock file at {:?}", lock_file))?;
@@ -1630,7 +1692,7 @@ impl SshRemoteConnection {
}
fn generate_stale_check_script(lock_file: &Path, max_age: u64) -> String {
format!(
shell_script!(
r#"
if [ ! -f "{lock_file}" ]; then
echo "lock file does not exist"
@@ -1658,18 +1720,15 @@ impl SshRemoteConnection {
else
echo "recent"
fi"#,
lock_file = lock_file.display(),
max_age = max_age
lock_file = &lock_file.to_string_lossy(),
max_age = &max_age.to_string()
)
}
async fn is_lock_stale(&self, lock_file: &Path, max_age: &Duration) -> Result<bool> {
let script = format!(
"'{}'",
Self::generate_stale_check_script(lock_file, max_age.as_secs())
);
let script = Self::generate_stale_check_script(lock_file, max_age.as_secs());
let output = run_cmd(self.socket.ssh_command("sh").arg("-c").arg(&script))
let output = run_cmd(self.socket.shell_script(script))
.await
.with_context(|| {
format!("failed to check whether lock file {:?} is stale", lock_file)
@@ -1682,9 +1741,12 @@ impl SshRemoteConnection {
}
async fn remove_lock_file(&self, lock_file: &Path) -> Result<()> {
run_cmd(self.socket.ssh_command("rm").arg("-f").arg(lock_file))
.await
.context("failed to remove lock file")?;
run_cmd(
self.socket
.ssh_command("rm", &["-f", &lock_file.to_string_lossy()]),
)
.await
.context("failed to remove lock file")?;
Ok(())
}
@@ -1696,52 +1758,77 @@ impl SshRemoteConnection {
cx: &mut AsyncAppContext,
) -> Result<()> {
if std::env::var("ZED_USE_CACHED_REMOTE_SERVER").is_ok() {
if let Ok(installed_version) =
run_cmd(self.socket.ssh_command(dst_path).arg("version")).await
if let Ok(installed_version) = run_cmd(
self.socket
.ssh_command(&dst_path.to_string_lossy(), &["version"]),
)
.await
{
log::info!("using cached server binary version {}", installed_version);
return Ok(());
}
}
if self.is_binary_in_use(dst_path).await? {
log::info!("server binary is opened by another process. not updating");
delegate.set_status(
Some("Skipping update of remote development server, since it's still in use"),
cx,
);
return Ok(());
if cfg!(not(debug_assertions)) {
// When we're not in dev mode, we don't want to switch out the binary if it's
// still open.
// In dev mode, that's fine, since we often kill Zed processes with Ctrl-C and want
// to still replace the binary.
if self.is_binary_in_use(dst_path).await? {
log::info!("server binary is opened by another process. not updating");
delegate.set_status(
Some("Skipping update of remote development server, since it's still in use"),
cx,
);
return Ok(());
}
}
let upload_binary_over_ssh = self.socket.connection_options.upload_binary_over_ssh;
let (binary, version) = delegate
let (binary, new_server_version) = delegate
.get_server_binary(platform, upload_binary_over_ssh, cx)
.await??;
let mut remote_version = None;
if cfg!(not(debug_assertions)) {
if let Ok(installed_version) =
run_cmd(self.socket.ssh_command(dst_path).arg("version")).await
let installed_version = if let Ok(version_output) = run_cmd(
self.socket
.ssh_command(&dst_path.to_string_lossy(), &["version"]),
)
.await
{
if let Ok(version) = installed_version.trim().parse::<SemanticVersion>() {
remote_version = Some(version);
if let Ok(version) = version_output.trim().parse::<SemanticVersion>() {
Some(ServerVersion::Semantic(version))
} else {
log::warn!("failed to parse version of remote server: {installed_version:?}",);
Some(ServerVersion::Commit(version_output.trim().to_string()))
}
}
} else {
None
};
if let Some(remote_version) = remote_version {
if remote_version == version {
log::info!("remote development server present and matching client version");
return Ok(());
} else if remote_version > version {
let error = anyhow!("The version of the remote server ({}) is newer than the Zed version ({}). Please update Zed.", remote_version, version);
return Err(error);
} else {
log::info!(
"remote development server has older version: {}. updating...",
remote_version
);
if let Some(installed_version) = installed_version {
use ServerVersion::*;
match (installed_version, new_server_version) {
(Semantic(installed), Semantic(new)) if installed == new => {
log::info!("remote development server present and matching client version");
return Ok(());
}
(Semantic(installed), Semantic(new)) if installed > new => {
let error = anyhow!("The version of the remote server ({}) is newer than the Zed version ({}). Please update Zed.", installed, new);
return Err(error);
}
(Commit(installed), Commit(new)) if installed == new => {
log::info!(
"remote development server present and matching client version {}",
installed
);
return Ok(());
}
(installed, _) => {
log::info!(
"remote development server has version: {}. updating...",
installed
);
}
}
}
}
@@ -1759,26 +1846,25 @@ impl SshRemoteConnection {
}
async fn is_binary_in_use(&self, binary_path: &Path) -> Result<bool> {
let script = format!(
r#"'
let script = shell_script!(
r#"
if command -v lsof >/dev/null 2>&1; then
if lsof "{}" >/dev/null 2>&1; then
if lsof "{binary_path}" >/dev/null 2>&1; then
echo "in_use"
exit 0
fi
elif command -v fuser >/dev/null 2>&1; then
if fuser "{}" >/dev/null 2>&1; then
if fuser "{binary_path}" >/dev/null 2>&1; then
echo "in_use"
exit 0
fi
fi
echo "not_in_use"
'"#,
binary_path.display(),
binary_path.display(),
"#,
binary_path = &binary_path.to_string_lossy(),
);
let output = run_cmd(self.socket.ssh_command("sh").arg("-c").arg(script))
let output = run_cmd(self.socket.shell_script(script))
.await
.context("failed to check if binary is in use")?;
@@ -1797,31 +1883,32 @@ impl SshRemoteConnection {
dst_path_gz.set_extension("gz");
if let Some(parent) = dst_path.parent() {
run_cmd(self.socket.ssh_command("mkdir").arg("-p").arg(parent)).await?;
run_cmd(
self.socket
.ssh_command("mkdir", &["-p", &parent.to_string_lossy()]),
)
.await?;
}
delegate.set_status(Some("Downloading remote development server on host"), cx);
let script = format!(
let script = shell_script!(
r#"
if command -v wget >/dev/null 2>&1; then
wget --max-redirect=5 --method=GET --header="Content-Type: application/json" --body-data='{}' '{}' -O '{}' && echo "wget"
elif command -v curl >/dev/null 2>&1; then
curl -L -X GET -H "Content-Type: application/json" -d '{}' '{}' -o '{}' && echo "curl"
if command -v curl >/dev/null 2>&1; then
curl -f -L -X GET -H "Content-Type: application/json" -d {body} {url} -o {dst_path} && echo "curl"
elif command -v wget >/dev/null 2>&1; then
wget --max-redirect=5 --method=GET --header="Content-Type: application/json" --body-data={body} {url} -O {dst_path} && echo "wget"
else
echo "Neither curl nor wget is available" >&2
exit 1
fi
"#,
body.replace("'", r#"\'"#),
url,
dst_path_gz.display(),
body.replace("'", r#"\'"#),
url,
dst_path_gz.display(),
body = body,
url = url,
dst_path = &dst_path_gz.to_string_lossy(),
);
let output = run_cmd(self.socket.ssh_command("bash").arg("-c").arg(script))
let output = run_cmd(self.socket.shell_script(script))
.await
.context("Failed to download server binary")?;
@@ -1844,7 +1931,11 @@ impl SshRemoteConnection {
dst_path_gz.set_extension("gz");
if let Some(parent) = dst_path.parent() {
run_cmd(self.socket.ssh_command("mkdir").arg("-p").arg(parent)).await?;
run_cmd(
self.socket
.ssh_command("mkdir", &["-p", &parent.to_string_lossy()]),
)
.await?;
}
let src_stat = fs::metadata(&src_path).await?;
@@ -1872,20 +1963,16 @@ impl SshRemoteConnection {
delegate.set_status(Some("Extracting remote development server"), cx);
run_cmd(
self.socket
.ssh_command("gunzip")
.arg("--force")
.arg(&dst_path_gz),
.ssh_command("gunzip", &["-f", &dst_path_gz.to_string_lossy()]),
)
.await?;
let server_mode = 0o755;
delegate.set_status(Some("Marking remote development server executable"), cx);
run_cmd(
self.socket
.ssh_command("chmod")
.arg(format!("{:o}", server_mode))
.arg(dst_path),
)
run_cmd(self.socket.ssh_command(
"chmod",
&[&format!("{:o}", server_mode), &dst_path.to_string_lossy()],
))
.await?;
Ok(())
@@ -1966,77 +2053,97 @@ impl ChannelClient {
mut incoming_rx: mpsc::UnboundedReceiver<Envelope>,
cx: &AsyncAppContext,
) -> Task<Result<()>> {
cx.spawn(|cx| {
async move {
let peer_id = PeerId { owner_id: 0, id: 0 };
while let Some(incoming) = incoming_rx.next().await {
let Some(this) = this.upgrade() else {
return anyhow::Ok(());
};
if let Some(ack_id) = incoming.ack_id {
let mut buffer = this.buffer.lock();
while buffer.front().is_some_and(|msg| msg.id <= ack_id) {
buffer.pop_front();
cx.spawn(|cx| async move {
let peer_id = PeerId { owner_id: 0, id: 0 };
while let Some(incoming) = incoming_rx.next().await {
let Some(this) = this.upgrade() else {
return anyhow::Ok(());
};
if let Some(ack_id) = incoming.ack_id {
let mut buffer = this.buffer.lock();
while buffer.front().is_some_and(|msg| msg.id <= ack_id) {
buffer.pop_front();
}
}
if let Some(proto::envelope::Payload::FlushBufferedMessages(_)) = &incoming.payload
{
log::debug!(
"{}:ssh message received. name:FlushBufferedMessages",
this.name
);
{
let buffer = this.buffer.lock();
for envelope in buffer.iter() {
this.outgoing_tx
.lock()
.unbounded_send(envelope.clone())
.ok();
}
}
if let Some(proto::envelope::Payload::FlushBufferedMessages(_)) =
&incoming.payload
{
log::debug!("{}:ssh message received. name:FlushBufferedMessages", this.name);
{
let buffer = this.buffer.lock();
for envelope in buffer.iter() {
this.outgoing_tx.lock().unbounded_send(envelope.clone()).ok();
}
let mut envelope = proto::Ack {}.into_envelope(0, Some(incoming.id), None);
envelope.id = this.next_message_id.fetch_add(1, SeqCst);
this.outgoing_tx.lock().unbounded_send(envelope).ok();
continue;
}
this.max_received.store(incoming.id, SeqCst);
if let Some(request_id) = incoming.responding_to {
let request_id = MessageId(request_id);
let sender = this.response_channels.lock().remove(&request_id);
if let Some(sender) = sender {
let (tx, rx) = oneshot::channel();
if incoming.payload.is_some() {
sender.send((incoming, tx)).ok();
}
let mut envelope = proto::Ack{}.into_envelope(0, Some(incoming.id), None);
envelope.id = this.next_message_id.fetch_add(1, SeqCst);
this.outgoing_tx.lock().unbounded_send(envelope).ok();
continue;
rx.await.ok();
}
this.max_received.store(incoming.id, SeqCst);
if let Some(request_id) = incoming.responding_to {
let request_id = MessageId(request_id);
let sender = this.response_channels.lock().remove(&request_id);
if let Some(sender) = sender {
let (tx, rx) = oneshot::channel();
if incoming.payload.is_some() {
sender.send((incoming, tx)).ok();
}
rx.await.ok();
}
} else if let Some(envelope) =
build_typed_envelope(peer_id, Instant::now(), incoming)
{
let type_name = envelope.payload_type_name();
if let Some(future) = ProtoMessageHandlerSet::handle_message(
&this.message_handlers,
envelope,
this.clone().into(),
cx.clone(),
) {
log::debug!("{}:ssh message received. name:{type_name}", this.name);
cx.foreground_executor().spawn(async move {
} else if let Some(envelope) =
build_typed_envelope(peer_id, Instant::now(), incoming)
{
let type_name = envelope.payload_type_name();
if let Some(future) = ProtoMessageHandlerSet::handle_message(
&this.message_handlers,
envelope,
this.clone().into(),
cx.clone(),
) {
log::debug!("{}:ssh message received. name:{type_name}", this.name);
cx.foreground_executor()
.spawn(async move {
match future.await {
Ok(_) => {
log::debug!("{}:ssh message handled. name:{type_name}", this.name);
log::debug!(
"{}:ssh message handled. name:{type_name}",
this.name
);
}
Err(error) => {
log::error!(
"{}:error handling message. type:{type_name}, error:{error}", this.name,
"{}:error handling message. type:{}, error:{}",
this.name,
type_name,
format!("{error:#}").lines().fold(
String::new(),
|mut message, line| {
if !message.is_empty() {
message.push(' ');
}
message.push_str(line);
message
}
)
);
}
}
}).detach()
} else {
log::error!("{}:unhandled ssh message name:{type_name}", this.name);
}
})
.detach()
} else {
log::error!("{}:unhandled ssh message name:{type_name}", this.name);
}
}
anyhow::Ok(())
}
anyhow::Ok(())
})
}
@@ -2224,12 +2331,12 @@ mod fake {
},
select_biased, FutureExt, SinkExt, StreamExt,
};
use gpui::{AsyncAppContext, SemanticVersion, Task};
use gpui::{AsyncAppContext, Task};
use rpc::proto::Envelope;
use super::{
ChannelClient, RemoteConnection, ServerBinary, SshClientDelegate, SshConnectionOptions,
SshPlatform,
ChannelClient, RemoteConnection, ServerBinary, ServerVersion, SshClientDelegate,
SshConnectionOptions, SshPlatform,
};
pub(super) struct FakeRemoteConnection {
@@ -2349,7 +2456,7 @@ mod fake {
_: SshPlatform,
_: bool,
_: &mut AsyncAppContext,
) -> oneshot::Receiver<Result<(ServerBinary, SemanticVersion)>> {
) -> oneshot::Receiver<Result<(ServerBinary, ServerVersion)>> {
unreachable!()
}

View File

@@ -22,9 +22,10 @@ debug-embed = ["dep:rust-embed"]
test-support = ["fs/test-support"]
[dependencies]
async-watch.workspace = true
anyhow.workspace = true
async-watch.workspace = true
backtrace = "0.3"
chrono.workspace = true
clap.workspace = true
client.workspace = true
env_logger.workspace = true
@@ -39,8 +40,10 @@ languages.workspace = true
log.workspace = true
lsp.workspace = true
node_runtime.workspace = true
project.workspace = true
paths = { workspace = true }
project.workspace = true
proto.workspace = true
release_channel.workspace = true
remote.workspace = true
reqwest_client.workspace = true
rpc.workspace = true
@@ -50,6 +53,7 @@ serde_json.workspace = true
settings.workspace = true
shellexpand.workspace = true
smol.workspace = true
telemetry_events.workspace = true
util.workspace = true
worktree.workspace = true

View File

@@ -1,3 +1,5 @@
use std::process::Command;
const ZED_MANIFEST: &str = include_str!("../zed/Cargo.toml");
fn main() {
@@ -7,4 +9,23 @@ fn main() {
"cargo:rustc-env=ZED_PKG_VERSION={}",
zed_cargo_toml.package.unwrap().version.unwrap()
);
// If we're building this for nightly, we want to set the ZED_COMMIT_SHA
if let Some(release_channel) = std::env::var("ZED_RELEASE_CHANNEL").ok() {
if release_channel.as_str() == "nightly" {
// Populate git sha environment variable if git is available
println!("cargo:rerun-if-changed=../../.git/logs/HEAD");
if let Some(output) = Command::new("git")
.args(["rev-parse", "HEAD"])
.output()
.ok()
.filter(|output| output.status.success())
{
let git_sha = String::from_utf8_lossy(&output.stdout);
let git_sha = git_sha.trim();
println!("cargo:rustc-env=ZED_COMMIT_SHA={git_sha}");
}
}
}
}

View File

@@ -72,7 +72,12 @@ fn main() {
}
},
Some(Commands::Version) => {
println!("{}", env!("ZED_PKG_VERSION"));
if let Some(build_sha) = option_env!("ZED_COMMIT_SHA") {
println!("{}", build_sha);
} else {
println!("{}", env!("ZED_PKG_VERSION"));
}
std::process::exit(0);
}
None => {

View File

@@ -528,6 +528,172 @@ async fn test_remote_lsp(cx: &mut TestAppContext, server_cx: &mut TestAppContext
})
}
#[gpui::test]
async fn test_remote_cancel_language_server_work(
cx: &mut TestAppContext,
server_cx: &mut TestAppContext,
) {
let fs = FakeFs::new(server_cx.executor());
fs.insert_tree(
"/code",
json!({
"project1": {
".git": {},
"README.md": "# project 1",
"src": {
"lib.rs": "fn one() -> usize { 1 }"
}
},
}),
)
.await;
let (project, headless) = init_test(&fs, cx, server_cx).await;
fs.insert_tree(
"/code/project1/.zed",
json!({
"settings.json": r#"
{
"languages": {"Rust":{"language_servers":["rust-analyzer"]}},
"lsp": {
"rust-analyzer": {
"binary": {
"path": "~/.cargo/bin/rust-analyzer"
}
}
}
}"#
}),
)
.await;
cx.update_model(&project, |project, _| {
project.languages().register_test_language(LanguageConfig {
name: "Rust".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["rs".into()],
..Default::default()
},
..Default::default()
});
project.languages().register_fake_lsp_adapter(
"Rust",
FakeLspAdapter {
name: "rust-analyzer",
..Default::default()
},
)
});
let mut fake_lsp = server_cx.update(|cx| {
headless.read(cx).languages.register_fake_language_server(
LanguageServerName("rust-analyzer".into()),
Default::default(),
None,
)
});
cx.run_until_parked();
let worktree_id = project
.update(cx, |project, cx| {
project.find_or_create_worktree("/code/project1", true, cx)
})
.await
.unwrap()
.0
.read_with(cx, |worktree, _| worktree.id());
cx.run_until_parked();
let buffer = project
.update(cx, |project, cx| {
project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
})
.await
.unwrap();
cx.run_until_parked();
let mut fake_lsp = fake_lsp.next().await.unwrap();
// Cancelling all language server work for a given buffer
{
// Two operations, one cancellable and one not.
fake_lsp
.start_progress_with(
"another-token",
lsp::WorkDoneProgressBegin {
cancellable: Some(false),
..Default::default()
},
)
.await;
let progress_token = "the-progress-token";
fake_lsp
.start_progress_with(
progress_token,
lsp::WorkDoneProgressBegin {
cancellable: Some(true),
..Default::default()
},
)
.await;
cx.executor().run_until_parked();
project.update(cx, |project, cx| {
project.cancel_language_server_work_for_buffers([buffer.clone()], cx)
});
cx.executor().run_until_parked();
// Verify the cancellation was received on the server side
let cancel_notification = fake_lsp
.receive_notification::<lsp::notification::WorkDoneProgressCancel>()
.await;
assert_eq!(
cancel_notification.token,
lsp::NumberOrString::String(progress_token.into())
);
}
// Cancelling work by server_id and token
{
let server_id = fake_lsp.server.server_id();
let progress_token = "the-progress-token";
fake_lsp
.start_progress_with(
progress_token,
lsp::WorkDoneProgressBegin {
cancellable: Some(true),
..Default::default()
},
)
.await;
cx.executor().run_until_parked();
project.update(cx, |project, cx| {
project.cancel_language_server_work(server_id, Some(progress_token.into()), cx)
});
cx.executor().run_until_parked();
// Verify the cancellation was received on the server side
let cancel_notification = fake_lsp
.receive_notification::<lsp::notification::WorkDoneProgressCancel>()
.await;
assert_eq!(
cancel_notification.token,
lsp::NumberOrString::String(progress_token.into())
);
}
}
#[gpui::test]
async fn test_remote_reload(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
let fs = FakeFs::new(server_cx.executor());

View File

@@ -1,12 +1,13 @@
use crate::headless_project::HeadlessAppState;
use crate::HeadlessProject;
use anyhow::{anyhow, Context, Result};
use client::ProxySettings;
use chrono::Utc;
use client::{telemetry, ProxySettings};
use fs::{Fs, RealFs};
use futures::channel::mpsc;
use futures::{select, select_biased, AsyncRead, AsyncWrite, AsyncWriteExt, FutureExt, SinkExt};
use git::GitHostingProviderRegistry;
use gpui::{AppContext, Context as _, ModelContext, UpdateGlobal as _};
use gpui::{AppContext, Context as _, Model, ModelContext, UpdateGlobal as _};
use http_client::{read_proxy_from_env, Uri};
use language::LanguageRegistry;
use node_runtime::{NodeBinaryOptions, NodeRuntime};
@@ -21,19 +22,23 @@ use remote::{
};
use reqwest_client::ReqwestClient;
use rpc::proto::{self, Envelope, SSH_PROJECT_ID};
use rpc::{AnyProtoClient, TypedEnvelope};
use settings::{watch_config_file, Settings, SettingsStore};
use smol::channel::{Receiver, Sender};
use smol::io::AsyncReadExt;
use smol::Async;
use smol::{net::unix::UnixListener, stream::StreamExt as _};
use std::ffi::OsStr;
use std::ops::ControlFlow;
use std::{env, thread};
use std::{
io::Write,
mem,
path::{Path, PathBuf},
sync::Arc,
};
use telemetry_events::LocationData;
use util::ResultExt;
fn init_logging_proxy() {
@@ -131,16 +136,97 @@ fn init_panic_hook() {
backtrace.drain(0..=ix);
}
let thread = thread::current();
let thread_name = thread.name().unwrap_or("<unnamed>");
log::error!(
"panic occurred: {}\nBacktrace:\n{}",
payload,
backtrace.join("\n")
&payload,
(&backtrace).join("\n")
);
let panic_data = telemetry_events::Panic {
thread: thread_name.into(),
payload: payload.clone(),
location_data: info.location().map(|location| LocationData {
file: location.file().into(),
line: location.line(),
}),
app_version: format!(
"remote-server-{}",
option_env!("ZED_COMMIT_SHA").unwrap_or(&env!("ZED_PKG_VERSION"))
),
release_channel: release_channel::RELEASE_CHANNEL.display_name().into(),
os_name: telemetry::os_name(),
os_version: Some(telemetry::os_version()),
architecture: env::consts::ARCH.into(),
panicked_on: Utc::now().timestamp_millis(),
backtrace,
system_id: None, // Set on SSH client
installation_id: None, // Set on SSH client
session_id: "".to_string(), // Set on SSH client
};
if let Some(panic_data_json) = serde_json::to_string(&panic_data).log_err() {
let timestamp = chrono::Utc::now().format("%Y_%m_%d %H_%M_%S").to_string();
let panic_file_path = paths::logs_dir().join(format!("zed-{timestamp}.panic"));
let panic_file = std::fs::OpenOptions::new()
.append(true)
.create(true)
.open(&panic_file_path)
.log_err();
if let Some(mut panic_file) = panic_file {
writeln!(&mut panic_file, "{panic_data_json}").log_err();
panic_file.flush().log_err();
}
}
std::process::abort();
}));
}
fn handle_panic_requests(project: &Model<HeadlessProject>, client: &Arc<ChannelClient>) {
let client: AnyProtoClient = client.clone().into();
client.add_request_handler(
project.downgrade(),
|_, _: TypedEnvelope<proto::GetPanicFiles>, _cx| async move {
let mut children = smol::fs::read_dir(paths::logs_dir()).await?;
let mut panic_files = Vec::new();
while let Some(child) = children.next().await {
let child = child?;
let child_path = child.path();
if child_path.extension() != Some(OsStr::new("panic")) {
continue;
}
let filename = if let Some(filename) = child_path.file_name() {
filename.to_string_lossy()
} else {
continue;
};
if !filename.starts_with("zed") {
continue;
}
let file_contents = smol::fs::read_to_string(&child_path)
.await
.context("error reading panic file")?;
panic_files.push(file_contents);
// We've done what we can, delete the file
std::fs::remove_file(child_path)
.context("error removing panic")
.log_err();
}
anyhow::Ok(proto::GetPanicFilesResponse {
file_contents: panic_files,
})
},
);
}
struct ServerListeners {
stdin: UnixListener,
stdout: UnixListener,
@@ -368,7 +454,7 @@ pub fn execute_run(
HeadlessProject::new(
HeadlessAppState {
session,
session: session.clone(),
fs,
http_client,
node_runtime,
@@ -378,6 +464,8 @@ pub fn execute_run(
)
});
handle_panic_requests(&project, &session);
mem::forget(project);
});
log::info!("gpui app is shut down. quitting.");

View File

@@ -16,7 +16,7 @@ pub struct ImageView {
impl ImageView {
pub fn from(base64_encoded_data: &str) -> Result<Self> {
let bytes = BASE64_STANDARD.decode(base64_encoded_data)?;
let bytes = BASE64_STANDARD.decode(base64_encoded_data.trim())?;
let format = image::guess_format(&bytes)?;
let mut data = image::load_from_memory_with_format(&bytes, format)?.into_rgba8();

View File

@@ -327,7 +327,7 @@ impl Render for ProjectSearchView {
div()
.flex_1()
.size_full()
.track_focus(&self.focus_handle)
.track_focus(&self.focus_handle(cx))
.child(self.results_editor.clone())
} else {
let model = self.model.read(cx);
@@ -365,7 +365,7 @@ impl Render for ProjectSearchView {
.size_full()
.justify_center()
.bg(cx.theme().colors().editor_background)
.track_focus(&self.focus_handle)
.track_focus(&self.focus_handle(cx))
.child(
h_flex()
.size_full()

View File

@@ -58,13 +58,13 @@ impl<'a> Statement<'a> {
&mut remaining_sql_ptr,
);
remaining_sql = CStr::from_ptr(remaining_sql_ptr);
statement.raw_statements.push(raw_statement);
connection.last_error().with_context(|| {
format!("Prepare call failed for query:\n{}", query.as_ref())
})?;
remaining_sql = CStr::from_ptr(remaining_sql_ptr);
statement.raw_statements.push(raw_statement);
if !connection.can_write() && sqlite3_stmt_readonly(raw_statement) == 0 {
let sql = CStr::from_ptr(sqlite3_sql(raw_statement));

View File

@@ -425,7 +425,11 @@ impl PickerDelegate for TasksModalDelegate {
)
}
fn confirm_completion(&self, _: String) -> Option<String> {
fn confirm_completion(
&mut self,
_: String,
_: &mut ViewContext<Picker<Self>>,
) -> Option<String> {
let task_index = self.matches.get(self.selected_index())?.candidate_id;
let tasks = self.candidates.as_ref()?;
let (_, task) = tasks.get(task_index)?;

View File

@@ -222,13 +222,13 @@ pub struct HangReport {
pub installation_id: Option<String>,
}
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct LocationData {
pub file: String,
pub line: u32,
}
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Panic {
/// The name of the thread that panicked
pub thread: String,

View File

@@ -975,7 +975,7 @@ impl Render for TerminalView {
div()
.size_full()
.relative()
.track_focus(&self.focus_handle)
.track_focus(&self.focus_handle(cx))
.key_context(self.dispatch_context(cx))
.on_action(cx.listener(TerminalView::send_text))
.on_action(cx.listener(TerminalView::send_keystroke))

View File

@@ -282,6 +282,13 @@ impl TitleBar {
return Vec::new();
};
let is_connecting_to_project = self
.workspace
.update(cx, |workspace, cx| {
recent_projects::is_connecting_over_ssh(workspace, cx)
})
.unwrap_or(false);
let room = room.read(cx);
let project = self.project.read(cx);
let is_local = project.is_local() || project.is_via_ssh();
@@ -298,7 +305,7 @@ impl TitleBar {
let mut children = Vec::new();
if is_local && can_share_projects {
if is_local && can_share_projects && !is_connecting_to_project {
children.push(
Button::new(
"toggle_sharing",

View File

@@ -348,7 +348,7 @@ impl Render for ContextMenu {
.min_w(px(200.))
.max_h(vh(0.75, cx))
.overflow_y_scroll()
.track_focus(&self.focus_handle)
.track_focus(&self.focus_handle(cx))
.on_mouse_down_out(cx.listener(|this, _, cx| this.cancel(&menu::Cancel, cx)))
.key_context("menu")
.on_action(cx.listener(ContextMenu::select_first))

View File

@@ -242,7 +242,7 @@ impl PickerDelegate for BranchListDelegate {
BranchEntry::NewBranch { name: branch_name } => branch_name,
};
let worktree = project
.worktrees(cx)
.visible_worktrees(cx)
.next()
.context("worktree disappeared")?;
let repository = ProjectPath::root_path(worktree.read(cx).id());

View File

@@ -72,7 +72,7 @@ impl Render for WelcomePage {
h_flex()
.size_full()
.bg(cx.theme().colors().editor_background)
.track_focus(&self.focus_handle)
.track_focus(&self.focus_handle(cx))
.child(
v_flex()
.w_80()

View File

@@ -658,7 +658,7 @@ impl Render for Dock {
div()
.key_context(dispatch_context)
.track_focus(&self.focus_handle)
.track_focus(&self.focus_handle(cx))
.flex()
.bg(cx.theme().colors().panel_background)
.border_color(cx.theme().colors().border)
@@ -689,7 +689,7 @@ impl Render for Dock {
} else {
div()
.key_context(dispatch_context)
.track_focus(&self.focus_handle)
.track_focus(&self.focus_handle(cx))
}
}
}
@@ -826,8 +826,8 @@ pub mod test {
}
impl Render for TestPanel {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
div().id("test").track_focus(&self.focus_handle)
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
div().id("test").track_focus(&self.focus_handle(cx))
}
}

View File

@@ -1173,8 +1173,8 @@ pub mod test {
}
impl Render for TestItem {
fn render(&mut self, _: &mut ViewContext<Self>) -> impl IntoElement {
gpui::div().track_focus(&self.focus_handle)
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
gpui::div().track_focus(&self.focus_handle(cx))
}
}

View File

@@ -2574,7 +2574,7 @@ impl Render for Pane {
v_flex()
.key_context(key_context)
.track_focus(&self.focus_handle)
.track_focus(&self.focus_handle(cx))
.size_full()
.flex_none()
.overflow_hidden()

View File

@@ -4465,7 +4465,7 @@ impl Workspace {
self.modal_layer.read(cx).has_active_modal()
}
pub fn active_modal<V: ManagedView + 'static>(&mut self, cx: &AppContext) -> Option<View<V>> {
pub fn active_modal<V: ManagedView + 'static>(&self, cx: &AppContext) -> Option<View<V>> {
self.modal_layer.read(cx).active_modal()
}
@@ -5715,7 +5715,7 @@ pub fn join_in_room_project(
.read(cx)
.collaborators()
.values()
.find(|collaborator| collaborator.replica_id == 0)?;
.find(|collaborator| collaborator.is_host)?;
Some(collaborator.peer_id)
});

View File

@@ -2,7 +2,7 @@
description = "The fast, collaborative code editor."
edition = "2021"
name = "zed"
version = "0.160.0"
version = "0.159.7"
publish = false
license = "GPL-3.0-or-later"
authors = ["Zed Team <hi@zed.dev>"]
@@ -77,6 +77,7 @@ profiling.workspace = true
project.workspace = true
project_panel.workspace = true
project_symbols.workspace = true
proto.workspace = true
quick_action_bar.workspace = true
recent_projects.workspace = true
release_channel.workspace = true

View File

@@ -1 +1 @@
dev
stable

View File

@@ -332,7 +332,7 @@ fn main() {
telemetry.start(
system_id.as_ref().map(|id| id.to_string()),
installation_id.as_ref().map(|id| id.to_string()),
session_id,
session_id.clone(),
cx,
);
@@ -368,7 +368,9 @@ fn main() {
auto_update::init(client.http_client(), cx);
reliability::init(
client.http_client(),
system_id.as_ref().map(|id| id.to_string()),
installation_id.clone().map(|id| id.to_string()),
session_id.clone(),
cx,
);

View File

@@ -1,13 +1,14 @@
use anyhow::{Context, Result};
use backtrace::{self, Backtrace};
use chrono::Utc;
use client::telemetry;
use client::{telemetry, TelemetrySettings};
use db::kvp::KEY_VALUE_STORE;
use gpui::{AppContext, SemanticVersion};
use http_client::{HttpRequestExt, Method};
use http_client::{self, HttpClient, HttpClientWithUrl};
use paths::{crashes_dir, crashes_retired_dir};
use project::Project;
use release_channel::ReleaseChannel;
use release_channel::RELEASE_CHANNEL;
use settings::Settings;
@@ -21,6 +22,7 @@ use std::{io::Write, panic, sync::atomic::AtomicU32, thread};
use telemetry_events::LocationData;
use telemetry_events::Panic;
use telemetry_events::PanicRequest;
use url::Url;
use util::ResultExt;
use crate::stdout_is_a_pty;
@@ -133,13 +135,73 @@ pub fn init_panic_hook(
pub fn init(
http_client: Arc<HttpClientWithUrl>,
system_id: Option<String>,
installation_id: Option<String>,
session_id: String,
cx: &mut AppContext,
) {
#[cfg(target_os = "macos")]
monitor_main_thread_hangs(http_client.clone(), installation_id.clone(), cx);
upload_panics_and_crashes(http_client, installation_id, cx)
let Some(panic_report_url) = http_client
.build_zed_api_url("/telemetry/panics", &[])
.log_err()
else {
return;
};
upload_panics_and_crashes(
http_client.clone(),
panic_report_url.clone(),
installation_id.clone(),
cx,
);
cx.observe_new_models(move |project: &mut Project, cx| {
let http_client = http_client.clone();
let panic_report_url = panic_report_url.clone();
let session_id = session_id.clone();
let installation_id = installation_id.clone();
let system_id = system_id.clone();
if let Some(ssh_client) = project.ssh_client() {
ssh_client.update(cx, |client, cx| {
if TelemetrySettings::get_global(cx).diagnostics {
let request = client.proto_client().request(proto::GetPanicFiles {});
cx.background_executor()
.spawn(async move {
let panic_files = request.await?;
for file in panic_files.file_contents {
let panic: Option<Panic> = serde_json::from_str(&file)
.log_err()
.or_else(|| {
file.lines()
.next()
.and_then(|line| serde_json::from_str(line).ok())
})
.unwrap_or_else(|| {
log::error!("failed to deserialize panic file {:?}", file);
None
});
if let Some(mut panic) = panic {
panic.session_id = session_id.clone();
panic.system_id = system_id.clone();
panic.installation_id = installation_id.clone();
upload_panic(&http_client, &panic_report_url, panic, &mut None)
.await?;
}
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
})
}
})
.detach();
}
#[cfg(target_os = "macos")]
@@ -346,16 +408,18 @@ pub fn monitor_main_thread_hangs(
fn upload_panics_and_crashes(
http: Arc<HttpClientWithUrl>,
panic_report_url: Url,
installation_id: Option<String>,
cx: &AppContext,
) {
let telemetry_settings = *client::TelemetrySettings::get_global(cx);
cx.background_executor()
.spawn(async move {
let most_recent_panic = upload_previous_panics(http.clone(), telemetry_settings)
.await
.log_err()
.flatten();
let most_recent_panic =
upload_previous_panics(http.clone(), &panic_report_url, telemetry_settings)
.await
.log_err()
.flatten();
upload_previous_crashes(http, most_recent_panic, installation_id, telemetry_settings)
.await
.log_err()
@@ -366,9 +430,9 @@ fn upload_panics_and_crashes(
/// Uploads panics via `zed.dev`.
async fn upload_previous_panics(
http: Arc<HttpClientWithUrl>,
panic_report_url: &Url,
telemetry_settings: client::TelemetrySettings,
) -> Result<Option<(i64, String)>> {
let panic_report_url = http.build_zed_api_url("/telemetry/panics", &[])?;
) -> anyhow::Result<Option<(i64, String)>> {
let mut children = smol::fs::read_dir(paths::logs_dir()).await?;
let mut most_recent_panic = None;
@@ -396,7 +460,7 @@ async fn upload_previous_panics(
.context("error reading panic file")?;
let panic: Option<Panic> = serde_json::from_str(&panic_file_content)
.ok()
.log_err()
.or_else(|| {
panic_file_content
.lines()
@@ -409,26 +473,8 @@ async fn upload_previous_panics(
});
if let Some(panic) = panic {
most_recent_panic = Some((panic.panicked_on, panic.payload.clone()));
let json_bytes = serde_json::to_vec(&PanicRequest { panic }).unwrap();
let Some(checksum) = client::telemetry::calculate_json_checksum(&json_bytes) else {
if !upload_panic(&http, &panic_report_url, panic, &mut most_recent_panic).await? {
continue;
};
let Ok(request) = http_client::Request::builder()
.method(Method::POST)
.uri(panic_report_url.as_ref())
.header("x-zed-checksum", checksum)
.body(json_bytes.into())
else {
continue;
};
let response = http.send(request).await.context("error sending panic")?;
if !response.status().is_success() {
log::error!("Error uploading panic to server: {}", response.status());
}
}
}
@@ -438,9 +484,42 @@ async fn upload_previous_panics(
.context("error removing panic")
.log_err();
}
Ok::<_, anyhow::Error>(most_recent_panic)
Ok(most_recent_panic)
}
async fn upload_panic(
http: &Arc<HttpClientWithUrl>,
panic_report_url: &Url,
panic: telemetry_events::Panic,
most_recent_panic: &mut Option<(i64, String)>,
) -> Result<bool> {
*most_recent_panic = Some((panic.panicked_on, panic.payload.clone()));
let json_bytes = serde_json::to_vec(&PanicRequest {
panic: panic.clone(),
})
.unwrap();
let Some(checksum) = client::telemetry::calculate_json_checksum(&json_bytes) else {
return Ok(false);
};
let Ok(request) = http_client::Request::builder()
.method(Method::POST)
.uri(panic_report_url.as_ref())
.header("x-zed-checksum", checksum)
.body(json_bytes.into())
else {
return Ok(false);
};
let response = http.send(request).await.context("error sending panic")?;
if !response.status().is_success() {
log::error!("Error uploading panic to server: {}", response.status());
}
Ok(true)
}
const LAST_CRASH_UPLOADED: &str = "LAST_CRASH_UPLOADED";
/// upload crashes from apple's diagnostic reports to our server.

View File

@@ -2047,6 +2047,9 @@ Run the `theme selector: toggle` action in the command palette to see a current
"auto_fold_dirs": true,
"scrollbar": {
"show": null
},
"indent_guides": {
"show": "always"
}
}
}
@@ -2164,27 +2167,54 @@ Run the `theme selector: toggle` action in the command palette to see a current
- Setting: `indent_size`
- Default: `20`
### Indent Guides
### Indent Guides: Show
- Description: Whether to show indent guides in the project panel.
- Description: Whether to show indent guides in the project panel. Possible values: "always", "never".
- Setting: `indent_guides`
- Default: `true`
### Scrollbar
- Description: Scrollbar related settings. Possible values: null, "auto", "system", "always", "never". Inherits editor settings when absent, see its description for more details.
- Setting: `scrollbar`
- Default:
```json
"scrollbar": {
"show": null
"indent_guides": {
"show": "always"
}
```
**Options**
1. Show scrollbar in project panel
1. Show indent guides in the project panel
```json
{
"indent_guides": {
"show": "always"
}
}
```
2. Hide indent guides in the project panel
```json
{
"indent_guides": {
"show": "never"
}
}
```
### Scrollbar: Show
- Description: Whether to show a scrollbar in the project panel. Possible values: null, "auto", "system", "always", "never". Inherits editor settings when absent, see its description for more details.
- Setting: `scrollbar`
- Default:
```json
"scrollbar": {
"show": null
}
```
**Options**
1. Show scrollbar in the project panel
```json
{
@@ -2194,7 +2224,7 @@ Run the `theme selector: toggle` action in the command palette to see a current
}
```
2. Hide scrollbar in project panel
2. Hide scrollbar in the project panel
```json
{
@@ -2237,9 +2267,11 @@ Run the `theme selector: toggle` action in the command palette to see a current
"folder_icons": true,
"git_status": true,
"indent_size": 20,
"indent_guides": true,
"auto_reveal_entries": true,
"auto_fold_dirs": true,
"indent_guides": {
"show": "always"
}
}
```

View File

@@ -63,6 +63,12 @@ if [[ $# -gt 0 ]]; then
fi
fi
# Get release channel
pushd crates/zed
channel=$(<RELEASE_CHANNEL)
export ZED_RELEASE_CHANNEL="${channel}"
popd
export ZED_BUNDLE=true
export MACOSX_DEPLOYMENT_TARGET=10.15.7
@@ -90,10 +96,6 @@ else
fi
echo "Creating application bundle"
pushd crates/zed
channel=$(<RELEASE_CHANNEL)
popd
pushd crates/zed
cp Cargo.toml Cargo.toml.backup
sed \