Compare commits
54 Commits
drop-image
...
v0.159.7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0000569e4a | ||
|
|
2353db0f47 | ||
|
|
061a8cf844 | ||
|
|
4d1030fd76 | ||
|
|
614f6ef04f | ||
|
|
b519fd5b2d | ||
|
|
0376285213 | ||
|
|
bcf6806a3f | ||
|
|
fe1f84152b | ||
|
|
e3b7e5e103 | ||
|
|
ed4792d4fc | ||
|
|
8172bb38a8 | ||
|
|
161c14c9f2 | ||
|
|
05f797f0c9 | ||
|
|
a4b818ec6b | ||
|
|
7dadb5b630 | ||
|
|
a8a690ebc1 | ||
|
|
8e251e3507 | ||
|
|
a776c00753 | ||
|
|
11d31e5b0e | ||
|
|
eb806b93c2 | ||
|
|
febd55d887 | ||
|
|
a6a8e46062 | ||
|
|
efc1840594 | ||
|
|
cd1eaa1971 | ||
|
|
cfcd398e8d | ||
|
|
cca65c85ac | ||
|
|
ad6d20ec4f | ||
|
|
1f44281933 | ||
|
|
7d4a643b16 | ||
|
|
91ee7f4000 | ||
|
|
874b4b8d55 | ||
|
|
9cdccb2308 | ||
|
|
70d6ad5a43 | ||
|
|
6a58d8c2b1 | ||
|
|
d641115d19 | ||
|
|
c300c1d600 | ||
|
|
fd3b5ff0d6 | ||
|
|
9517964a04 | ||
|
|
0798c642d4 | ||
|
|
6281df900c | ||
|
|
c248d15d13 | ||
|
|
07612cf0a3 | ||
|
|
7ad9f1b908 | ||
|
|
c1fedde6f5 | ||
|
|
2f030e3b9a | ||
|
|
391243da0a | ||
|
|
80da78b450 | ||
|
|
3b3477c5a1 | ||
|
|
8e80ce8430 | ||
|
|
1b1872666c | ||
|
|
ac5cb8b969 | ||
|
|
c4060fe075 | ||
|
|
ca336567ef |
2
.github/workflows/bump_patch_version.yml
vendored
2
.github/workflows/bump_patch_version.yml
vendored
@@ -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
43
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
6
assets/icons/file_icons/gleam.svg
Normal file
6
assets/icons/file_icons/gleam.svg
Normal 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 |
@@ -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 }],
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -23,6 +23,7 @@ language.workspace = true
|
||||
project.workspace = true
|
||||
smallvec.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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?;
|
||||
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
]
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
};
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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:?}");
|
||||
}
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -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"] },
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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!()
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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.");
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1 +1 @@
|
||||
dev
|
||||
stable
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -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 \
|
||||
|
||||
Reference in New Issue
Block a user