Compare commits
26 Commits
repro-hang
...
expand-sel
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02701b49f2 | ||
|
|
b338475855 | ||
|
|
a27369c42c | ||
|
|
d4099eed3a | ||
|
|
5b6401519b | ||
|
|
293e080f03 | ||
|
|
633b665379 | ||
|
|
7fd334fddb | ||
|
|
10226a3992 | ||
|
|
383e868af0 | ||
|
|
40802d91d4 | ||
|
|
6d5784daa6 | ||
|
|
f80eb264fb | ||
|
|
3d956ca68b | ||
|
|
7ce131aaf8 | ||
|
|
60be47d115 | ||
|
|
bd187883da | ||
|
|
4f9217bca0 | ||
|
|
ce5222f1df | ||
|
|
cf7b0c8971 | ||
|
|
7bc4cb9868 | ||
|
|
f84f3ffeb7 | ||
|
|
c564a4a26c | ||
|
|
515fd7b75f | ||
|
|
662a4440cc | ||
|
|
5dee43b05c |
38
Cargo.lock
generated
38
Cargo.lock
generated
@@ -854,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",
|
||||
@@ -1350,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",
|
||||
@@ -1441,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",
|
||||
@@ -2366,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",
|
||||
@@ -2569,7 +2569,7 @@ dependencies = [
|
||||
"gpui",
|
||||
"hex",
|
||||
"http_client",
|
||||
"hyper 0.14.30",
|
||||
"hyper 0.14.31",
|
||||
"indoc",
|
||||
"jsonwebtoken",
|
||||
"language",
|
||||
@@ -5570,9 +5570,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",
|
||||
@@ -5585,7 +5585,7 @@ dependencies = [
|
||||
"httpdate",
|
||||
"itoa",
|
||||
"pin-project-lite",
|
||||
"socket2 0.5.7",
|
||||
"socket2 0.4.10",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
@@ -5620,7 +5620,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",
|
||||
@@ -5653,7 +5653,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",
|
||||
@@ -6346,6 +6346,7 @@ dependencies = [
|
||||
"env_logger 0.11.5",
|
||||
"futures 0.3.30",
|
||||
"gpui",
|
||||
"itertools 0.13.0",
|
||||
"language",
|
||||
"lsp",
|
||||
"project",
|
||||
@@ -6357,6 +6358,7 @@ dependencies = [
|
||||
"ui",
|
||||
"util",
|
||||
"workspace",
|
||||
"zed_actions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6489,7 +6491,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-targets 0.52.6",
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -9536,6 +9538,7 @@ dependencies = [
|
||||
"log",
|
||||
"parking_lot",
|
||||
"prost",
|
||||
"release_channel",
|
||||
"rpc",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -9660,7 +9663,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",
|
||||
@@ -13452,7 +13455,7 @@ dependencies = [
|
||||
"futures-util",
|
||||
"headers",
|
||||
"http 0.2.12",
|
||||
"hyper 0.14.30",
|
||||
"hyper 0.14.31",
|
||||
"log",
|
||||
"mime",
|
||||
"mime_guess",
|
||||
@@ -15033,7 +15036,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.160.0"
|
||||
version = "0.161.0"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"anyhow",
|
||||
@@ -15173,13 +15176,6 @@ dependencies = [
|
||||
"zed_extension_api 0.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_dart"
|
||||
version = "0.1.2"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_deno"
|
||||
version = "0.0.2"
|
||||
|
||||
@@ -138,7 +138,6 @@ members = [
|
||||
"extensions/astro",
|
||||
"extensions/clojure",
|
||||
"extensions/csharp",
|
||||
"extensions/dart",
|
||||
"extensions/deno",
|
||||
"extensions/elixir",
|
||||
"extensions/elm",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<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 width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.3848 9.30444C7.3848 9.30444 7.53254 10.2646 8.53248 10.0882C9.53242 9.91193 9.36378 8.95549 9.36378 8.95549" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M5.54155 5.54157C6.12355 4.90104 6.01688 2.62541 7.22875 2.3985C8.44063 2.17158 9.19097 4.33148 9.91982 4.6814C10.6487 5.03133 12.8517 4.3028 13.4381 5.38734C14.0244 6.47188 12.1395 7.95973 12.026 8.64088C11.9126 9.32203 13.3614 11.2416 12.4675 12.1701C11.5736 13.0986 9.73005 11.7545 8.90486 11.8834C8.07966 12.0123 6.79244 13.9095 5.67367 13.3502C4.55491 12.7909 5.16702 10.5455 4.82437 9.87612C4.48171 9.20673 2.34028 8.54978 2.4525 7.35049C2.56471 6.15121 4.95956 6.1821 5.54155 5.54157Z" stroke="#FF7676" stroke-opacity="0.52" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M5.54155 5.54157C6.12355 4.90104 6.01688 2.62541 7.22875 2.3985C8.44063 2.17158 9.19097 4.33148 9.91982 4.6814C10.6487 5.03133 12.8517 4.3028 13.4381 5.38734C14.0244 6.47188 12.1395 7.95973 12.026 8.64088C11.9126 9.32203 13.3614 11.2416 12.4675 12.1701C11.5736 13.0986 9.73005 11.7545 8.90486 11.8834C8.07966 12.0123 6.79244 13.9095 5.67367 13.3502C4.55491 12.7909 5.16702 10.5455 4.82437 9.87612C4.48171 9.20673 2.34028 8.54978 2.4525 7.35049C2.56471 6.15121 4.95956 6.1821 5.54155 5.54157Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="6.25098" cy="7.75" r="0.75" fill="black"/>
|
||||
<circle cx="10.1035" cy="7.25" r="0.75" fill="black"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 1.6 KiB |
1
assets/icons/wand.svg
Normal file
1
assets/icons/wand.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-wand"><path d="M15 4V2"/><path d="M15 16v-2"/><path d="M8 9h2"/><path d="M20 9h2"/><path d="M17.8 11.8 19 13"/><path d="M15 9h.01"/><path d="M17.8 6.2 19 5"/><path d="m3 21 9-9"/><path d="M12.2 6.2 11 5"/></svg>
|
||||
|
After Width: | Height: | Size: 414 B |
@@ -414,6 +414,23 @@
|
||||
// 2. Never show indent guides:
|
||||
// "never"
|
||||
"show": "always"
|
||||
},
|
||||
/// Scrollbar-related settings
|
||||
"scrollbar": {
|
||||
/// When to show the scrollbar in the project panel.
|
||||
/// This setting can take four values:
|
||||
///
|
||||
/// 1. null (default): Inherit editor settings
|
||||
/// 2. Show the scrollbar if there's important information or
|
||||
/// follow the system's configured behavior (default):
|
||||
/// "auto"
|
||||
/// 3. Match the system's configured behavior:
|
||||
/// "system"
|
||||
/// 4. Always show the scrollbar:
|
||||
/// "always"
|
||||
/// 5. Never show the scrollbar:
|
||||
/// "never"
|
||||
"show": null
|
||||
}
|
||||
},
|
||||
"collaboration_panel": {
|
||||
@@ -635,6 +652,12 @@
|
||||
// Sets a delay after which the inline blame information is shown.
|
||||
// Delay is restarted with every cursor movement.
|
||||
// "delay_ms": 600
|
||||
//
|
||||
// Whether or not do display the git commit summary on the same line.
|
||||
// "show_commit_summary": false
|
||||
//
|
||||
// The minimum column number to show the inline blame information at
|
||||
// "min_column": 0
|
||||
}
|
||||
},
|
||||
// Configuration for how direnv configuration should be loaded. May take 2 values:
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"allow_concurrent_runs": false,
|
||||
// What to do with the terminal pane and tab, after the command was started:
|
||||
// * `always` — always show the terminal pane, add and focus the corresponding task's tab in it (default)
|
||||
// * `no_focus` — always show the terminal pane, add/reuse the task's tab there, but don't focus it
|
||||
// * `never` — avoid changing current terminal pane focus, but still add/reuse the task's tab there
|
||||
"reveal": "always",
|
||||
// What to do with the terminal pane and tab, after the command had finished:
|
||||
|
||||
@@ -352,7 +352,10 @@ impl ActivityIndicator {
|
||||
.into_any_element(),
|
||||
),
|
||||
message: format!("Formatting failed: {}. Click to see logs.", failure),
|
||||
on_click: Some(Arc::new(|_, cx| {
|
||||
on_click: Some(Arc::new(|indicator, cx| {
|
||||
indicator.project.update(cx, |project, cx| {
|
||||
project.reset_last_formatting_failure(cx);
|
||||
});
|
||||
cx.dispatch_action(Box::new(workspace::OpenLog));
|
||||
})),
|
||||
});
|
||||
|
||||
@@ -73,12 +73,11 @@ use std::{
|
||||
};
|
||||
use terminal_view::{terminal_panel::TerminalPanel, TerminalView};
|
||||
use text::SelectionGoal;
|
||||
use ui::TintColor;
|
||||
use ui::{
|
||||
prelude::*,
|
||||
utils::{format_distance_from_now, DateTimeType},
|
||||
Avatar, ButtonLike, ContextMenu, Disclosure, ElevationIndex, KeyBinding, ListItem,
|
||||
ListItemSpacing, PopoverMenu, PopoverMenuHandle, Tooltip,
|
||||
ListItemSpacing, PopoverMenu, PopoverMenuHandle, TintColor, Tooltip,
|
||||
};
|
||||
use util::{maybe, ResultExt};
|
||||
use workspace::{
|
||||
@@ -1462,6 +1461,7 @@ type MessageHeader = MessageMetadata;
|
||||
|
||||
#[derive(Clone)]
|
||||
enum AssistError {
|
||||
FileRequired,
|
||||
PaymentRequired,
|
||||
MaxMonthlySpendReached,
|
||||
Message(SharedString),
|
||||
@@ -1628,7 +1628,10 @@ impl ContextEditor {
|
||||
|
||||
self.last_error = None;
|
||||
|
||||
if let Some(user_message) = self
|
||||
if request_type == RequestType::SuggestEdits && !self.context.read(cx).contains_files(cx) {
|
||||
self.last_error = Some(AssistError::FileRequired);
|
||||
cx.notify();
|
||||
} else if let Some(user_message) = self
|
||||
.context
|
||||
.update(cx, |context, cx| context.assist(request_type, cx))
|
||||
{
|
||||
@@ -2200,12 +2203,14 @@ impl ContextEditor {
|
||||
let max_width = cx.max_width;
|
||||
let gutter_width = cx.gutter_dimensions.full_width();
|
||||
let block_id = cx.block_id;
|
||||
let selected = cx.selected;
|
||||
this.update(&mut **cx, |this, cx| {
|
||||
this.render_patch(
|
||||
patch_range.clone(),
|
||||
max_width,
|
||||
gutter_width,
|
||||
block_id,
|
||||
selected,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
@@ -3435,6 +3440,7 @@ impl ContextEditor {
|
||||
max_width: Pixels,
|
||||
gutter_width: Pixels,
|
||||
id: BlockId,
|
||||
selected: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<AnyElement> {
|
||||
let snapshot = self.editor.update(cx, |editor, cx| editor.snapshot(cx));
|
||||
@@ -3453,8 +3459,13 @@ impl ContextEditor {
|
||||
|
||||
Some(
|
||||
v_flex()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.border_color(if selected {
|
||||
cx.theme().colors().border_focused
|
||||
} else {
|
||||
cx.theme().colors().border
|
||||
})
|
||||
.id(id)
|
||||
.ml(gutter_width)
|
||||
.p_2()
|
||||
@@ -3702,6 +3713,7 @@ impl ContextEditor {
|
||||
.elevation_2(cx)
|
||||
.occlude()
|
||||
.child(match last_error {
|
||||
AssistError::FileRequired => self.render_file_required_error(cx),
|
||||
AssistError::PaymentRequired => self.render_payment_required_error(cx),
|
||||
AssistError::MaxMonthlySpendReached => {
|
||||
self.render_max_monthly_spend_reached_error(cx)
|
||||
@@ -3714,6 +3726,41 @@ impl ContextEditor {
|
||||
)
|
||||
}
|
||||
|
||||
fn render_file_required_error(&self, cx: &mut ViewContext<Self>) -> AnyElement {
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.items_center()
|
||||
.child(Icon::new(IconName::Warning).color(Color::Warning))
|
||||
.child(
|
||||
Label::new("Suggest Edits needs a file to edit").weight(FontWeight::MEDIUM),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("error-message")
|
||||
.max_h_24()
|
||||
.overflow_y_scroll()
|
||||
.child(Label::new(
|
||||
"To include files, type /file or /tab in your prompt.",
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_end()
|
||||
.mt_1()
|
||||
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|
||||
|this, _, cx| {
|
||||
this.last_error = None;
|
||||
cx.notify();
|
||||
},
|
||||
))),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_payment_required_error(&self, cx: &mut ViewContext<Self>) -> AnyElement {
|
||||
const ERROR_MESSAGE: &str = "Free tier exceeded. Subscribe and add payment to continue using Zed LLMs. You'll be billed at cost for tokens used.";
|
||||
|
||||
@@ -3928,13 +3975,7 @@ impl Render for ContextEditor {
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let focus_handle = self
|
||||
.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
Some(workspace.active_item_as::<Editor>(cx)?.focus_handle(cx))
|
||||
})
|
||||
.ok()
|
||||
.flatten();
|
||||
|
||||
v_flex()
|
||||
.key_context("ContextEditor")
|
||||
.capture_action(cx.listener(ContextEditor::cancel))
|
||||
@@ -3982,28 +4023,7 @@ impl Render for ContextEditor {
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(render_inject_context_menu(cx.view().downgrade(), cx))
|
||||
.child(
|
||||
IconButton::new("quote-button", IconName::Quote)
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click(|_, cx| {
|
||||
cx.dispatch_action(QuoteSelection.boxed_clone());
|
||||
})
|
||||
.tooltip(move |cx| {
|
||||
cx.new_view(|cx| {
|
||||
Tooltip::new("Insert Selection").key_binding(
|
||||
focus_handle.as_ref().and_then(|handle| {
|
||||
KeyBinding::for_action_in(
|
||||
&QuoteSelection,
|
||||
&handle,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
)
|
||||
})
|
||||
.into()
|
||||
}),
|
||||
),
|
||||
.child(render_inject_context_menu(cx.view().downgrade(), cx)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
@@ -4298,6 +4318,7 @@ fn render_inject_context_menu(
|
||||
Button::new("trigger", "Add Context")
|
||||
.icon(IconName::Plus)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_position(IconPosition::Start)
|
||||
.tooltip(|cx| Tooltip::text("Type / to insert via keyboard", cx)),
|
||||
)
|
||||
@@ -4472,7 +4493,7 @@ impl Render for ContextEditorToolbarItem {
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.gap_2()
|
||||
.child(Label::new("Insert Context"))
|
||||
.child(Label::new("Add Context"))
|
||||
.child(Label::new("/ command").color(Color::Muted))
|
||||
.into_any()
|
||||
},
|
||||
@@ -4496,7 +4517,7 @@ impl Render for ContextEditorToolbarItem {
|
||||
}
|
||||
},
|
||||
)
|
||||
.action("Insert Selection", QuoteSelection.boxed_clone())
|
||||
.action("Add Selection", QuoteSelection.boxed_clone())
|
||||
}))
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
mod context_tests;
|
||||
|
||||
use crate::{
|
||||
prompts::PromptBuilder, slash_command::SlashCommandLine, AssistantEdit, AssistantPatch,
|
||||
AssistantPatchStatus, MessageId, MessageStatus,
|
||||
prompts::PromptBuilder,
|
||||
slash_command::{file_command::FileCommandMetadata, SlashCommandLine},
|
||||
AssistantEdit, AssistantPatch, AssistantPatchStatus, MessageId, MessageStatus,
|
||||
};
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use assistant_slash_command::{
|
||||
@@ -66,7 +67,7 @@ impl ContextId {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum RequestType {
|
||||
/// Request a normal chat response from the model.
|
||||
Chat,
|
||||
@@ -989,6 +990,20 @@ impl Context {
|
||||
&self.slash_command_output_sections
|
||||
}
|
||||
|
||||
pub fn contains_files(&self, cx: &AppContext) -> bool {
|
||||
let buffer = self.buffer.read(cx);
|
||||
self.slash_command_output_sections.iter().any(|section| {
|
||||
section.is_valid(buffer)
|
||||
&& section
|
||||
.metadata
|
||||
.as_ref()
|
||||
.and_then(|metadata| {
|
||||
serde_json::from_value::<FileCommandMetadata>(metadata.clone()).ok()
|
||||
})
|
||||
.is_some()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn pending_tool_uses(&self) -> Vec<&PendingToolUse> {
|
||||
self.pending_tool_uses_by_id.values().collect()
|
||||
}
|
||||
|
||||
@@ -311,7 +311,7 @@ impl PromptBuilder {
|
||||
}
|
||||
|
||||
pub fn generate_workflow_prompt(&self) -> Result<String, RenderError> {
|
||||
self.handlebars.lock().render("edit_workflow", &())
|
||||
self.handlebars.lock().render("suggest_edits", &())
|
||||
}
|
||||
|
||||
pub fn generate_project_slash_command_prompt(
|
||||
|
||||
@@ -14,7 +14,7 @@ use language_model::{
|
||||
use semantic_index::{FileSummary, SemanticDb};
|
||||
use smol::channel;
|
||||
use std::sync::{atomic::AtomicBool, Arc};
|
||||
use ui::{BorrowAppContext, WindowContext};
|
||||
use ui::{prelude::*, BorrowAppContext, WindowContext};
|
||||
use util::ResultExt;
|
||||
use workspace::Workspace;
|
||||
|
||||
@@ -37,6 +37,10 @@ impl SlashCommand for AutoCommand {
|
||||
"Automatically infer what context to add".into()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Wand
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
self.description()
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ use gpui::{Task, WeakView, WindowContext};
|
||||
use language::{BufferSnapshot, LspAdapterDelegate};
|
||||
use std::sync::{atomic::AtomicBool, Arc};
|
||||
use text::OffsetRangeExt;
|
||||
use ui::prelude::*;
|
||||
use workspace::Workspace;
|
||||
|
||||
pub(crate) struct DeltaSlashCommand;
|
||||
@@ -27,6 +28,10 @@ impl SlashCommand for DeltaSlashCommand {
|
||||
self.description()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Diff
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
@@ -98,6 +98,10 @@ impl SlashCommand for DiagnosticsSlashCommand {
|
||||
"Insert diagnostics".into()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::XCircle
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
self.description()
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ impl SlashCommand for FileSlashCommand {
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Insert file".into()
|
||||
"Insert file and/or directory".into()
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
@@ -128,6 +128,10 @@ impl SlashCommand for FileSlashCommand {
|
||||
true
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::File
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
self: Arc<Self>,
|
||||
arguments: &[String],
|
||||
|
||||
@@ -24,7 +24,8 @@ use std::{
|
||||
ops::DerefMut,
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
};
|
||||
use ui::{BorrowAppContext as _, IconName};
|
||||
|
||||
use ui::prelude::*;
|
||||
use workspace::Workspace;
|
||||
|
||||
pub struct ProjectSlashCommand {
|
||||
@@ -50,6 +51,10 @@ impl SlashCommand for ProjectSlashCommand {
|
||||
"Generate a semantic search based on context".into()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Folder
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
self.description()
|
||||
}
|
||||
|
||||
@@ -21,6 +21,10 @@ impl SlashCommand for PromptSlashCommand {
|
||||
"Insert prompt from library".into()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Library
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
self.description()
|
||||
}
|
||||
|
||||
@@ -38,6 +38,10 @@ impl SlashCommand for SearchSlashCommand {
|
||||
"Search your project semantically".into()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::SearchCode
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
self.description()
|
||||
}
|
||||
|
||||
@@ -22,6 +22,10 @@ impl SlashCommand for OutlineSlashCommand {
|
||||
"Insert symbols for active tab".into()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::ListTree
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
self.description()
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ use std::{
|
||||
path::PathBuf,
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
};
|
||||
use ui::{ActiveTheme, WindowContext};
|
||||
use ui::{prelude::*, ActiveTheme, WindowContext};
|
||||
use util::ResultExt;
|
||||
use workspace::Workspace;
|
||||
|
||||
@@ -31,6 +31,10 @@ impl SlashCommand for TabSlashCommand {
|
||||
"Insert open tabs (active tab by default)".to_owned()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::FileTree
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
self.description()
|
||||
}
|
||||
|
||||
@@ -33,6 +33,10 @@ impl SlashCommand for TerminalSlashCommand {
|
||||
"Insert terminal output".into()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Terminal
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
self.description()
|
||||
}
|
||||
|
||||
@@ -1,19 +1,13 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use assistant_slash_command::SlashCommandRegistry;
|
||||
use gpui::AnyElement;
|
||||
use gpui::DismissEvent;
|
||||
use gpui::WeakView;
|
||||
use picker::PickerEditorPosition;
|
||||
|
||||
use ui::ListItemSpacing;
|
||||
|
||||
use gpui::SharedString;
|
||||
use gpui::Task;
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use ui::{prelude::*, ListItem, PopoverMenu, PopoverTrigger};
|
||||
use gpui::{AnyElement, DismissEvent, SharedString, Task, WeakView};
|
||||
use picker::{Picker, PickerDelegate, PickerEditorPosition};
|
||||
use ui::{prelude::*, KeyBinding, ListItem, ListItemSpacing, PopoverMenu, PopoverTrigger};
|
||||
|
||||
use crate::assistant_panel::ContextEditor;
|
||||
use crate::QuoteSelection;
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub(super) struct SlashCommandSelector<T: PopoverTrigger> {
|
||||
@@ -27,6 +21,7 @@ struct SlashCommandInfo {
|
||||
name: SharedString,
|
||||
description: SharedString,
|
||||
args: Option<SharedString>,
|
||||
icon: IconName,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -37,6 +32,7 @@ enum SlashCommandEntry {
|
||||
renderer: fn(&mut WindowContext<'_>) -> AnyElement,
|
||||
on_confirm: fn(&mut WindowContext<'_>),
|
||||
},
|
||||
QuoteButton,
|
||||
}
|
||||
|
||||
impl AsRef<str> for SlashCommandEntry {
|
||||
@@ -44,6 +40,7 @@ impl AsRef<str> for SlashCommandEntry {
|
||||
match self {
|
||||
SlashCommandEntry::Info(SlashCommandInfo { name, .. })
|
||||
| SlashCommandEntry::Advert { name, .. } => name,
|
||||
SlashCommandEntry::QuoteButton => "Quote Selection",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -145,16 +142,23 @@ impl PickerDelegate for SlashCommandDelegate {
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||
if let Some(command) = self.filtered_commands.get(self.selected_index) {
|
||||
if let SlashCommandEntry::Info(info) = command {
|
||||
self.active_context_editor
|
||||
.update(cx, |context_editor, cx| {
|
||||
context_editor.insert_command(&info.name, cx)
|
||||
})
|
||||
.ok();
|
||||
} else if let SlashCommandEntry::Advert { on_confirm, .. } = command {
|
||||
on_confirm(cx);
|
||||
match command {
|
||||
SlashCommandEntry::Info(info) => {
|
||||
self.active_context_editor
|
||||
.update(cx, |context_editor, cx| {
|
||||
context_editor.insert_command(&info.name, cx)
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
SlashCommandEntry::QuoteButton => {
|
||||
cx.dispatch_action(Box::new(QuoteSelection));
|
||||
}
|
||||
SlashCommandEntry::Advert { on_confirm, .. } => {
|
||||
on_confirm(cx);
|
||||
}
|
||||
}
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
@@ -181,46 +185,78 @@ impl PickerDelegate for SlashCommandDelegate {
|
||||
.spacing(ListItemSpacing::Dense)
|
||||
.selected(selected)
|
||||
.child(
|
||||
h_flex()
|
||||
v_flex()
|
||||
.group(format!("command-entry-label-{ix}"))
|
||||
.w_full()
|
||||
.min_w(px(250.))
|
||||
.child(
|
||||
v_flex()
|
||||
.child(
|
||||
h_flex()
|
||||
.child(div().font_buffer(cx).child({
|
||||
let mut label = format!("/{}", info.name);
|
||||
if let Some(args) =
|
||||
info.args.as_ref().filter(|_| selected)
|
||||
{
|
||||
label.push_str(&args);
|
||||
}
|
||||
Label::new(label).size(LabelSize::Small)
|
||||
}))
|
||||
.children(info.args.clone().filter(|_| !selected).map(
|
||||
|args| {
|
||||
div()
|
||||
.font_buffer(cx)
|
||||
.child(
|
||||
Label::new(args)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.visible_on_hover(format!(
|
||||
"command-entry-label-{ix}"
|
||||
))
|
||||
},
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
Label::new(info.description.clone())
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(Icon::new(info.icon).size(IconSize::XSmall))
|
||||
.child(div().font_buffer(cx).child({
|
||||
let mut label = format!("{}", info.name);
|
||||
if let Some(args) = info.args.as_ref().filter(|_| selected)
|
||||
{
|
||||
label.push_str(&args);
|
||||
}
|
||||
Label::new(label).size(LabelSize::Small)
|
||||
}))
|
||||
.children(info.args.clone().filter(|_| !selected).map(
|
||||
|args| {
|
||||
div()
|
||||
.font_buffer(cx)
|
||||
.child(
|
||||
Label::new(args)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.visible_on_hover(format!(
|
||||
"command-entry-label-{ix}"
|
||||
))
|
||||
},
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
Label::new(info.description.clone())
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
),
|
||||
),
|
||||
SlashCommandEntry::QuoteButton => {
|
||||
let focus = cx.focus_handle();
|
||||
let key_binding = KeyBinding::for_action_in(&QuoteSelection, &focus, cx);
|
||||
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Dense)
|
||||
.selected(selected)
|
||||
.child(
|
||||
v_flex()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(Icon::new(IconName::Quote).size(IconSize::XSmall))
|
||||
.child(
|
||||
div().font_buffer(cx).child(
|
||||
Label::new("selection").size(LabelSize::Small),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(
|
||||
Label::new("Insert editor selection")
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
.children(key_binding.map(|kb| kb.render(cx))),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
SlashCommandEntry::Advert { renderer, .. } => Some(
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
@@ -251,31 +287,50 @@ impl<T: PopoverTrigger> RenderOnce for SlashCommandSelector<T> {
|
||||
name: command_name.into(),
|
||||
description: menu_text,
|
||||
args,
|
||||
icon: command.icon(),
|
||||
}))
|
||||
})
|
||||
.chain([SlashCommandEntry::Advert {
|
||||
name: "create-your-command".into(),
|
||||
renderer: |cx| {
|
||||
v_flex()
|
||||
.child(
|
||||
h_flex()
|
||||
.font_buffer(cx)
|
||||
.items_center()
|
||||
.gap_1()
|
||||
.child(div().font_buffer(cx).child(
|
||||
Label::new("create-your-command").size(LabelSize::Small),
|
||||
))
|
||||
.child(Icon::new(IconName::ArrowUpRight).size(IconSize::XSmall)),
|
||||
)
|
||||
.child(
|
||||
Label::new("Learn how to create a custom command")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.into_any_element()
|
||||
.chain([
|
||||
SlashCommandEntry::Advert {
|
||||
name: "create-your-command".into(),
|
||||
renderer: |cx| {
|
||||
v_flex()
|
||||
.w_full()
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.font_buffer(cx)
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.child(
|
||||
h_flex()
|
||||
.items_center()
|
||||
.gap_1p5()
|
||||
.child(Icon::new(IconName::Plus).size(IconSize::XSmall))
|
||||
.child(
|
||||
div().font_buffer(cx).child(
|
||||
Label::new("create-your-command")
|
||||
.size(LabelSize::Small),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Icon::new(IconName::ArrowUpRight)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Label::new("Create your custom command")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.into_any_element()
|
||||
},
|
||||
on_confirm: |cx| cx.open_url("https://zed.dev/docs/extensions/slash-commands"),
|
||||
},
|
||||
on_confirm: |cx| cx.open_url("https://zed.dev/docs/extensions/slash-commands"),
|
||||
}])
|
||||
SlashCommandEntry::QuoteButton,
|
||||
])
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let delegate = SlashCommandDelegate {
|
||||
|
||||
@@ -62,6 +62,9 @@ pub type SlashCommandResult = Result<BoxStream<'static, Result<SlashCommandEvent
|
||||
|
||||
pub trait SlashCommand: 'static + Send + Sync {
|
||||
fn name(&self) -> String;
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Slash
|
||||
}
|
||||
fn label(&self, _cx: &AppContext) -> CodeLabel {
|
||||
CodeLabel::plain(self.name(), None)
|
||||
}
|
||||
|
||||
@@ -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?;
|
||||
|
||||
|
||||
@@ -52,9 +52,7 @@ CREATE TABLE "projects" (
|
||||
"host_user_id" INTEGER REFERENCES users (id),
|
||||
"host_connection_id" INTEGER,
|
||||
"host_connection_server_id" INTEGER REFERENCES servers (id) ON DELETE CASCADE,
|
||||
"unregistered" BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
"hosted_project_id" INTEGER REFERENCES hosted_projects (id),
|
||||
"dev_server_project_id" INTEGER REFERENCES dev_server_projects(id)
|
||||
"unregistered" BOOLEAN NOT NULL DEFAULT FALSE
|
||||
);
|
||||
CREATE INDEX "index_projects_on_host_connection_server_id" ON "projects" ("host_connection_server_id");
|
||||
CREATE INDEX "index_projects_on_host_connection_id_and_host_connection_server_id" ON "projects" ("host_connection_id", "host_connection_server_id");
|
||||
@@ -399,30 +397,6 @@ CREATE TABLE rate_buckets (
|
||||
);
|
||||
CREATE INDEX idx_user_id_rate_limit ON rate_buckets (user_id, rate_limit_name);
|
||||
|
||||
CREATE TABLE hosted_projects (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
channel_id INTEGER NOT NULL REFERENCES channels(id),
|
||||
name TEXT NOT NULL,
|
||||
visibility TEXT NOT NULL,
|
||||
deleted_at TIMESTAMP NULL
|
||||
);
|
||||
CREATE INDEX idx_hosted_projects_on_channel_id ON hosted_projects (channel_id);
|
||||
CREATE UNIQUE INDEX uix_hosted_projects_on_channel_id_and_name ON hosted_projects (channel_id, name) WHERE (deleted_at IS NULL);
|
||||
|
||||
CREATE TABLE dev_servers (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id),
|
||||
name TEXT NOT NULL,
|
||||
ssh_connection_string TEXT,
|
||||
hashed_token TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE dev_server_projects (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
dev_server_id INTEGER NOT NULL REFERENCES dev_servers(id),
|
||||
paths TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS billing_preferences (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
ALTER TABLE projects DROP COLUMN dev_server_project_id;
|
||||
ALTER TABLE projects DROP COLUMN hosted_project_id;
|
||||
|
||||
DROP TABLE hosted_projects;
|
||||
DROP TABLE dev_server_projects;
|
||||
DROP TABLE dev_servers;
|
||||
@@ -750,49 +750,6 @@ impl Database {
|
||||
Ok((project, replica_id as ReplicaId))
|
||||
}
|
||||
|
||||
pub async fn leave_hosted_project(
|
||||
&self,
|
||||
project_id: ProjectId,
|
||||
connection: ConnectionId,
|
||||
) -> Result<LeftProject> {
|
||||
self.transaction(|tx| async move {
|
||||
let result = project_collaborator::Entity::delete_many()
|
||||
.filter(
|
||||
Condition::all()
|
||||
.add(project_collaborator::Column::ProjectId.eq(project_id))
|
||||
.add(project_collaborator::Column::ConnectionId.eq(connection.id as i32))
|
||||
.add(
|
||||
project_collaborator::Column::ConnectionServerId
|
||||
.eq(connection.owner_id as i32),
|
||||
),
|
||||
)
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
if result.rows_affected == 0 {
|
||||
return Err(anyhow!("not in the project"))?;
|
||||
}
|
||||
|
||||
let project = project::Entity::find_by_id(project_id)
|
||||
.one(&*tx)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("no such project"))?;
|
||||
let collaborators = project
|
||||
.find_related(project_collaborator::Entity)
|
||||
.all(&*tx)
|
||||
.await?;
|
||||
let connection_ids = collaborators
|
||||
.into_iter()
|
||||
.map(|collaborator| collaborator.connection())
|
||||
.collect();
|
||||
Ok(LeftProject {
|
||||
id: project.id,
|
||||
connection_ids,
|
||||
should_unshare: false,
|
||||
})
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Removes the given connection from the specified project.
|
||||
pub async fn leave_project(
|
||||
&self,
|
||||
|
||||
@@ -80,6 +80,8 @@ pub struct ConfirmCodeAction {
|
||||
pub struct ToggleComments {
|
||||
#[serde(default)]
|
||||
pub advance_downwards: bool,
|
||||
#[serde(default)]
|
||||
pub ignore_indent: bool,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Deserialize, Default)]
|
||||
@@ -157,6 +159,13 @@ pub struct DeleteToPreviousWordStart {
|
||||
pub struct FoldAtLevel {
|
||||
pub level: u32,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Deserialize, Default)]
|
||||
pub struct SpawnNearestTask {
|
||||
#[serde(default)]
|
||||
pub reveal: task::RevealStrategy,
|
||||
}
|
||||
|
||||
impl_actions!(
|
||||
editor,
|
||||
[
|
||||
@@ -182,6 +191,7 @@ impl_actions!(
|
||||
SelectToBeginningOfLine,
|
||||
SelectToEndOfLine,
|
||||
SelectUpByLines,
|
||||
SpawnNearestTask,
|
||||
ShowCompletions,
|
||||
ToggleCodeActions,
|
||||
ToggleComments,
|
||||
|
||||
@@ -660,7 +660,7 @@ impl DisplaySnapshot {
|
||||
new_start..new_end
|
||||
}
|
||||
|
||||
pub fn point_to_display_point(&self, point: MultiBufferPoint, bias: Bias) -> DisplayPoint {
|
||||
fn point_to_display_point(&self, point: MultiBufferPoint, bias: Bias) -> DisplayPoint {
|
||||
let inlay_point = self.inlay_snapshot.to_inlay_point(point);
|
||||
let fold_point = self.fold_snapshot.to_fold_point(inlay_point, bias);
|
||||
let tab_point = self.tab_snapshot.to_tab_point(fold_point);
|
||||
@@ -669,7 +669,7 @@ impl DisplaySnapshot {
|
||||
DisplayPoint(block_point)
|
||||
}
|
||||
|
||||
pub fn display_point_to_point(&self, point: DisplayPoint, bias: Bias) -> Point {
|
||||
fn display_point_to_point(&self, point: DisplayPoint, bias: Bias) -> Point {
|
||||
self.inlay_snapshot
|
||||
.to_buffer_point(self.display_point_to_inlay_point(point, bias))
|
||||
}
|
||||
@@ -942,6 +942,14 @@ impl DisplaySnapshot {
|
||||
DisplayPoint(clipped)
|
||||
}
|
||||
|
||||
pub fn clip_point_2(&self, point: DisplayPoint, bias: Bias, skip_blocks: bool) -> DisplayPoint {
|
||||
let mut clipped = self.block_snapshot.clip_point_2(point.0, bias, skip_blocks);
|
||||
if self.clip_at_line_ends {
|
||||
clipped = self.clip_at_line_end(DisplayPoint(clipped)).0
|
||||
}
|
||||
DisplayPoint(clipped)
|
||||
}
|
||||
|
||||
pub fn clip_ignoring_line_ends(&self, point: DisplayPoint, bias: Bias) -> DisplayPoint {
|
||||
DisplayPoint(self.block_snapshot.clip_point(point.0, bias))
|
||||
}
|
||||
|
||||
@@ -1298,6 +1298,68 @@ impl BlockSnapshot {
|
||||
cursor.item().map_or(false, |t| t.block.is_some())
|
||||
}
|
||||
|
||||
pub fn clip_point_2(&self, point: BlockPoint, bias: Bias, skip_blocks: bool) -> BlockPoint {
|
||||
let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&());
|
||||
cursor.seek(&BlockRow(point.row), Bias::Right, &());
|
||||
|
||||
let max_input_row = WrapRow(self.transforms.summary().input_rows);
|
||||
let mut search_left =
|
||||
(bias == Bias::Left && cursor.start().1 .0 > 0) || cursor.end(&()).1 == max_input_row;
|
||||
let mut reversed = false;
|
||||
|
||||
loop {
|
||||
if let Some(transform) = cursor.item() {
|
||||
let (output_start_row, input_start_row) = cursor.start();
|
||||
let (output_end_row, input_end_row) = cursor.end(&());
|
||||
let output_start = Point::new(output_start_row.0, 0);
|
||||
let output_end = Point::new(output_end_row.0, 0);
|
||||
let input_start = Point::new(input_start_row.0, 0);
|
||||
let input_end = Point::new(input_end_row.0, 0);
|
||||
|
||||
match transform.block.as_ref() {
|
||||
Some(Block::Custom(block))
|
||||
if matches!(block.placement, BlockPlacement::Replace(_)) =>
|
||||
{
|
||||
if bias == Bias::Left {
|
||||
return BlockPoint(output_start);
|
||||
} else {
|
||||
return BlockPoint(Point::new(output_end.row - 1, 0));
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let input_point = if point.row >= output_end_row.0 {
|
||||
let line_len = self.wrap_snapshot.line_len(input_end_row.0 - 1);
|
||||
self.wrap_snapshot
|
||||
.clip_point(WrapPoint::new(input_end_row.0 - 1, line_len), bias)
|
||||
} else {
|
||||
let output_overshoot = point.0.saturating_sub(output_start);
|
||||
self.wrap_snapshot
|
||||
.clip_point(WrapPoint(input_start + output_overshoot), bias)
|
||||
};
|
||||
|
||||
if (input_start..input_end).contains(&input_point.0) {
|
||||
let input_overshoot = input_point.0.saturating_sub(input_start);
|
||||
return BlockPoint(output_start + input_overshoot);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if search_left {
|
||||
cursor.prev(&());
|
||||
} else {
|
||||
cursor.next(&());
|
||||
}
|
||||
} else if reversed {
|
||||
return self.max_point();
|
||||
} else {
|
||||
reversed = true;
|
||||
search_left = !search_left;
|
||||
cursor.seek(&BlockRow(point.row), Bias::Right, &());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clip_point(&self, point: BlockPoint, bias: Bias) -> BlockPoint {
|
||||
let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&());
|
||||
cursor.seek(&BlockRow(point.row), Bias::Right, &());
|
||||
|
||||
@@ -502,6 +502,19 @@ struct RunnableTasks {
|
||||
context_range: Range<BufferOffset>,
|
||||
}
|
||||
|
||||
impl RunnableTasks {
|
||||
fn resolve<'a>(
|
||||
&'a self,
|
||||
cx: &'a task::TaskContext,
|
||||
) -> impl Iterator<Item = (TaskSourceKind, ResolvedTask)> + 'a {
|
||||
self.templates.iter().filter_map(|(kind, template)| {
|
||||
template
|
||||
.resolve_task(&kind.to_id_base(), cx)
|
||||
.map(|task| (kind.clone(), task))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ResolvedTasks {
|
||||
templates: SmallVec<[(TaskSourceKind, ResolvedTask); 1]>,
|
||||
@@ -3471,8 +3484,8 @@ impl Editor {
|
||||
}
|
||||
let new_anchor_selections = new_selections.iter().map(|e| &e.0);
|
||||
let new_selection_deltas = new_selections.iter().map(|e| e.1);
|
||||
let map = this.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
let new_selections = resolve_multiple::<usize, _>(new_anchor_selections, &map)
|
||||
let snapshot = this.buffer.read(cx).read(cx);
|
||||
let new_selections = resolve_multiple::<usize, _>(new_anchor_selections, &snapshot)
|
||||
.zip(new_selection_deltas)
|
||||
.map(|(selection, delta)| Selection {
|
||||
id: selection.id,
|
||||
@@ -3485,20 +3498,18 @@ impl Editor {
|
||||
|
||||
let mut i = 0;
|
||||
for (position, delta, selection_id, pair) in new_autoclose_regions {
|
||||
let position = position.to_offset(&map.buffer_snapshot) + delta;
|
||||
let start = map.buffer_snapshot.anchor_before(position);
|
||||
let end = map.buffer_snapshot.anchor_after(position);
|
||||
let position = position.to_offset(&snapshot) + delta;
|
||||
let start = snapshot.anchor_before(position);
|
||||
let end = snapshot.anchor_after(position);
|
||||
while let Some(existing_state) = this.autoclose_regions.get(i) {
|
||||
match existing_state.range.start.cmp(&start, &map.buffer_snapshot) {
|
||||
match existing_state.range.start.cmp(&start, &snapshot) {
|
||||
Ordering::Less => i += 1,
|
||||
Ordering::Greater => break,
|
||||
Ordering::Equal => {
|
||||
match end.cmp(&existing_state.range.end, &map.buffer_snapshot) {
|
||||
Ordering::Less => i += 1,
|
||||
Ordering::Equal => break,
|
||||
Ordering::Greater => break,
|
||||
}
|
||||
}
|
||||
Ordering::Equal => match end.cmp(&existing_state.range.end, &snapshot) {
|
||||
Ordering::Less => i += 1,
|
||||
Ordering::Equal => break,
|
||||
Ordering::Greater => break,
|
||||
},
|
||||
}
|
||||
}
|
||||
this.autoclose_regions.insert(
|
||||
@@ -3511,6 +3522,7 @@ impl Editor {
|
||||
);
|
||||
}
|
||||
|
||||
drop(snapshot);
|
||||
let had_active_inline_completion = this.has_active_inline_completion(cx);
|
||||
this.change_selections_inner(Some(Autoscroll::fit()), false, cx, |s| {
|
||||
s.select(new_selections)
|
||||
@@ -4724,29 +4736,7 @@ impl Editor {
|
||||
.as_ref()
|
||||
.zip(editor.project.clone())
|
||||
.map(|(tasks, project)| {
|
||||
let position = Point::new(buffer_row, tasks.column);
|
||||
let range_start = buffer.read(cx).anchor_at(position, Bias::Right);
|
||||
let location = Location {
|
||||
buffer: buffer.clone(),
|
||||
range: range_start..range_start,
|
||||
};
|
||||
// Fill in the environmental variables from the tree-sitter captures
|
||||
let mut captured_task_variables = TaskVariables::default();
|
||||
for (capture_name, value) in tasks.extra_variables.clone() {
|
||||
captured_task_variables.insert(
|
||||
task::VariableName::Custom(capture_name.into()),
|
||||
value.clone(),
|
||||
);
|
||||
}
|
||||
project.update(cx, |project, cx| {
|
||||
project.task_store().update(cx, |task_store, cx| {
|
||||
task_store.task_context_for_location(
|
||||
captured_task_variables,
|
||||
location,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})
|
||||
Self::build_tasks_context(&project, &buffer, buffer_row, tasks, cx)
|
||||
});
|
||||
|
||||
Some(cx.spawn(|editor, mut cx| async move {
|
||||
@@ -4757,15 +4747,7 @@ impl Editor {
|
||||
let resolved_tasks =
|
||||
tasks.zip(task_context).map(|(tasks, task_context)| {
|
||||
Arc::new(ResolvedTasks {
|
||||
templates: tasks
|
||||
.templates
|
||||
.iter()
|
||||
.filter_map(|(kind, template)| {
|
||||
template
|
||||
.resolve_task(&kind.to_id_base(), &task_context)
|
||||
.map(|task| (kind.clone(), task))
|
||||
})
|
||||
.collect(),
|
||||
templates: tasks.resolve(&task_context).collect(),
|
||||
position: snapshot.buffer_snapshot.anchor_before(Point::new(
|
||||
multibuffer_point.row,
|
||||
tasks.column,
|
||||
@@ -5471,6 +5453,132 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
fn build_tasks_context(
|
||||
project: &Model<Project>,
|
||||
buffer: &Model<Buffer>,
|
||||
buffer_row: u32,
|
||||
tasks: &Arc<RunnableTasks>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<Option<task::TaskContext>> {
|
||||
let position = Point::new(buffer_row, tasks.column);
|
||||
let range_start = buffer.read(cx).anchor_at(position, Bias::Right);
|
||||
let location = Location {
|
||||
buffer: buffer.clone(),
|
||||
range: range_start..range_start,
|
||||
};
|
||||
// Fill in the environmental variables from the tree-sitter captures
|
||||
let mut captured_task_variables = TaskVariables::default();
|
||||
for (capture_name, value) in tasks.extra_variables.clone() {
|
||||
captured_task_variables.insert(
|
||||
task::VariableName::Custom(capture_name.into()),
|
||||
value.clone(),
|
||||
);
|
||||
}
|
||||
project.update(cx, |project, cx| {
|
||||
project.task_store().update(cx, |task_store, cx| {
|
||||
task_store.task_context_for_location(captured_task_variables, location, cx)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn spawn_nearest_task(&mut self, action: &SpawnNearestTask, cx: &mut ViewContext<Self>) {
|
||||
let Some((workspace, _)) = self.workspace.clone() else {
|
||||
return;
|
||||
};
|
||||
let Some(project) = self.project.clone() else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Try to find a closest, enclosing node using tree-sitter that has a
|
||||
// task
|
||||
let Some((buffer, buffer_row, tasks)) = self
|
||||
.find_enclosing_node_task(cx)
|
||||
// Or find the task that's closest in row-distance.
|
||||
.or_else(|| self.find_closest_task(cx))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let reveal_strategy = action.reveal;
|
||||
let task_context = Self::build_tasks_context(&project, &buffer, buffer_row, &tasks, cx);
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
let context = task_context.await?;
|
||||
let (task_source_kind, mut resolved_task) = tasks.resolve(&context).next()?;
|
||||
|
||||
let resolved = resolved_task.resolved.as_mut()?;
|
||||
resolved.reveal = reveal_strategy;
|
||||
|
||||
workspace
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
workspace::tasks::schedule_resolved_task(
|
||||
workspace,
|
||||
task_source_kind,
|
||||
resolved_task,
|
||||
false,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok()
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn find_closest_task(
|
||||
&mut self,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<(Model<Buffer>, u32, Arc<RunnableTasks>)> {
|
||||
let cursor_row = self.selections.newest_adjusted(cx).head().row;
|
||||
|
||||
let ((buffer_id, row), tasks) = self
|
||||
.tasks
|
||||
.iter()
|
||||
.min_by_key(|((_, row), _)| cursor_row.abs_diff(*row))?;
|
||||
|
||||
let buffer = self.buffer.read(cx).buffer(*buffer_id)?;
|
||||
let tasks = Arc::new(tasks.to_owned());
|
||||
Some((buffer, *row, tasks))
|
||||
}
|
||||
|
||||
fn find_enclosing_node_task(
|
||||
&mut self,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<(Model<Buffer>, u32, Arc<RunnableTasks>)> {
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
let offset = self.selections.newest::<usize>(cx).head();
|
||||
let excerpt = snapshot.excerpt_containing(offset..offset)?;
|
||||
let buffer_id = excerpt.buffer().remote_id();
|
||||
|
||||
let layer = excerpt.buffer().syntax_layer_at(offset)?;
|
||||
let mut cursor = layer.node().walk();
|
||||
|
||||
while cursor.goto_first_child_for_byte(offset).is_some() {
|
||||
if cursor.node().end_byte() == offset {
|
||||
cursor.goto_next_sibling();
|
||||
}
|
||||
}
|
||||
|
||||
// Ascend to the smallest ancestor that contains the range and has a task.
|
||||
loop {
|
||||
let node = cursor.node();
|
||||
let node_range = node.byte_range();
|
||||
let symbol_start_row = excerpt.buffer().offset_to_point(node.start_byte()).row;
|
||||
|
||||
// Check if this node contains our offset
|
||||
if node_range.start <= offset && node_range.end >= offset {
|
||||
// If it contains offset, check for task
|
||||
if let Some(tasks) = self.tasks.get(&(buffer_id, symbol_start_row)) {
|
||||
let buffer = self.buffer.read(cx).buffer(buffer_id)?;
|
||||
return Some((buffer, symbol_start_row, Arc::new(tasks.to_owned())));
|
||||
}
|
||||
}
|
||||
|
||||
if !cursor.goto_parent() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn render_run_indicator(
|
||||
&self,
|
||||
_style: &EditorStyle,
|
||||
@@ -7355,11 +7463,12 @@ impl Editor {
|
||||
if !selection.is_empty() && !line_mode {
|
||||
selection.goal = SelectionGoal::None;
|
||||
}
|
||||
let (cursor, goal) = movement::up(
|
||||
let (cursor, goal) = movement::up2(
|
||||
map,
|
||||
selection.start,
|
||||
selection.goal,
|
||||
false,
|
||||
false,
|
||||
text_layout_details,
|
||||
);
|
||||
selection.collapse_to(cursor, goal);
|
||||
@@ -7520,8 +7629,16 @@ impl Editor {
|
||||
pub fn select_up(&mut self, _: &SelectUp, cx: &mut ViewContext<Self>) {
|
||||
let text_layout_details = &self.text_layout_details(cx);
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_heads_with(|map, head, goal| {
|
||||
movement::up(map, head, goal, false, text_layout_details)
|
||||
s.move_with(|map, selection| {
|
||||
let (head, goal) = movement::up2(
|
||||
map,
|
||||
selection.head(),
|
||||
selection.goal,
|
||||
false,
|
||||
!selection.reversed,
|
||||
text_layout_details,
|
||||
);
|
||||
selection.set_head(head, goal);
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -8665,14 +8782,22 @@ impl Editor {
|
||||
let snapshot = this.buffer.read(cx).read(cx);
|
||||
let empty_str: Arc<str> = Arc::default();
|
||||
let mut suffixes_inserted = Vec::new();
|
||||
let ignore_indent = action.ignore_indent;
|
||||
|
||||
fn comment_prefix_range(
|
||||
snapshot: &MultiBufferSnapshot,
|
||||
row: MultiBufferRow,
|
||||
comment_prefix: &str,
|
||||
comment_prefix_whitespace: &str,
|
||||
ignore_indent: bool,
|
||||
) -> Range<Point> {
|
||||
let start = Point::new(row.0, snapshot.indent_size_for_line(row).len);
|
||||
let indent_size = if ignore_indent {
|
||||
0
|
||||
} else {
|
||||
snapshot.indent_size_for_line(row).len
|
||||
};
|
||||
|
||||
let start = Point::new(row.0, indent_size);
|
||||
|
||||
let mut line_bytes = snapshot
|
||||
.bytes_in_range(start..snapshot.max_point())
|
||||
@@ -8768,7 +8893,16 @@ impl Editor {
|
||||
}
|
||||
|
||||
// If the language has line comments, toggle those.
|
||||
let full_comment_prefixes = language.line_comment_prefixes();
|
||||
let mut full_comment_prefixes = language.line_comment_prefixes().to_vec();
|
||||
|
||||
// If ignore_indent is set, trim spaces from the right side of all full_comment_prefixes
|
||||
if ignore_indent {
|
||||
full_comment_prefixes = full_comment_prefixes
|
||||
.into_iter()
|
||||
.map(|s| Arc::from(s.trim_end()))
|
||||
.collect();
|
||||
}
|
||||
|
||||
if !full_comment_prefixes.is_empty() {
|
||||
let first_prefix = full_comment_prefixes
|
||||
.first()
|
||||
@@ -8795,6 +8929,7 @@ impl Editor {
|
||||
row,
|
||||
&prefix[..trimmed_prefix_len],
|
||||
&prefix[trimmed_prefix_len..],
|
||||
ignore_indent,
|
||||
)
|
||||
})
|
||||
.max_by_key(|range| range.end.column - range.start.column)
|
||||
@@ -8835,6 +8970,7 @@ impl Editor {
|
||||
start_row,
|
||||
comment_prefix,
|
||||
comment_prefix_whitespace,
|
||||
ignore_indent,
|
||||
);
|
||||
let suffix_range = comment_suffix_range(
|
||||
snapshot.deref(),
|
||||
|
||||
@@ -8533,6 +8533,131 @@ async fn test_toggle_comment(cx: &mut gpui::TestAppContext) {
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_toggle_comment_ignore_indent(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
let language = Arc::new(Language::new(
|
||||
LanguageConfig {
|
||||
line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::LANGUAGE.into()),
|
||||
));
|
||||
cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
|
||||
|
||||
let toggle_comments = &ToggleComments {
|
||||
advance_downwards: false,
|
||||
ignore_indent: true,
|
||||
};
|
||||
|
||||
// If multiple selections intersect a line, the line is only toggled once.
|
||||
cx.set_state(indoc! {"
|
||||
fn a() {
|
||||
// «b();
|
||||
// c();
|
||||
// ˇ» d();
|
||||
}
|
||||
"});
|
||||
|
||||
cx.update_editor(|e, cx| e.toggle_comments(toggle_comments, cx));
|
||||
|
||||
cx.assert_editor_state(indoc! {"
|
||||
fn a() {
|
||||
«b();
|
||||
c();
|
||||
ˇ» d();
|
||||
}
|
||||
"});
|
||||
|
||||
// The comment prefix is inserted at the beginning of each line
|
||||
cx.update_editor(|e, cx| e.toggle_comments(toggle_comments, cx));
|
||||
|
||||
cx.assert_editor_state(indoc! {"
|
||||
fn a() {
|
||||
// «b();
|
||||
// c();
|
||||
// ˇ» d();
|
||||
}
|
||||
"});
|
||||
|
||||
// If a selection ends at the beginning of a line, that line is not toggled.
|
||||
cx.set_selections_state(indoc! {"
|
||||
fn a() {
|
||||
// b();
|
||||
// «c();
|
||||
ˇ»// d();
|
||||
}
|
||||
"});
|
||||
|
||||
cx.update_editor(|e, cx| e.toggle_comments(toggle_comments, cx));
|
||||
|
||||
cx.assert_editor_state(indoc! {"
|
||||
fn a() {
|
||||
// b();
|
||||
«c();
|
||||
ˇ»// d();
|
||||
}
|
||||
"});
|
||||
|
||||
// If a selection span a single line and is empty, the line is toggled.
|
||||
cx.set_state(indoc! {"
|
||||
fn a() {
|
||||
a();
|
||||
b();
|
||||
ˇ
|
||||
}
|
||||
"});
|
||||
|
||||
cx.update_editor(|e, cx| e.toggle_comments(toggle_comments, cx));
|
||||
|
||||
cx.assert_editor_state(indoc! {"
|
||||
fn a() {
|
||||
a();
|
||||
b();
|
||||
//ˇ
|
||||
}
|
||||
"});
|
||||
|
||||
// If a selection span multiple lines, empty lines are not toggled.
|
||||
cx.set_state(indoc! {"
|
||||
fn a() {
|
||||
«a();
|
||||
|
||||
c();ˇ»
|
||||
}
|
||||
"});
|
||||
|
||||
cx.update_editor(|e, cx| e.toggle_comments(toggle_comments, cx));
|
||||
|
||||
cx.assert_editor_state(indoc! {"
|
||||
fn a() {
|
||||
// «a();
|
||||
|
||||
// c();ˇ»
|
||||
}
|
||||
"});
|
||||
|
||||
// If a selection includes multiple comment prefixes, all lines are uncommented.
|
||||
cx.set_state(indoc! {"
|
||||
fn a() {
|
||||
// «a();
|
||||
/// b();
|
||||
//! c();ˇ»
|
||||
}
|
||||
"});
|
||||
|
||||
cx.update_editor(|e, cx| e.toggle_comments(toggle_comments, cx));
|
||||
|
||||
cx.assert_editor_state(indoc! {"
|
||||
fn a() {
|
||||
«a();
|
||||
b();
|
||||
c();ˇ»
|
||||
}
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
@@ -8554,6 +8679,7 @@ async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext)
|
||||
|
||||
let toggle_comments = &ToggleComments {
|
||||
advance_downwards: true,
|
||||
ignore_indent: false,
|
||||
};
|
||||
|
||||
// Single cursor on one line -> advance
|
||||
@@ -13204,6 +13330,89 @@ async fn test_goto_definition_with_find_all_references_fallback(cx: &mut gpui::T
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_find_enclosing_node_with_task(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let language = Arc::new(Language::new(
|
||||
LanguageConfig::default(),
|
||||
Some(tree_sitter_rust::LANGUAGE.into()),
|
||||
));
|
||||
|
||||
let text = r#"
|
||||
#[cfg(test)]
|
||||
mod tests() {
|
||||
#[test]
|
||||
fn runnable_1() {
|
||||
let a = 1;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runnable_2() {
|
||||
let a = 1;
|
||||
let b = 2;
|
||||
}
|
||||
}
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_file("/file.rs", Default::default()).await;
|
||||
|
||||
let project = Project::test(fs, ["/a".as_ref()], cx).await;
|
||||
let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||
let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
|
||||
let buffer = cx.new_model(|cx| Buffer::local(text, cx).with_language(language, cx));
|
||||
let multi_buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer.clone(), cx));
|
||||
|
||||
let editor = cx.new_view(|cx| {
|
||||
Editor::new(
|
||||
EditorMode::Full,
|
||||
multi_buffer,
|
||||
Some(project.clone()),
|
||||
true,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.tasks.insert(
|
||||
(buffer.read(cx).remote_id(), 3),
|
||||
RunnableTasks {
|
||||
templates: vec![],
|
||||
offset: MultiBufferOffset(43),
|
||||
column: 0,
|
||||
extra_variables: HashMap::default(),
|
||||
context_range: BufferOffset(43)..BufferOffset(85),
|
||||
},
|
||||
);
|
||||
editor.tasks.insert(
|
||||
(buffer.read(cx).remote_id(), 8),
|
||||
RunnableTasks {
|
||||
templates: vec![],
|
||||
offset: MultiBufferOffset(86),
|
||||
column: 0,
|
||||
extra_variables: HashMap::default(),
|
||||
context_range: BufferOffset(86)..BufferOffset(191),
|
||||
},
|
||||
);
|
||||
|
||||
// Test finding task when cursor is inside function body
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
|
||||
});
|
||||
let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
|
||||
assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
|
||||
|
||||
// Test finding task when cursor is on function name
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
|
||||
});
|
||||
let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
|
||||
assert_eq!(row, 8, "Should find task when cursor is on function name");
|
||||
});
|
||||
}
|
||||
|
||||
fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
|
||||
let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
|
||||
point..point
|
||||
|
||||
@@ -449,7 +449,8 @@ impl EditorElement {
|
||||
register_action(view, cx, Editor::apply_all_diff_hunks);
|
||||
register_action(view, cx, Editor::apply_selected_diff_hunks);
|
||||
register_action(view, cx, Editor::open_active_item_in_terminal);
|
||||
register_action(view, cx, Editor::reload_file)
|
||||
register_action(view, cx, Editor::reload_file);
|
||||
register_action(view, cx, Editor::spawn_nearest_task);
|
||||
}
|
||||
|
||||
fn register_key_listeners(&self, cx: &mut WindowContext, layout: &EditorLayout) {
|
||||
|
||||
@@ -76,6 +76,26 @@ pub fn saturating_right(map: &DisplaySnapshot, mut point: DisplayPoint) -> Displ
|
||||
map.clip_point(point, Bias::Right)
|
||||
}
|
||||
|
||||
/// Returns a display point for the preceding displayed line (which might be a soft-wrapped line).
|
||||
pub fn up2(
|
||||
map: &DisplaySnapshot,
|
||||
start: DisplayPoint,
|
||||
goal: SelectionGoal,
|
||||
preserve_column_at_start: bool,
|
||||
skip_replace_blocks: bool,
|
||||
text_layout_details: &TextLayoutDetails,
|
||||
) -> (DisplayPoint, SelectionGoal) {
|
||||
up_by_rows2(
|
||||
map,
|
||||
start,
|
||||
1,
|
||||
goal,
|
||||
preserve_column_at_start,
|
||||
skip_replace_blocks,
|
||||
text_layout_details,
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns a display point for the preceding displayed line (which might be a soft-wrapped line).
|
||||
pub fn up(
|
||||
map: &DisplaySnapshot,
|
||||
@@ -112,6 +132,26 @@ pub fn down(
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns a display point for the next displayed line (which might be a soft-wrapped line).
|
||||
pub fn down2(
|
||||
map: &DisplaySnapshot,
|
||||
start: DisplayPoint,
|
||||
goal: SelectionGoal,
|
||||
preserve_column_at_end: bool,
|
||||
skip_replace_blocks: bool,
|
||||
text_layout_details: &TextLayoutDetails,
|
||||
) -> (DisplayPoint, SelectionGoal) {
|
||||
down_by_rows2(
|
||||
map,
|
||||
start,
|
||||
1,
|
||||
goal,
|
||||
preserve_column_at_end,
|
||||
skip_replace_blocks,
|
||||
text_layout_details,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn up_by_rows(
|
||||
map: &DisplaySnapshot,
|
||||
start: DisplayPoint,
|
||||
@@ -151,6 +191,46 @@ pub(crate) fn up_by_rows(
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn up_by_rows2(
|
||||
map: &DisplaySnapshot,
|
||||
start: DisplayPoint,
|
||||
row_count: u32,
|
||||
goal: SelectionGoal,
|
||||
preserve_column_at_start: bool,
|
||||
skip_replace_blocks: bool,
|
||||
text_layout_details: &TextLayoutDetails,
|
||||
) -> (DisplayPoint, SelectionGoal) {
|
||||
let mut goal_x = match goal {
|
||||
SelectionGoal::HorizontalPosition(x) => x.into(),
|
||||
SelectionGoal::WrappedHorizontalPosition((_, x)) => x.into(),
|
||||
SelectionGoal::HorizontalRange { end, .. } => end.into(),
|
||||
_ => map.x_for_display_point(start, text_layout_details),
|
||||
};
|
||||
|
||||
let prev_row = DisplayRow(start.row().0.saturating_sub(row_count));
|
||||
let mut point = map.clip_point(
|
||||
DisplayPoint::new(prev_row, map.line_len(prev_row)),
|
||||
Bias::Left,
|
||||
);
|
||||
if point.row() < start.row() {
|
||||
*point.column_mut() = map.display_column_for_x(point.row(), goal_x, text_layout_details)
|
||||
} else if preserve_column_at_start {
|
||||
return (start, goal);
|
||||
} else {
|
||||
point = DisplayPoint::new(DisplayRow(0), 0);
|
||||
goal_x = px(0.);
|
||||
}
|
||||
|
||||
let mut clipped_point = map.clip_point_2(point, Bias::Left, skip_replace_blocks);
|
||||
if clipped_point.row() < point.row() {
|
||||
clipped_point = map.clip_point_2(point, Bias::Right, skip_replace_blocks);
|
||||
}
|
||||
(
|
||||
clipped_point,
|
||||
SelectionGoal::HorizontalPosition(goal_x.into()),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn down_by_rows(
|
||||
map: &DisplaySnapshot,
|
||||
start: DisplayPoint,
|
||||
@@ -187,6 +267,43 @@ pub(crate) fn down_by_rows(
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn down_by_rows2(
|
||||
map: &DisplaySnapshot,
|
||||
start: DisplayPoint,
|
||||
row_count: u32,
|
||||
goal: SelectionGoal,
|
||||
preserve_column_at_end: bool,
|
||||
skip_replace_blocks: bool,
|
||||
text_layout_details: &TextLayoutDetails,
|
||||
) -> (DisplayPoint, SelectionGoal) {
|
||||
let mut goal_x = match goal {
|
||||
SelectionGoal::HorizontalPosition(x) => x.into(),
|
||||
SelectionGoal::WrappedHorizontalPosition((_, x)) => x.into(),
|
||||
SelectionGoal::HorizontalRange { end, .. } => end.into(),
|
||||
_ => map.x_for_display_point(start, text_layout_details),
|
||||
};
|
||||
|
||||
let new_row = DisplayRow(start.row().0 + row_count);
|
||||
let mut point = map.clip_point(DisplayPoint::new(new_row, 0), Bias::Right);
|
||||
if point.row() > start.row() {
|
||||
*point.column_mut() = map.display_column_for_x(point.row(), goal_x, text_layout_details)
|
||||
} else if preserve_column_at_end {
|
||||
return (start, goal);
|
||||
} else {
|
||||
point = map.max_point();
|
||||
goal_x = map.x_for_display_point(point, text_layout_details)
|
||||
}
|
||||
|
||||
let mut clipped_point = map.clip_point_2(point, Bias::Right, skip_replace_blocks);
|
||||
if clipped_point.row() > point.row() {
|
||||
clipped_point = map.clip_point_2(point, Bias::Left, skip_replace_blocks);
|
||||
}
|
||||
(
|
||||
clipped_point,
|
||||
SelectionGoal::HorizontalPosition(goal_x.into()),
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns a position of the start of line.
|
||||
/// If `stop_at_soft_boundaries` is true, the returned position is that of the
|
||||
/// displayed line (e.g. it could actually be in the middle of a text line if that line is soft-wrapped).
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::{
|
||||
cell::Ref,
|
||||
cmp, iter, mem,
|
||||
iter, mem,
|
||||
ops::{Deref, DerefMut, Range, Sub},
|
||||
sync::Arc,
|
||||
};
|
||||
@@ -111,9 +111,9 @@ impl SelectionsCollection {
|
||||
where
|
||||
D: 'a + TextDimension + Ord + Sub<D, Output = D>,
|
||||
{
|
||||
let map = self.display_map(cx);
|
||||
let disjoint_anchors = &self.disjoint;
|
||||
let mut disjoint = resolve_multiple::<D, _>(disjoint_anchors.iter(), &map).peekable();
|
||||
let mut disjoint =
|
||||
resolve_multiple::<D, _>(disjoint_anchors.iter(), &self.buffer(cx)).peekable();
|
||||
|
||||
let mut pending_opt = self.pending::<D>(cx);
|
||||
|
||||
@@ -199,21 +199,21 @@ impl SelectionsCollection {
|
||||
where
|
||||
D: 'a + TextDimension + Ord + Sub<D, Output = D> + std::fmt::Debug,
|
||||
{
|
||||
let map = self.display_map(cx);
|
||||
let buffer = self.buffer(cx);
|
||||
let start_ix = match self
|
||||
.disjoint
|
||||
.binary_search_by(|probe| probe.end.cmp(&range.start, &map.buffer_snapshot))
|
||||
.binary_search_by(|probe| probe.end.cmp(&range.start, &buffer))
|
||||
{
|
||||
Ok(ix) | Err(ix) => ix,
|
||||
};
|
||||
let end_ix = match self
|
||||
.disjoint
|
||||
.binary_search_by(|probe| probe.start.cmp(&range.end, &map.buffer_snapshot))
|
||||
.binary_search_by(|probe| probe.start.cmp(&range.end, &buffer))
|
||||
{
|
||||
Ok(ix) => ix + 1,
|
||||
Err(ix) => ix,
|
||||
};
|
||||
resolve_multiple(&self.disjoint[start_ix..end_ix], &map).collect()
|
||||
resolve_multiple(&self.disjoint[start_ix..end_ix], &buffer).collect()
|
||||
}
|
||||
|
||||
pub fn all_display(
|
||||
@@ -538,9 +538,9 @@ impl<'a> MutableSelectionsCollection<'a> {
|
||||
}
|
||||
|
||||
pub fn select_anchors(&mut self, selections: Vec<Selection<Anchor>>) {
|
||||
let map = self.display_map();
|
||||
let buffer = self.buffer.read(self.cx).snapshot(self.cx);
|
||||
let resolved_selections =
|
||||
resolve_multiple::<usize, _>(&selections, &map).collect::<Vec<_>>();
|
||||
resolve_multiple::<usize, _>(&selections, &buffer).collect::<Vec<_>>();
|
||||
self.select(resolved_selections);
|
||||
}
|
||||
|
||||
@@ -804,8 +804,8 @@ impl<'a> MutableSelectionsCollection<'a> {
|
||||
.collect();
|
||||
|
||||
if !adjusted_disjoint.is_empty() {
|
||||
let map = self.display_map();
|
||||
let resolved_selections = resolve_multiple(adjusted_disjoint.iter(), &map).collect();
|
||||
let resolved_selections =
|
||||
resolve_multiple(adjusted_disjoint.iter(), &self.buffer()).collect();
|
||||
self.select::<usize>(resolved_selections);
|
||||
}
|
||||
|
||||
@@ -851,55 +851,25 @@ impl<'a> DerefMut for MutableSelectionsCollection<'a> {
|
||||
// Panics if passed selections are not in order
|
||||
pub(crate) fn resolve_multiple<'a, D, I>(
|
||||
selections: I,
|
||||
map: &'a DisplaySnapshot,
|
||||
snapshot: &MultiBufferSnapshot,
|
||||
) -> impl 'a + Iterator<Item = Selection<D>>
|
||||
where
|
||||
D: TextDimension + Clone + Ord + Sub<D, Output = D>,
|
||||
D: TextDimension + Ord + Sub<D, Output = D>,
|
||||
I: 'a + IntoIterator<Item = &'a Selection<Anchor>>,
|
||||
{
|
||||
let (to_summarize, selections) = selections.into_iter().tee();
|
||||
let mut summaries = map
|
||||
.buffer_snapshot
|
||||
.summaries_for_anchors::<Point, _>(to_summarize.flat_map(|s| [&s.start, &s.end]))
|
||||
let mut summaries = snapshot
|
||||
.summaries_for_anchors::<D, _>(
|
||||
to_summarize
|
||||
.flat_map(|s| [&s.start, &s.end])
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.into_iter();
|
||||
let mut selection_endpoints = map.buffer_snapshot.dimensions_from_points::<D>(
|
||||
iter::from_fn(move || {
|
||||
let start = map.display_point_to_point(
|
||||
map.point_to_display_point(summaries.next().unwrap(), Bias::Left),
|
||||
Bias::Left,
|
||||
);
|
||||
let end = map.display_point_to_point(
|
||||
map.point_to_display_point(summaries.next().unwrap(), Bias::Right),
|
||||
Bias::Right,
|
||||
);
|
||||
Some([start, end])
|
||||
})
|
||||
.flatten(),
|
||||
);
|
||||
|
||||
let mut selections = selections
|
||||
.map(move |s| {
|
||||
let start = selection_endpoints.next().unwrap();
|
||||
let end = selection_endpoints.next().unwrap();
|
||||
Selection {
|
||||
id: s.id,
|
||||
start,
|
||||
end,
|
||||
reversed: s.reversed,
|
||||
goal: s.goal,
|
||||
}
|
||||
})
|
||||
.peekable();
|
||||
iter::from_fn(move || {
|
||||
let mut selection = selections.next()?;
|
||||
while let Some(next_selection) = selections.peek() {
|
||||
if selection.end >= next_selection.start {
|
||||
selection.end = cmp::max(selection.end, next_selection.end.clone());
|
||||
selections.next();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Some(selection)
|
||||
selections.map(move |s| Selection {
|
||||
id: s.id,
|
||||
start: summaries.next().unwrap(),
|
||||
end: summaries.next().unwrap(),
|
||||
reversed: s.reversed,
|
||||
goal: s.goal,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -77,9 +77,9 @@ impl GitHostingProvider for Gitlab {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut path_segments = url.path_segments()?;
|
||||
let owner = path_segments.next()?;
|
||||
let repo = path_segments.next()?.trim_end_matches(".git");
|
||||
let mut path_segments = url.path_segments()?.collect::<Vec<_>>();
|
||||
let repo = path_segments.pop()?.trim_end_matches(".git");
|
||||
let owner = path_segments.join("/");
|
||||
|
||||
Some(ParsedGitRemote {
|
||||
owner: owner.into(),
|
||||
@@ -178,6 +178,23 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_remote_url_given_self_hosted_https_url_with_subgroup() {
|
||||
let remote_url = "https://gitlab.my-enterprise.com/group/subgroup/zed.git";
|
||||
let parsed_remote = Gitlab::from_remote_url(remote_url)
|
||||
.unwrap()
|
||||
.parse_remote_url(remote_url)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
parsed_remote,
|
||||
ParsedGitRemote {
|
||||
owner: "group/subgroup".into(),
|
||||
repo: "zed".into(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_gitlab_permalink() {
|
||||
let permalink = Gitlab::new().build_permalink(
|
||||
|
||||
@@ -75,6 +75,18 @@ impl Keymap {
|
||||
.filter(move |binding| binding.action().partial_eq(action))
|
||||
}
|
||||
|
||||
/// all bindings for input returns all bindings that might match the input
|
||||
/// (without checking context)
|
||||
pub fn all_bindings_for_input(&self, input: &[Keystroke]) -> Vec<KeyBinding> {
|
||||
self.bindings()
|
||||
.rev()
|
||||
.filter_map(|binding| {
|
||||
binding.match_keystrokes(input).filter(|pending| !pending)?;
|
||||
Some(binding.clone())
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// bindings_for_input returns a list of bindings that match the given input,
|
||||
/// and a boolean indicating whether or not more bindings might match if
|
||||
/// the input was longer.
|
||||
|
||||
@@ -69,6 +69,11 @@ impl KeyBinding {
|
||||
pub fn action(&self) -> &dyn Action {
|
||||
self.action.as_ref()
|
||||
}
|
||||
|
||||
/// Get the predicate used to match this binding
|
||||
pub fn predicate(&self) -> Option<&KeyBindingContextPredicate> {
|
||||
self.context_predicate.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for KeyBinding {
|
||||
|
||||
@@ -11,9 +11,12 @@ use std::fmt;
|
||||
pub struct KeyContext(SmallVec<[ContextEntry; 1]>);
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
|
||||
struct ContextEntry {
|
||||
key: SharedString,
|
||||
value: Option<SharedString>,
|
||||
/// An entry in a KeyContext
|
||||
pub struct ContextEntry {
|
||||
/// The key (or name if no value)
|
||||
pub key: SharedString,
|
||||
/// The value
|
||||
pub value: Option<SharedString>,
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a str> for KeyContext {
|
||||
@@ -39,6 +42,17 @@ impl KeyContext {
|
||||
context
|
||||
}
|
||||
|
||||
/// Returns the primary context entry (usually the name of the component)
|
||||
pub fn primary(&self) -> Option<&ContextEntry> {
|
||||
self.0.iter().find(|p| p.value.is_none())
|
||||
}
|
||||
|
||||
/// Returns everything except the primary context entry.
|
||||
pub fn secondary(&self) -> impl Iterator<Item = &ContextEntry> {
|
||||
let primary = self.primary();
|
||||
self.0.iter().filter(move |&p| Some(p) != primary)
|
||||
}
|
||||
|
||||
/// Parse a key context from a string.
|
||||
/// The key context format is very simple:
|
||||
/// - either a single identifier, such as `StatusBar`
|
||||
@@ -178,6 +192,20 @@ pub enum KeyBindingContextPredicate {
|
||||
),
|
||||
}
|
||||
|
||||
impl fmt::Display for KeyBindingContextPredicate {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Identifier(name) => write!(f, "{}", name),
|
||||
Self::Equal(left, right) => write!(f, "{} == {}", left, right),
|
||||
Self::NotEqual(left, right) => write!(f, "{} != {}", left, right),
|
||||
Self::Not(pred) => write!(f, "!{}", pred),
|
||||
Self::Child(parent, child) => write!(f, "{} > {}", parent, child),
|
||||
Self::And(left, right) => write!(f, "({} && {})", left, right),
|
||||
Self::Or(left, right) => write!(f, "({} || {})", left, right),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl KeyBindingContextPredicate {
|
||||
/// Parse a string in the same format as the keymap's context field.
|
||||
///
|
||||
|
||||
@@ -121,6 +121,32 @@ impl Keystroke {
|
||||
})
|
||||
}
|
||||
|
||||
/// Produces a representation of this key that Parse can understand.
|
||||
pub fn unparse(&self) -> String {
|
||||
let mut str = String::new();
|
||||
if self.modifiers.control {
|
||||
str.push_str("ctrl-");
|
||||
}
|
||||
if self.modifiers.alt {
|
||||
str.push_str("alt-");
|
||||
}
|
||||
if self.modifiers.platform {
|
||||
#[cfg(target_os = "macos")]
|
||||
str.push_str("cmd-");
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
str.push_str("super-");
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
str.push_str("win-");
|
||||
}
|
||||
if self.modifiers.shift {
|
||||
str.push_str("shift-");
|
||||
}
|
||||
str.push_str(&self.key);
|
||||
str
|
||||
}
|
||||
|
||||
/// Returns true if this keystroke left
|
||||
/// the ime system in an incomplete state.
|
||||
pub fn is_ime_in_progress(&self) -> bool {
|
||||
|
||||
@@ -3324,17 +3324,18 @@ impl<'a> WindowContext<'a> {
|
||||
return;
|
||||
}
|
||||
|
||||
self.pending_input_changed();
|
||||
self.propagate_event = true;
|
||||
for binding in match_result.bindings {
|
||||
self.dispatch_action_on_node(node_id, binding.action.as_ref());
|
||||
if !self.propagate_event {
|
||||
self.dispatch_keystroke_observers(event, Some(binding.action));
|
||||
self.pending_input_changed();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
self.finish_dispatch_key_event(event, dispatch_path)
|
||||
self.finish_dispatch_key_event(event, dispatch_path);
|
||||
self.pending_input_changed();
|
||||
}
|
||||
|
||||
fn finish_dispatch_key_event(
|
||||
@@ -3664,6 +3665,22 @@ impl<'a> WindowContext<'a> {
|
||||
receiver
|
||||
}
|
||||
|
||||
/// Returns the current context stack.
|
||||
pub fn context_stack(&self) -> Vec<KeyContext> {
|
||||
let dispatch_tree = &self.window.rendered_frame.dispatch_tree;
|
||||
let node_id = self
|
||||
.window
|
||||
.focus
|
||||
.and_then(|focus_id| dispatch_tree.focusable_node_id(focus_id))
|
||||
.unwrap_or_else(|| dispatch_tree.root_node_id());
|
||||
|
||||
dispatch_tree
|
||||
.dispatch_path(node_id)
|
||||
.iter()
|
||||
.filter_map(move |&node_id| dispatch_tree.node(node_id).context.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Returns all available actions for the focused element.
|
||||
pub fn available_actions(&self) -> Vec<Box<dyn Action>> {
|
||||
let node_id = self
|
||||
@@ -3704,6 +3721,11 @@ impl<'a> WindowContext<'a> {
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns key bindings that invoke the given action on the currently focused element.
|
||||
pub fn all_bindings_for_input(&self, input: &[Keystroke]) -> Vec<KeyBinding> {
|
||||
RefCell::borrow(&self.keymap).all_bindings_for_input(input)
|
||||
}
|
||||
|
||||
/// Returns any bindings that would invoke the given action on the given focus handle if it were focused.
|
||||
pub fn bindings_for_action_in(
|
||||
&self,
|
||||
|
||||
@@ -19,6 +19,7 @@ copilot.workspace = true
|
||||
editor.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
itertools.workspace = true
|
||||
language.workspace = true
|
||||
lsp.workspace = true
|
||||
project.workspace = true
|
||||
@@ -28,6 +29,7 @@ theme.workspace = true
|
||||
tree-sitter.workspace = true
|
||||
ui.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
client = { workspace = true, features = ["test-support"] }
|
||||
|
||||
280
crates/language_tools/src/key_context_view.rs
Normal file
280
crates/language_tools/src/key_context_view.rs
Normal file
@@ -0,0 +1,280 @@
|
||||
use gpui::{
|
||||
actions, Action, AppContext, EventEmitter, FocusHandle, FocusableView,
|
||||
KeyBindingContextPredicate, KeyContext, Keystroke, MouseButton, Render, Subscription,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use serde_json::json;
|
||||
use ui::{
|
||||
div, h_flex, px, v_flex, ButtonCommon, Clickable, FluentBuilder, InteractiveElement, Label,
|
||||
LabelCommon, LabelSize, ParentElement, SharedString, StatefulInteractiveElement, Styled,
|
||||
ViewContext, VisualContext, WindowContext,
|
||||
};
|
||||
use ui::{Button, ButtonStyle};
|
||||
use workspace::Item;
|
||||
use workspace::Workspace;
|
||||
|
||||
actions!(debug, [OpenKeyContextView]);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.observe_new_views(|workspace: &mut Workspace, _| {
|
||||
workspace.register_action(|workspace, _: &OpenKeyContextView, cx| {
|
||||
let key_context_view = cx.new_view(KeyContextView::new);
|
||||
workspace.add_item_to_active_pane(Box::new(key_context_view), None, true, cx)
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
struct KeyContextView {
|
||||
pending_keystrokes: Option<Vec<Keystroke>>,
|
||||
last_keystrokes: Option<SharedString>,
|
||||
last_possibilities: Vec<(SharedString, SharedString, Option<bool>)>,
|
||||
context_stack: Vec<KeyContext>,
|
||||
focus_handle: FocusHandle,
|
||||
_subscriptions: [Subscription; 2],
|
||||
}
|
||||
|
||||
impl KeyContextView {
|
||||
pub fn new(cx: &mut ViewContext<Self>) -> Self {
|
||||
let sub1 = cx.observe_keystrokes(|this, e, cx| {
|
||||
let mut pending = this.pending_keystrokes.take().unwrap_or_default();
|
||||
pending.push(e.keystroke.clone());
|
||||
let mut possibilities = cx.all_bindings_for_input(&pending);
|
||||
possibilities.reverse();
|
||||
this.context_stack = cx.context_stack();
|
||||
this.last_keystrokes = Some(
|
||||
json!(pending.iter().map(|p| p.unparse()).join(" "))
|
||||
.to_string()
|
||||
.into(),
|
||||
);
|
||||
this.last_possibilities = possibilities
|
||||
.into_iter()
|
||||
.map(|binding| {
|
||||
let match_state = if let Some(predicate) = binding.predicate() {
|
||||
if this.matches(predicate) {
|
||||
if this.action_matches(&e.action, binding.action()) {
|
||||
Some(true)
|
||||
} else {
|
||||
Some(false)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
if this.action_matches(&e.action, binding.action()) {
|
||||
Some(true)
|
||||
} else {
|
||||
Some(false)
|
||||
}
|
||||
};
|
||||
let predicate = if let Some(predicate) = binding.predicate() {
|
||||
format!("{}", predicate)
|
||||
} else {
|
||||
"".to_string()
|
||||
};
|
||||
let mut name = binding.action().name();
|
||||
if name == "zed::NoAction" {
|
||||
name = "(null)"
|
||||
}
|
||||
|
||||
(
|
||||
name.to_owned().into(),
|
||||
json!(predicate).to_string().into(),
|
||||
match_state,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
});
|
||||
let sub2 = cx.observe_pending_input(|this, cx| {
|
||||
this.pending_keystrokes = cx
|
||||
.pending_input_keystrokes()
|
||||
.map(|k| k.iter().cloned().collect());
|
||||
if this.pending_keystrokes.is_some() {
|
||||
this.last_keystrokes.take();
|
||||
}
|
||||
cx.notify();
|
||||
});
|
||||
|
||||
Self {
|
||||
context_stack: Vec::new(),
|
||||
pending_keystrokes: None,
|
||||
last_keystrokes: None,
|
||||
last_possibilities: Vec::new(),
|
||||
focus_handle: cx.focus_handle(),
|
||||
_subscriptions: [sub1, sub2],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<()> for KeyContextView {}
|
||||
|
||||
impl FocusableView for KeyContextView {
|
||||
fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
impl KeyContextView {
|
||||
fn set_context_stack(&mut self, stack: Vec<KeyContext>, cx: &mut ViewContext<Self>) {
|
||||
self.context_stack = stack;
|
||||
cx.notify()
|
||||
}
|
||||
|
||||
fn matches(&self, predicate: &KeyBindingContextPredicate) -> bool {
|
||||
let mut stack = self.context_stack.clone();
|
||||
while !stack.is_empty() {
|
||||
if predicate.eval(&stack) {
|
||||
return true;
|
||||
}
|
||||
stack.pop();
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn action_matches(&self, a: &Option<Box<dyn Action>>, b: &dyn Action) -> bool {
|
||||
if let Some(last_action) = a {
|
||||
last_action.partial_eq(b)
|
||||
} else {
|
||||
b.name() == "zed::NoAction"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Item for KeyContextView {
|
||||
type Event = ();
|
||||
|
||||
fn to_item_events(_: &Self::Event, _: impl FnMut(workspace::item::ItemEvent)) {}
|
||||
|
||||
fn tab_content_text(&self, _cx: &WindowContext) -> Option<SharedString> {
|
||||
Some("Keyboard Context".into())
|
||||
}
|
||||
|
||||
fn telemetry_event_text(&self) -> Option<&'static str> {
|
||||
None
|
||||
}
|
||||
|
||||
fn clone_on_split(
|
||||
&self,
|
||||
_workspace_id: Option<workspace::WorkspaceId>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<gpui::View<Self>>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Some(cx.new_view(Self::new))
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for KeyContextView {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl ui::IntoElement {
|
||||
use itertools::Itertools;
|
||||
v_flex()
|
||||
.id("key-context-view")
|
||||
.overflow_scroll()
|
||||
.size_full()
|
||||
.max_h_full()
|
||||
.pt_4()
|
||||
.pl_4()
|
||||
.track_focus(&self.focus_handle)
|
||||
.key_context("KeyContextView")
|
||||
.on_mouse_up_out(
|
||||
MouseButton::Left,
|
||||
cx.listener(|this, _, cx| {
|
||||
this.last_keystrokes.take();
|
||||
this.set_context_stack(cx.context_stack(), cx);
|
||||
}),
|
||||
)
|
||||
.on_mouse_up_out(
|
||||
MouseButton::Right,
|
||||
cx.listener(|_, _, cx| {
|
||||
cx.defer(|this, cx| {
|
||||
this.last_keystrokes.take();
|
||||
this.set_context_stack(cx.context_stack(), cx);
|
||||
});
|
||||
}),
|
||||
)
|
||||
.child(Label::new("Keyboard Context").size(LabelSize::Large))
|
||||
.child(Label::new("This view lets you determine the current context stack for creating custom key bindings in Zed. When a keyboard shortcut is triggered, it also shows all the possible contexts it could have triggered in, and which one matched."))
|
||||
.child(
|
||||
h_flex()
|
||||
.mt_4()
|
||||
.gap_4()
|
||||
.child(
|
||||
Button::new("default", "Open Documentation")
|
||||
.style(ButtonStyle::Filled)
|
||||
.on_click(|_, cx| cx.open_url("https://zed.dev/docs/key-bindings")),
|
||||
)
|
||||
.child(
|
||||
Button::new("default", "View default keymap")
|
||||
.style(ButtonStyle::Filled)
|
||||
.key_binding(ui::KeyBinding::for_action(
|
||||
&zed_actions::OpenDefaultKeymap,
|
||||
cx,
|
||||
))
|
||||
.on_click(|_, cx| {
|
||||
cx.dispatch_action(workspace::SplitRight.boxed_clone());
|
||||
cx.dispatch_action(zed_actions::OpenDefaultKeymap.boxed_clone());
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new("default", "Edit your keymap")
|
||||
.style(ButtonStyle::Filled)
|
||||
.key_binding(ui::KeyBinding::for_action(&zed_actions::OpenKeymap, cx))
|
||||
.on_click(|_, cx| {
|
||||
cx.dispatch_action(workspace::SplitRight.boxed_clone());
|
||||
cx.dispatch_action(zed_actions::OpenKeymap.boxed_clone());
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Label::new("Current Context Stack")
|
||||
.size(LabelSize::Large)
|
||||
.mt_8(),
|
||||
)
|
||||
.children({
|
||||
cx.context_stack().iter().enumerate().map(|(i, context)| {
|
||||
let primary = context.primary().map(|e| e.key.clone()).unwrap_or_default();
|
||||
let secondary = context
|
||||
.secondary()
|
||||
.map(|e| {
|
||||
if let Some(value) = e.value.as_ref() {
|
||||
format!("{}={}", e.key, value)
|
||||
} else {
|
||||
e.key.to_string()
|
||||
}
|
||||
})
|
||||
.join(" ");
|
||||
Label::new(format!("{} {}", primary, secondary)).ml(px(12. * (i + 1) as f32))
|
||||
})
|
||||
})
|
||||
.child(Label::new("Last Keystroke").mt_4().size(LabelSize::Large))
|
||||
.when_some(self.pending_keystrokes.as_ref(), |el, keystrokes| {
|
||||
el.child(
|
||||
Label::new(format!(
|
||||
"Waiting for more input: {}",
|
||||
keystrokes.iter().map(|k| k.unparse()).join(" ")
|
||||
))
|
||||
.ml(px(12.)),
|
||||
)
|
||||
})
|
||||
.when_some(self.last_keystrokes.as_ref(), |el, keystrokes| {
|
||||
el.child(Label::new(format!("Typed: {}", keystrokes)).ml_4())
|
||||
.children(
|
||||
self.last_possibilities
|
||||
.iter()
|
||||
.map(|(name, predicate, state)| {
|
||||
let (text, color) = match state {
|
||||
Some(true) => ("(match)", ui::Color::Success),
|
||||
Some(false) => ("(low precedence)", ui::Color::Hint),
|
||||
None => ("(no match)", ui::Color::Error),
|
||||
};
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.ml_8()
|
||||
.child(div().min_w(px(200.)).child(Label::new(name.clone())))
|
||||
.child(Label::new(predicate.clone()))
|
||||
.child(Label::new(text).color(color))
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
mod key_context_view;
|
||||
mod lsp_log;
|
||||
mod syntax_tree_view;
|
||||
|
||||
@@ -12,4 +13,5 @@ pub use syntax_tree_view::{SyntaxTreeToolbarItemView, SyntaxTreeView};
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
lsp_log::init(cx);
|
||||
syntax_tree_view::init(cx);
|
||||
key_context_view::init(cx);
|
||||
}
|
||||
|
||||
@@ -3083,58 +3083,6 @@ impl MultiBufferSnapshot {
|
||||
summaries
|
||||
}
|
||||
|
||||
pub fn dimensions_from_points<'a, D>(
|
||||
&'a self,
|
||||
points: impl 'a + IntoIterator<Item = Point>,
|
||||
) -> impl 'a + Iterator<Item = D>
|
||||
where
|
||||
D: TextDimension,
|
||||
{
|
||||
let mut cursor = self.excerpts.cursor::<TextSummary>(&());
|
||||
let mut memoized_source_start: Option<Point> = None;
|
||||
let mut points = points.into_iter();
|
||||
std::iter::from_fn(move || {
|
||||
let point = points.next()?;
|
||||
|
||||
// Clear the memoized source start if the point is in a different excerpt than previous.
|
||||
if memoized_source_start.map_or(false, |_| point >= cursor.end(&()).lines) {
|
||||
memoized_source_start = None;
|
||||
}
|
||||
|
||||
// Now determine where the excerpt containing the point starts in its source buffer.
|
||||
// We'll use this value to calculate overshoot next.
|
||||
let source_start = if let Some(source_start) = memoized_source_start {
|
||||
source_start
|
||||
} else {
|
||||
cursor.seek_forward(&point, Bias::Right, &());
|
||||
if let Some(excerpt) = cursor.item() {
|
||||
let source_start = excerpt.range.context.start.to_point(&excerpt.buffer);
|
||||
memoized_source_start = Some(source_start);
|
||||
source_start
|
||||
} else {
|
||||
return Some(D::from_text_summary(cursor.start()));
|
||||
}
|
||||
};
|
||||
|
||||
// First, assume the output dimension is at least the start of the excerpt containing the point
|
||||
let mut output = D::from_text_summary(cursor.start());
|
||||
|
||||
// If the point lands within its excerpt, calculate and add the overshoot in dimension D.
|
||||
if let Some(excerpt) = cursor.item() {
|
||||
let overshoot = point - cursor.start().lines;
|
||||
if !overshoot.is_zero() {
|
||||
let end_in_excerpt = source_start + overshoot;
|
||||
output.add_assign(
|
||||
&excerpt
|
||||
.buffer
|
||||
.text_summary_for_range::<D, _>(source_start..end_in_excerpt),
|
||||
);
|
||||
}
|
||||
}
|
||||
Some(output)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn refresh_anchors<'a, I>(&'a self, anchors: I) -> Vec<(usize, Anchor, bool)>
|
||||
where
|
||||
I: 'a + IntoIterator<Item = &'a Anchor>,
|
||||
@@ -4758,12 +4706,6 @@ impl<'a> sum_tree::SeekTarget<'a, ExcerptSummary, ExcerptSummary> for usize {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> sum_tree::SeekTarget<'a, ExcerptSummary, TextSummary> for Point {
|
||||
fn cmp(&self, cursor_location: &TextSummary, _: &()) -> cmp::Ordering {
|
||||
Ord::cmp(self, &cursor_location.lines)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> sum_tree::SeekTarget<'a, ExcerptSummary, Option<&'a Locator>> for Locator {
|
||||
fn cmp(&self, cursor_location: &Option<&'a Locator>, _: &()) -> cmp::Ordering {
|
||||
Ord::cmp(&Some(self), cursor_location)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,4 @@
|
||||
use editor::ShowScrollbar;
|
||||
use gpui::Pixels;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -29,6 +30,23 @@ pub struct OutlinePanelSettings {
|
||||
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 ScrollbarSettings {
|
||||
/// When to show the scrollbar in the project panel.
|
||||
///
|
||||
/// Default: inherits editor scrollbar settings
|
||||
pub show: Option<ShowScrollbar>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
pub struct ScrollbarSettingsContent {
|
||||
/// When to show the scrollbar in the project panel.
|
||||
///
|
||||
/// Default: inherits editor scrollbar settings
|
||||
pub show: Option<Option<ShowScrollbar>>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
@@ -85,6 +103,8 @@ pub struct OutlinePanelSettingsContent {
|
||||
pub auto_fold_dirs: Option<bool>,
|
||||
/// Settings related to indent guides in the outline panel.
|
||||
pub indent_guides: Option<IndentGuidesSettingsContent>,
|
||||
/// Scrollbar-related settings
|
||||
pub scrollbar: Option<ScrollbarSettingsContent>,
|
||||
}
|
||||
|
||||
impl Settings for OutlinePanelSettings {
|
||||
|
||||
@@ -5270,6 +5270,10 @@ impl LspStore {
|
||||
self.last_formatting_failure.as_deref()
|
||||
}
|
||||
|
||||
pub fn reset_last_formatting_failure(&mut self) {
|
||||
self.last_formatting_failure = None;
|
||||
}
|
||||
|
||||
pub fn environment_for_buffer(
|
||||
&self,
|
||||
buffer: &Model<Buffer>,
|
||||
|
||||
@@ -2414,6 +2414,11 @@ impl Project {
|
||||
self.lsp_store.read(cx).last_formatting_failure()
|
||||
}
|
||||
|
||||
pub fn reset_last_formatting_failure(&self, cx: &mut AppContext) {
|
||||
self.lsp_store
|
||||
.update(cx, |store, _| store.reset_last_formatting_failure());
|
||||
}
|
||||
|
||||
pub fn update_diagnostics(
|
||||
&mut self,
|
||||
language_server_id: LanguageServerId,
|
||||
|
||||
@@ -257,7 +257,6 @@ message Envelope {
|
||||
FindSearchCandidatesResponse find_search_candidates_response = 244;
|
||||
|
||||
CloseBuffer close_buffer = 245;
|
||||
UpdateUserSettings update_user_settings = 246;
|
||||
|
||||
ShutdownRemoteServer shutdown_remote_server = 257;
|
||||
|
||||
@@ -309,6 +308,7 @@ message Envelope {
|
||||
reserved 205 to 206;
|
||||
reserved 221;
|
||||
reserved 224 to 229;
|
||||
reserved 246;
|
||||
reserved 247 to 254;
|
||||
reserved 255 to 256;
|
||||
}
|
||||
@@ -2361,17 +2361,6 @@ message AddWorktreeResponse {
|
||||
string canonicalized_path = 2;
|
||||
}
|
||||
|
||||
message UpdateUserSettings {
|
||||
uint64 project_id = 1;
|
||||
string content = 2;
|
||||
optional Kind kind = 3;
|
||||
|
||||
enum Kind {
|
||||
Settings = 0;
|
||||
Tasks = 1;
|
||||
}
|
||||
}
|
||||
|
||||
message GetPathMetadata {
|
||||
uint64 project_id = 1;
|
||||
string path = 2;
|
||||
|
||||
@@ -342,7 +342,6 @@ messages!(
|
||||
(FindSearchCandidates, Background),
|
||||
(FindSearchCandidatesResponse, Background),
|
||||
(CloseBuffer, Foreground),
|
||||
(UpdateUserSettings, Foreground),
|
||||
(ShutdownRemoteServer, Foreground),
|
||||
(RemoveWorktree, Foreground),
|
||||
(LanguageServerLog, Foreground),
|
||||
@@ -559,7 +558,6 @@ entity_messages!(
|
||||
UpdateContext,
|
||||
SynchronizeContexts,
|
||||
LspExtSwitchSourceHeader,
|
||||
UpdateUserSettings,
|
||||
LanguageServerLog,
|
||||
Toast,
|
||||
HideToast,
|
||||
|
||||
@@ -13,8 +13,7 @@ use gpui::{AppContext, Model};
|
||||
|
||||
use language::CursorShape;
|
||||
use markdown::{Markdown, MarkdownStyle};
|
||||
use release_channel::{AppVersion, ReleaseChannel};
|
||||
use remote::ssh_session::{ServerBinary, ServerVersion};
|
||||
use release_channel::ReleaseChannel;
|
||||
use remote::{SshConnectionOptions, SshPlatform, SshRemoteClient};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -441,23 +440,66 @@ impl remote::SshClientDelegate for SshClientDelegate {
|
||||
self.update_status(status, cx)
|
||||
}
|
||||
|
||||
fn get_server_binary(
|
||||
fn download_server_binary_locally(
|
||||
&self,
|
||||
platform: SshPlatform,
|
||||
upload_binary_over_ssh: bool,
|
||||
release_channel: ReleaseChannel,
|
||||
version: Option<SemanticVersion>,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> oneshot::Receiver<Result<(ServerBinary, ServerVersion)>> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
let this = self.clone();
|
||||
) -> Task<anyhow::Result<PathBuf>> {
|
||||
cx.spawn(|mut cx| async move {
|
||||
tx.send(
|
||||
this.get_server_binary_impl(platform, upload_binary_over_ssh, &mut cx)
|
||||
.await,
|
||||
let binary_path = AutoUpdater::download_remote_server_release(
|
||||
platform.os,
|
||||
platform.arch,
|
||||
release_channel,
|
||||
version,
|
||||
&mut cx,
|
||||
)
|
||||
.ok();
|
||||
.await
|
||||
.map_err(|e| {
|
||||
anyhow!(
|
||||
"Failed to download remote server binary (version: {}, os: {}, arch: {}): {}",
|
||||
version
|
||||
.map(|v| format!("{}", v))
|
||||
.unwrap_or("unknown".to_string()),
|
||||
platform.os,
|
||||
platform.arch,
|
||||
e
|
||||
)
|
||||
})?;
|
||||
Ok(binary_path)
|
||||
})
|
||||
.detach();
|
||||
rx
|
||||
}
|
||||
|
||||
fn get_download_params(
|
||||
&self,
|
||||
platform: SshPlatform,
|
||||
release_channel: ReleaseChannel,
|
||||
version: Option<SemanticVersion>,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Task<Result<(String, String)>> {
|
||||
cx.spawn(|mut cx| async move {
|
||||
let (release, request_body) = AutoUpdater::get_remote_server_release_url(
|
||||
platform.os,
|
||||
platform.arch,
|
||||
release_channel,
|
||||
version,
|
||||
&mut cx,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
anyhow!(
|
||||
"Failed to get remote server binary download url (version: {}, os: {}, arch: {}): {}",
|
||||
version.map(|v| format!("{}", v)).unwrap_or("unknown".to_string()),
|
||||
platform.os,
|
||||
platform.arch,
|
||||
e
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok((release.url, request_body))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fn remote_server_binary_path(
|
||||
@@ -485,208 +527,6 @@ impl SshClientDelegate {
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
async fn get_server_binary_impl(
|
||||
&self,
|
||||
platform: SshPlatform,
|
||||
upload_binary_via_ssh: bool,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<(ServerBinary, ServerVersion)> {
|
||||
let (version, release_channel) = cx.update(|cx| {
|
||||
let version = AppVersion::global(cx);
|
||||
let channel = ReleaseChannel::global(cx);
|
||||
|
||||
(version, channel)
|
||||
})?;
|
||||
|
||||
// In dev mode, build the remote server binary from source
|
||||
#[cfg(debug_assertions)]
|
||||
if release_channel == ReleaseChannel::Dev {
|
||||
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),
|
||||
ServerVersion::Semantic(version),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// For nightly channel, always get latest
|
||||
let current_version = if release_channel == ReleaseChannel::Nightly {
|
||||
None
|
||||
} else {
|
||||
Some(version)
|
||||
};
|
||||
|
||||
self.update_status(
|
||||
Some(&format!("Checking remote server release {}", version)),
|
||||
cx,
|
||||
);
|
||||
|
||||
if upload_binary_via_ssh {
|
||||
let binary_path = AutoUpdater::download_remote_server_release(
|
||||
platform.os,
|
||||
platform.arch,
|
||||
release_channel,
|
||||
current_version,
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
anyhow!(
|
||||
"Failed to download remote server binary (version: {}, os: {}, arch: {}): {}",
|
||||
version,
|
||||
platform.os,
|
||||
platform.arch,
|
||||
e
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok((
|
||||
ServerBinary::LocalBinary(binary_path),
|
||||
ServerVersion::Semantic(version),
|
||||
))
|
||||
} else {
|
||||
let (release, request_body) = AutoUpdater::get_remote_server_release_url(
|
||||
platform.os,
|
||||
platform.arch,
|
||||
release_channel,
|
||||
current_version,
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
anyhow!(
|
||||
"Failed to get remote server binary download url (version: {}, os: {}, arch: {}): {}",
|
||||
version,
|
||||
platform.os,
|
||||
platform.arch,
|
||||
e
|
||||
)
|
||||
})?;
|
||||
|
||||
let version = release
|
||||
.version
|
||||
.parse::<SemanticVersion>()
|
||||
.map(ServerVersion::Semantic)
|
||||
.unwrap_or_else(|_| ServerVersion::Commit(release.version));
|
||||
Ok((
|
||||
ServerBinary::ReleaseUrl {
|
||||
url: release.url,
|
||||
body: request_body,
|
||||
},
|
||||
version,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
async fn build_local(
|
||||
&self,
|
||||
cx: &mut AsyncAppContext,
|
||||
platform: SshPlatform,
|
||||
version: gpui::SemanticVersion,
|
||||
) -> Result<Option<(PathBuf, gpui::SemanticVersion)>> {
|
||||
use smol::process::{Command, Stdio};
|
||||
|
||||
async fn run_cmd(command: &mut Command) -> Result<()> {
|
||||
let output = command
|
||||
.kill_on_drop(true)
|
||||
.stderr(Stdio::inherit())
|
||||
.output()
|
||||
.await?;
|
||||
if !output.status.success() {
|
||||
Err(anyhow!("Failed to run command: {:?}", command))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
if platform.arch == std::env::consts::ARCH && platform.os == std::env::consts::OS {
|
||||
self.update_status(Some("Building remote server binary from source"), cx);
|
||||
log::info!("building remote server binary from source");
|
||||
run_cmd(Command::new("cargo").args([
|
||||
"build",
|
||||
"--package",
|
||||
"remote_server",
|
||||
"--features",
|
||||
"debug-embed",
|
||||
"--target-dir",
|
||||
"target/remote_server",
|
||||
]))
|
||||
.await?;
|
||||
|
||||
self.update_status(Some("Compressing binary"), cx);
|
||||
|
||||
run_cmd(Command::new("gzip").args([
|
||||
"-9",
|
||||
"-f",
|
||||
"target/remote_server/debug/remote_server",
|
||||
]))
|
||||
.await?;
|
||||
|
||||
let path = std::env::current_dir()?.join("target/remote_server/debug/remote_server.gz");
|
||||
return Ok(Some((path, version)));
|
||||
} else if let Some(triple) = platform.triple() {
|
||||
smol::fs::create_dir_all("target/remote_server").await?;
|
||||
|
||||
self.update_status(Some("Installing cross.rs for cross-compilation"), cx);
|
||||
log::info!("installing cross");
|
||||
run_cmd(Command::new("cargo").args([
|
||||
"install",
|
||||
"cross",
|
||||
"--git",
|
||||
"https://github.com/cross-rs/cross",
|
||||
]))
|
||||
.await?;
|
||||
|
||||
self.update_status(
|
||||
Some(&format!(
|
||||
"Building remote server binary from source for {} with Docker",
|
||||
&triple
|
||||
)),
|
||||
cx,
|
||||
);
|
||||
log::info!("building remote server binary from source for {}", &triple);
|
||||
run_cmd(
|
||||
Command::new("cross")
|
||||
.args([
|
||||
"build",
|
||||
"--package",
|
||||
"remote_server",
|
||||
"--features",
|
||||
"debug-embed",
|
||||
"--target-dir",
|
||||
"target/remote_server",
|
||||
"--target",
|
||||
&triple,
|
||||
])
|
||||
.env(
|
||||
"CROSS_CONTAINER_OPTS",
|
||||
"--mount type=bind,src=./target,dst=/app/target",
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
|
||||
self.update_status(Some("Compressing binary"), cx);
|
||||
|
||||
run_cmd(Command::new("gzip").args([
|
||||
"-9",
|
||||
"-f",
|
||||
&format!("target/remote_server/{}/debug/remote_server", triple),
|
||||
]))
|
||||
.await?;
|
||||
|
||||
let path = std::env::current_dir()?.join(format!(
|
||||
"target/remote_server/{}/debug/remote_server.gz",
|
||||
triple
|
||||
));
|
||||
|
||||
return Ok(Some((path, version)));
|
||||
} else {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_connecting_over_ssh(workspace: &Workspace, cx: &AppContext) -> bool {
|
||||
|
||||
@@ -35,6 +35,7 @@ smol.workspace = true
|
||||
tempfile.workspace = true
|
||||
thiserror.workspace = true
|
||||
util.workspace = true
|
||||
release_channel.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -21,6 +21,7 @@ use gpui::{
|
||||
ModelContext, SemanticVersion, Task, WeakModel,
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
use release_channel::{AppCommitSha, AppVersion, ReleaseChannel};
|
||||
use rpc::{
|
||||
proto::{self, build_typed_envelope, Envelope, EnvelopedMessage, PeerId, RequestMessage},
|
||||
AnyProtoClient, EntityMessageSubscriber, ErrorExt, ProtoClient, ProtoMessageHandlerSet,
|
||||
@@ -227,10 +228,19 @@ pub enum ServerBinary {
|
||||
ReleaseUrl { url: String, body: String },
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum ServerVersion {
|
||||
Semantic(SemanticVersion),
|
||||
Commit(String),
|
||||
}
|
||||
impl ServerVersion {
|
||||
pub fn semantic_version(&self) -> Option<SemanticVersion> {
|
||||
match self {
|
||||
Self::Semantic(version) => Some(*version),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ServerVersion {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
@@ -252,12 +262,21 @@ pub trait SshClientDelegate: Send + Sync {
|
||||
platform: SshPlatform,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<PathBuf>;
|
||||
fn get_server_binary(
|
||||
fn get_download_params(
|
||||
&self,
|
||||
platform: SshPlatform,
|
||||
upload_binary_over_ssh: bool,
|
||||
release_channel: ReleaseChannel,
|
||||
version: Option<SemanticVersion>,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> oneshot::Receiver<Result<(ServerBinary, ServerVersion)>>;
|
||||
) -> Task<Result<(String, String)>>;
|
||||
|
||||
fn download_server_binary_locally(
|
||||
&self,
|
||||
platform: SshPlatform,
|
||||
release_channel: ReleaseChannel,
|
||||
version: Option<SemanticVersion>,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Task<Result<PathBuf>>;
|
||||
fn set_status(&self, status: Option<&str>, cx: &mut AsyncAppContext);
|
||||
}
|
||||
|
||||
@@ -1727,86 +1746,123 @@ impl SshRemoteConnection {
|
||||
platform: SshPlatform,
|
||||
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
|
||||
{
|
||||
log::info!("using cached server binary version {}", installed_version);
|
||||
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, new_server_version) = delegate
|
||||
.get_server_binary(platform, upload_binary_over_ssh, cx)
|
||||
.await??;
|
||||
|
||||
if cfg!(not(debug_assertions)) {
|
||||
let installed_version = if let Ok(version_output) =
|
||||
run_cmd(self.socket.ssh_command(dst_path).arg("version")).await
|
||||
{
|
||||
let current_version = match run_cmd(self.socket.ssh_command(dst_path).arg("version")).await
|
||||
{
|
||||
Ok(version_output) => {
|
||||
if let Ok(version) = version_output.trim().parse::<SemanticVersion>() {
|
||||
Some(ServerVersion::Semantic(version))
|
||||
} else {
|
||||
Some(ServerVersion::Commit(version_output.trim().to_string()))
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
Err(_) => None,
|
||||
};
|
||||
let (release_channel, wanted_version) = cx.update(|cx| {
|
||||
let release_channel = ReleaseChannel::global(cx);
|
||||
let wanted_version = match release_channel {
|
||||
ReleaseChannel::Nightly => {
|
||||
AppCommitSha::try_global(cx).map(|sha| ServerVersion::Commit(sha.0))
|
||||
}
|
||||
ReleaseChannel::Dev => None,
|
||||
_ => Some(ServerVersion::Semantic(AppVersion::global(cx))),
|
||||
};
|
||||
(release_channel, wanted_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
|
||||
);
|
||||
}
|
||||
match (¤t_version, &wanted_version) {
|
||||
(Some(current), Some(wanted)) if current == wanted => {
|
||||
log::info!("remote development server present and matching client version");
|
||||
return Ok(());
|
||||
}
|
||||
(Some(ServerVersion::Semantic(current)), Some(ServerVersion::Semantic(wanted)))
|
||||
if current > wanted =>
|
||||
{
|
||||
anyhow::bail!("The version of the remote server ({}) is newer than the Zed version ({}). Please update Zed.", current, wanted);
|
||||
}
|
||||
_ => {
|
||||
log::info!("Installing remote development server");
|
||||
}
|
||||
}
|
||||
|
||||
if self.is_binary_in_use(dst_path).await? {
|
||||
// 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 cfg!(not(debug_assertions)) {
|
||||
anyhow::bail!("The remote server version ({:?}) does not match the wanted version ({:?}), but is in use by another Zed client so cannot be upgraded.", ¤t_version, &wanted_version)
|
||||
} else {
|
||||
log::info!("Binary is currently in use, ignoring because this is a dev build")
|
||||
}
|
||||
}
|
||||
|
||||
if wanted_version.is_none() {
|
||||
if std::env::var("ZED_BUILD_REMOTE_SERVER").is_err() {
|
||||
if let Some(current_version) = current_version {
|
||||
log::warn!(
|
||||
"In development, using cached remote server binary version ({})",
|
||||
current_version
|
||||
);
|
||||
|
||||
return Ok(());
|
||||
} else {
|
||||
anyhow::bail!(
|
||||
"ZED_BUILD_REMOTE_SERVER is not set, but no remote server exists at ({:?})",
|
||||
dst_path
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
let src_path = self.build_local(platform, delegate, cx).await?;
|
||||
|
||||
return self
|
||||
.upload_local_server_binary(&src_path, dst_path, delegate, cx)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
anyhow::bail!("Running development build in release mode, cannot cross compile (unset ZED_BUILD_REMOTE_SERVER)")
|
||||
}
|
||||
|
||||
let upload_binary_over_ssh = self.socket.connection_options.upload_binary_over_ssh;
|
||||
|
||||
if !upload_binary_over_ssh {
|
||||
let (url, body) = delegate
|
||||
.get_download_params(
|
||||
platform,
|
||||
release_channel,
|
||||
wanted_version.clone().and_then(|v| v.semantic_version()),
|
||||
cx,
|
||||
)
|
||||
.await?;
|
||||
|
||||
match self
|
||||
.download_binary_on_server(&url, &body, dst_path, delegate, cx)
|
||||
.await
|
||||
{
|
||||
Ok(_) => return Ok(()),
|
||||
Err(e) => {
|
||||
log::error!(
|
||||
"Failed to download binary on server, attempting to upload server: {}",
|
||||
e
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match binary {
|
||||
ServerBinary::LocalBinary(src_path) => {
|
||||
self.upload_local_server_binary(&src_path, dst_path, delegate, cx)
|
||||
.await
|
||||
}
|
||||
ServerBinary::ReleaseUrl { url, body } => {
|
||||
self.download_binary_on_server(&url, &body, dst_path, delegate, cx)
|
||||
.await
|
||||
}
|
||||
}
|
||||
let src_path = delegate
|
||||
.download_server_binary_locally(
|
||||
platform,
|
||||
release_channel,
|
||||
wanted_version.and_then(|v| v.semantic_version()),
|
||||
cx,
|
||||
)
|
||||
.await?;
|
||||
|
||||
self.upload_local_server_binary(&src_path, dst_path, delegate, cx)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn is_binary_in_use(&self, binary_path: &Path) -> Result<bool> {
|
||||
@@ -1853,26 +1909,25 @@ impl SshRemoteConnection {
|
||||
|
||||
delegate.set_status(Some("Downloading remote development server on host"), cx);
|
||||
|
||||
let body = shlex::try_quote(body).unwrap();
|
||||
let url = shlex::try_quote(url).unwrap();
|
||||
let dst_str = dst_path_gz.to_string_lossy();
|
||||
let dst_escaped = shlex::try_quote(&dst_str).unwrap();
|
||||
|
||||
let script = format!(
|
||||
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_escaped} && 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_escaped} && 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(),
|
||||
"#
|
||||
);
|
||||
|
||||
let output = run_cmd(self.socket.ssh_command("bash").arg("-c").arg(script))
|
||||
let output = run_cmd(self.socket.ssh_command("sh").arg("-c").arg(script))
|
||||
.await
|
||||
.context("Failed to download server binary")?;
|
||||
|
||||
@@ -1974,6 +2029,113 @@ impl SshRemoteConnection {
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
async fn build_local(
|
||||
&self,
|
||||
platform: SshPlatform,
|
||||
delegate: &Arc<dyn SshClientDelegate>,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<PathBuf> {
|
||||
use smol::process::{Command, Stdio};
|
||||
|
||||
async fn run_cmd(command: &mut Command) -> Result<()> {
|
||||
let output = command
|
||||
.kill_on_drop(true)
|
||||
.stderr(Stdio::inherit())
|
||||
.output()
|
||||
.await?;
|
||||
if !output.status.success() {
|
||||
Err(anyhow!("Failed to run command: {:?}", command))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
if platform.arch == std::env::consts::ARCH && platform.os == std::env::consts::OS {
|
||||
delegate.set_status(Some("Building remote server binary from source"), cx);
|
||||
log::info!("building remote server binary from source");
|
||||
run_cmd(Command::new("cargo").args([
|
||||
"build",
|
||||
"--package",
|
||||
"remote_server",
|
||||
"--features",
|
||||
"debug-embed",
|
||||
"--target-dir",
|
||||
"target/remote_server",
|
||||
]))
|
||||
.await?;
|
||||
|
||||
delegate.set_status(Some("Compressing binary"), cx);
|
||||
|
||||
run_cmd(Command::new("gzip").args([
|
||||
"-9",
|
||||
"-f",
|
||||
"target/remote_server/debug/remote_server",
|
||||
]))
|
||||
.await?;
|
||||
|
||||
let path = std::env::current_dir()?.join("target/remote_server/debug/remote_server.gz");
|
||||
return Ok(path);
|
||||
}
|
||||
let Some(triple) = platform.triple() else {
|
||||
anyhow::bail!("can't cross compile for: {:?}", platform);
|
||||
};
|
||||
smol::fs::create_dir_all("target/remote_server").await?;
|
||||
|
||||
delegate.set_status(Some("Installing cross.rs for cross-compilation"), cx);
|
||||
log::info!("installing cross");
|
||||
run_cmd(Command::new("cargo").args([
|
||||
"install",
|
||||
"cross",
|
||||
"--git",
|
||||
"https://github.com/cross-rs/cross",
|
||||
]))
|
||||
.await?;
|
||||
|
||||
delegate.set_status(
|
||||
Some(&format!(
|
||||
"Building remote server binary from source for {} with Docker",
|
||||
&triple
|
||||
)),
|
||||
cx,
|
||||
);
|
||||
log::info!("building remote server binary from source for {}", &triple);
|
||||
run_cmd(
|
||||
Command::new("cross")
|
||||
.args([
|
||||
"build",
|
||||
"--package",
|
||||
"remote_server",
|
||||
"--features",
|
||||
"debug-embed",
|
||||
"--target-dir",
|
||||
"target/remote_server",
|
||||
"--target",
|
||||
&triple,
|
||||
])
|
||||
.env(
|
||||
"CROSS_CONTAINER_OPTS",
|
||||
"--mount type=bind,src=./target,dst=/app/target",
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
|
||||
delegate.set_status(Some("Compressing binary"), cx);
|
||||
|
||||
run_cmd(Command::new("gzip").args([
|
||||
"-9",
|
||||
"-f",
|
||||
&format!("target/remote_server/{}/debug/remote_server", triple),
|
||||
]))
|
||||
.await?;
|
||||
|
||||
let path = std::env::current_dir()?.join(format!(
|
||||
"target/remote_server/{}/debug/remote_server.gz",
|
||||
triple
|
||||
));
|
||||
|
||||
return Ok(path);
|
||||
}
|
||||
}
|
||||
|
||||
type ResponseChannels = Mutex<HashMap<MessageId, oneshot::Sender<(Envelope, oneshot::Sender<()>)>>>;
|
||||
@@ -2295,12 +2457,12 @@ mod fake {
|
||||
},
|
||||
select_biased, FutureExt, SinkExt, StreamExt,
|
||||
};
|
||||
use gpui::{AsyncAppContext, Task, TestAppContext};
|
||||
use gpui::{AsyncAppContext, SemanticVersion, Task, TestAppContext};
|
||||
use release_channel::ReleaseChannel;
|
||||
use rpc::proto::Envelope;
|
||||
|
||||
use super::{
|
||||
ChannelClient, RemoteConnection, ServerBinary, ServerVersion, SshClientDelegate,
|
||||
SshConnectionOptions, SshPlatform,
|
||||
ChannelClient, RemoteConnection, SshClientDelegate, SshConnectionOptions, SshPlatform,
|
||||
};
|
||||
|
||||
pub(super) struct FakeRemoteConnection {
|
||||
@@ -2412,23 +2574,36 @@ mod fake {
|
||||
) -> oneshot::Receiver<Result<String>> {
|
||||
unreachable!()
|
||||
}
|
||||
fn remote_server_binary_path(
|
||||
|
||||
fn download_server_binary_locally(
|
||||
&self,
|
||||
_: SshPlatform,
|
||||
_: ReleaseChannel,
|
||||
_: Option<SemanticVersion>,
|
||||
_: &mut AsyncAppContext,
|
||||
) -> Result<PathBuf> {
|
||||
) -> Task<Result<PathBuf>> {
|
||||
unreachable!()
|
||||
}
|
||||
fn get_server_binary(
|
||||
|
||||
fn get_download_params(
|
||||
&self,
|
||||
_: SshPlatform,
|
||||
_: bool,
|
||||
_: &mut AsyncAppContext,
|
||||
) -> oneshot::Receiver<Result<(ServerBinary, ServerVersion)>> {
|
||||
_platform: SshPlatform,
|
||||
_release_channel: ReleaseChannel,
|
||||
_version: Option<SemanticVersion>,
|
||||
_cx: &mut AsyncAppContext,
|
||||
) -> Task<Result<(String, String)>> {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn set_status(&self, _: Option<&str>, _: &mut AsyncAppContext) {}
|
||||
|
||||
fn remote_server_binary_path(
|
||||
&self,
|
||||
_platform: SshPlatform,
|
||||
_cx: &mut AsyncAppContext,
|
||||
) -> Result<PathBuf> {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -66,6 +66,8 @@ pub enum RevealStrategy {
|
||||
/// Always show the terminal pane, add and focus the corresponding task's tab in it.
|
||||
#[default]
|
||||
Always,
|
||||
/// Always show the terminal pane, add the task's tab in it, but don't focus it.
|
||||
NoFocus,
|
||||
/// Do not change terminal pane focus, but still add/reuse the task's tab there.
|
||||
Never,
|
||||
}
|
||||
|
||||
@@ -575,9 +575,9 @@ impl TerminalPanel {
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn activate_terminal_view(&self, item_index: usize, cx: &mut WindowContext) {
|
||||
fn activate_terminal_view(&self, item_index: usize, focus: bool, cx: &mut WindowContext) {
|
||||
self.pane.update(cx, |pane, cx| {
|
||||
pane.activate_item(item_index, true, true, cx)
|
||||
pane.activate_item(item_index, true, focus, cx)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -616,8 +616,14 @@ impl TerminalPanel {
|
||||
pane.add_item(terminal_view, true, focus, None, cx);
|
||||
});
|
||||
|
||||
if reveal_strategy == RevealStrategy::Always {
|
||||
workspace.focus_panel::<Self>(cx);
|
||||
match reveal_strategy {
|
||||
RevealStrategy::Always => {
|
||||
workspace.focus_panel::<Self>(cx);
|
||||
}
|
||||
RevealStrategy::NoFocus => {
|
||||
workspace.open_panel::<Self>(cx);
|
||||
}
|
||||
RevealStrategy::Never => {}
|
||||
}
|
||||
Ok(terminal)
|
||||
})?;
|
||||
@@ -698,7 +704,7 @@ impl TerminalPanel {
|
||||
|
||||
match reveal {
|
||||
RevealStrategy::Always => {
|
||||
self.activate_terminal_view(terminal_item_index, cx);
|
||||
self.activate_terminal_view(terminal_item_index, true, cx);
|
||||
let task_workspace = self.workspace.clone();
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
task_workspace
|
||||
@@ -707,6 +713,16 @@ impl TerminalPanel {
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
RevealStrategy::NoFocus => {
|
||||
self.activate_terminal_view(terminal_item_index, false, cx);
|
||||
let task_workspace = self.workspace.clone();
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
task_workspace
|
||||
.update(&mut cx, |workspace, cx| workspace.open_panel::<Self>(cx))
|
||||
.ok()
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
RevealStrategy::Never => {}
|
||||
}
|
||||
|
||||
|
||||
@@ -284,6 +284,7 @@ pub enum IconName {
|
||||
Update,
|
||||
UserGroup,
|
||||
Visible,
|
||||
Wand,
|
||||
Warning,
|
||||
WholeWord,
|
||||
XCircle,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
description = "The fast, collaborative code editor."
|
||||
edition = "2021"
|
||||
name = "zed"
|
||||
version = "0.160.0"
|
||||
version = "0.161.0"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
authors = ["Zed Team <hi@zed.dev>"]
|
||||
|
||||
@@ -68,7 +68,6 @@ actions!(
|
||||
Hide,
|
||||
HideOthers,
|
||||
Minimize,
|
||||
OpenDefaultKeymap,
|
||||
OpenDefaultSettings,
|
||||
OpenProjectSettings,
|
||||
OpenProjectTasks,
|
||||
@@ -474,7 +473,7 @@ pub fn initialize_workspace(
|
||||
.register_action(open_project_tasks_file)
|
||||
.register_action(
|
||||
move |workspace: &mut Workspace,
|
||||
_: &OpenDefaultKeymap,
|
||||
_: &zed_actions::OpenDefaultKeymap,
|
||||
cx: &mut ViewContext<Workspace>| {
|
||||
open_bundled_file(
|
||||
workspace,
|
||||
|
||||
@@ -18,7 +18,10 @@ pub fn app_menus() -> Vec<Menu> {
|
||||
MenuItem::action("Open Settings", super::OpenSettings),
|
||||
MenuItem::action("Open Key Bindings", zed_actions::OpenKeymap),
|
||||
MenuItem::action("Open Default Settings", super::OpenDefaultSettings),
|
||||
MenuItem::action("Open Default Key Bindings", super::OpenDefaultKeymap),
|
||||
MenuItem::action(
|
||||
"Open Default Key Bindings",
|
||||
zed_actions::OpenDefaultKeymap,
|
||||
),
|
||||
MenuItem::action("Open Project Settings", super::OpenProjectSettings),
|
||||
MenuItem::action("Select Theme...", theme_selector::Toggle::default()),
|
||||
],
|
||||
|
||||
@@ -47,6 +47,9 @@ impl OpenRequest {
|
||||
this.parse_file_path(file)
|
||||
} else if let Some(file) = url.strip_prefix("zed://file") {
|
||||
this.parse_file_path(file)
|
||||
} else if let Some(file) = url.strip_prefix("zed://ssh") {
|
||||
let ssh_url = "ssh:/".to_string() + file;
|
||||
this.parse_ssh_file_path(&ssh_url, cx)?
|
||||
} else if url.starts_with("ssh://") {
|
||||
this.parse_ssh_file_path(&url, cx)?
|
||||
} else if let Some(request_path) = parse_zed_link(&url, cx) {
|
||||
|
||||
@@ -26,6 +26,7 @@ actions!(
|
||||
zed,
|
||||
[
|
||||
OpenSettings,
|
||||
OpenDefaultKeymap,
|
||||
OpenAccountSettings,
|
||||
OpenServerSettings,
|
||||
Quit,
|
||||
|
||||
@@ -137,7 +137,7 @@ Zed has the following internal prompt templates:
|
||||
|
||||
- `content_prompt.hbs`: Used for generating content in the editor.
|
||||
- `terminal_assistant_prompt.hbs`: Used for the terminal assistant feature.
|
||||
- `edit_workflow.hbs`: Used for generating the edit workflow prompt.
|
||||
- `suggest_edits.hbs`: Used for generating the model instructions for the XML Suggest Edits should return.
|
||||
- `step_resolution.hbs`: Used for generating the step resolution prompt.
|
||||
|
||||
At this point it is unknown if we will expand templates further to be user-creatable.
|
||||
@@ -215,7 +215,7 @@ The following templates can be overridden:
|
||||
given system information and latest terminal output if relevant.
|
||||
```
|
||||
|
||||
3. `edit_workflow.hbs`: Used for generating the edit workflow prompt.
|
||||
3. `suggest_edits.hbs`: Used for generating the model instructions for the XML Suggest Edits should return.
|
||||
|
||||
4. `step_resolution.hbs`: Used for generating the step resolution prompt.
|
||||
|
||||
|
||||
@@ -1043,6 +1043,32 @@ To interpret all `.c` files as C++, files called `MyLockFile` as TOML and files
|
||||
}
|
||||
```
|
||||
|
||||
3. Show a commit summary next to the commit date and author:
|
||||
|
||||
```json
|
||||
{
|
||||
"git": {
|
||||
"inline_blame": {
|
||||
"enabled": true,
|
||||
"show_commit_summary": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
4. Use this as the minimum column at which to display inline blame information:
|
||||
|
||||
```json
|
||||
{
|
||||
"git": {
|
||||
"inline_blame": {
|
||||
"enabled": true,
|
||||
"min_column": 80
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Indent Guides
|
||||
|
||||
- Description: Configuration related to indent guides. Indent guides can be configured separately for each language.
|
||||
@@ -2271,6 +2297,9 @@ Run the `theme selector: toggle` action in the command palette to see a current
|
||||
"auto_fold_dirs": true,
|
||||
"indent_guides": {
|
||||
"show": "always"
|
||||
},
|
||||
"scrollbar": {
|
||||
"show": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Dart
|
||||
|
||||
Dart support is available through the [Dart extension](https://github.com/zed-industries/zed/tree/main/extensions/dart).
|
||||
Dart support is available through the [Dart extension](https://github.com/zed-extensions/dart).
|
||||
|
||||
- Tree Sitter: [UserNobody14/tree-sitter-dart](https://github.com/UserNobody14/tree-sitter-dart)
|
||||
- Language Server: [dart language-server](https://github.com/dart-lang/sdk)
|
||||
|
||||
@@ -163,6 +163,23 @@ Here's a snippet for Zed settings.json (the language server will restart automat
|
||||
}
|
||||
```
|
||||
|
||||
### Multi-project workspaces
|
||||
|
||||
If you want rust-analyzer to analyze multiple Rust projects in the same folder that are not listed in `[members]` in the Cargo workspace,
|
||||
you can list them in `linkedProjects` in the local project settings:
|
||||
|
||||
```json
|
||||
{
|
||||
"lsp": {
|
||||
"rust-analyzer": {
|
||||
"initialization_options": {
|
||||
"linkedProjects": ["./path/to/a/Cargo.toml", "./path/to/b/Cargo.toml"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Snippets
|
||||
|
||||
There's a way get custom completion items from rust-analyzer, that will transform the code according to the snippet body:
|
||||
|
||||
@@ -16,14 +16,14 @@ On your local machine, Zed runs its UI, talks to language models, uses Tree-sitt
|
||||
|
||||
## Setup
|
||||
|
||||
1. Download and install the latest [Zed Preview](https://zed.dev/releases/preview). You need at least Zed v0.159.
|
||||
1. Download and install the latest [Zed](https://zed.dev/releases). You need at least Zed v0.159.
|
||||
1. Open the remote projects dialogue with <kbd>cmd-shift-p remote</kbd> or <kbd>cmd-control-o</kbd>.
|
||||
1. Click "Connect New Server" and enter the command you use to SSH into the server. See [Supported SSH options](#supported-ssh-options) for options you can pass.
|
||||
1. Your local machine will attempt to connect to the remote server using the `ssh` binary on your path. Assuming the connection is successful, Zed will download the server on the remote host and start it.
|
||||
1. Once the Zed server is running, you will be prompted to choose a path to open on the remote server.
|
||||
> **Note:** Zed does not currently handle opening very large directories (for example, `/` or `~` that may have >100,000 files) very well. We are working on improving this, but suggest in the meantime opening only specific projects, or subfolders of very large mono-repos.
|
||||
|
||||
For simple cases where you don't need any SSH arguments, you can run `zed ssh://[<user>@]<host>[:<port>]/<path>` to open a remote folder/file directly.
|
||||
For simple cases where you don't need any SSH arguments, you can run `zed ssh://[<user>@]<host>[:<port>]/<path>` to open a remote folder/file directly. If you'd like to hotlink into an SSH project, use a link of the format: `zed://ssh/[<user>@]<host>[:<port>]/<path>`.
|
||||
|
||||
## Supported platforms
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ Zed supports ways to spawn (and rerun) commands using its integrated terminal to
|
||||
"allow_concurrent_runs": false,
|
||||
// What to do with the terminal pane and tab, after the command was started:
|
||||
// * `always` — always show the terminal pane, add and focus the corresponding task's tab in it (default)
|
||||
// * `no_focus` — always show the terminal pane, add/reuse the task's tab there, but don't focus it
|
||||
// * `never` — avoid changing current terminal pane focus, but still add/reuse the task's tab there
|
||||
"reveal": "always",
|
||||
// What to do with the terminal pane and tab, after the command had finished:
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
[package]
|
||||
name = "zed_dart"
|
||||
version = "0.1.2"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
license = "Apache-2.0"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/dart.rs"
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
zed_extension_api = "0.1.0"
|
||||
@@ -1 +0,0 @@
|
||||
../../LICENSE-APACHE
|
||||
@@ -1,6 +0,0 @@
|
||||
## Roadmap
|
||||
|
||||
1. Add `dart run` command.
|
||||
2. Add `dart test` command.
|
||||
3. Add `flutter test --name` command, to allow running a single test or group of tests.
|
||||
4. Auto hot reload Flutter app when files change.
|
||||
@@ -1,16 +0,0 @@
|
||||
id = "dart"
|
||||
name = "Dart"
|
||||
description = "Dart support."
|
||||
version = "0.1.2"
|
||||
schema_version = 1
|
||||
authors = ["Abdullah Alsigar <abdullah.alsigar@gmail.com>", "Flo <flo80@users.noreply.github.com>", "ybbond <hi@ybbond.id>"]
|
||||
repository = "https://github.com/zed-industries/zed"
|
||||
|
||||
[language_servers.dart]
|
||||
name = "Dart LSP"
|
||||
language = "Dart"
|
||||
languages = ["Dart"]
|
||||
|
||||
[grammars.dart]
|
||||
repository = "https://github.com/UserNobody14/tree-sitter-dart"
|
||||
commit = "6da46473ab8accb13da48113f4634e729a71d335"
|
||||
@@ -1,6 +0,0 @@
|
||||
("(" @open ")" @close)
|
||||
("[" @open "]" @close)
|
||||
("{" @open "}" @close)
|
||||
("<" @open ">" @close)
|
||||
("\"" @open "\"" @close)
|
||||
("'" @open "'" @close)
|
||||
@@ -1,15 +0,0 @@
|
||||
name = "Dart"
|
||||
grammar = "dart"
|
||||
path_suffixes = ["dart"]
|
||||
line_comments = ["// ", "/// "]
|
||||
autoclose_before = ";:.,=}])>"
|
||||
brackets = [
|
||||
{ start = "{", end = "}", close = true, newline = true },
|
||||
{ start = "[", end = "]", close = true, newline = true },
|
||||
{ start = "(", end = ")", close = true, newline = true },
|
||||
{ start = "<", end = ">", close = true, newline = false},
|
||||
{ start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] },
|
||||
{ start = "'", end = "'", close = true, newline = false, not_in = ["string"] },
|
||||
{ start = "/*", end = " */", close = true, newline = false, not_in = ["string", "comment"] },
|
||||
{ start = "`", end = "`", close = true, newline = false, not_in = ["string", "comment"] },
|
||||
]
|
||||
@@ -1,269 +0,0 @@
|
||||
(dotted_identifier_list) @string
|
||||
|
||||
; Methods
|
||||
; --------------------
|
||||
(super) @function
|
||||
|
||||
(function_expression_body (identifier) @type)
|
||||
; ((identifier)(selector (argument_part)) @function)
|
||||
|
||||
(((identifier) @function (#match? @function "^_?[a-z]"))
|
||||
. (selector . (argument_part))) @function
|
||||
|
||||
; Annotations
|
||||
; --------------------
|
||||
(annotation
|
||||
name: (identifier) @attribute)
|
||||
|
||||
; Operators and Tokens
|
||||
; --------------------
|
||||
(template_substitution
|
||||
"$" @punctuation.special
|
||||
"{" @punctuation.special
|
||||
"}" @punctuation.special) @none
|
||||
|
||||
(template_substitution
|
||||
"$" @punctuation.special
|
||||
(identifier_dollar_escaped) @variable) @none
|
||||
|
||||
(escape_sequence) @string.escape
|
||||
|
||||
[
|
||||
"@"
|
||||
"=>"
|
||||
".."
|
||||
"??"
|
||||
"=="
|
||||
"?"
|
||||
":"
|
||||
"&&"
|
||||
"%"
|
||||
"<"
|
||||
">"
|
||||
"="
|
||||
">="
|
||||
"<="
|
||||
"||"
|
||||
(multiplicative_operator)
|
||||
(increment_operator)
|
||||
(is_operator)
|
||||
(prefix_operator)
|
||||
(equality_operator)
|
||||
(additive_operator)
|
||||
] @operator
|
||||
|
||||
[
|
||||
"("
|
||||
")"
|
||||
"["
|
||||
"]"
|
||||
"{"
|
||||
"}"
|
||||
] @punctuation.bracket
|
||||
|
||||
; Delimiters
|
||||
; --------------------
|
||||
[
|
||||
";"
|
||||
"."
|
||||
","
|
||||
] @punctuation.delimiter
|
||||
|
||||
; Types
|
||||
; --------------------
|
||||
(class_definition
|
||||
name: (identifier) @type)
|
||||
(constructor_signature
|
||||
name: (identifier) @type)
|
||||
(scoped_identifier
|
||||
scope: (identifier) @type)
|
||||
(function_signature
|
||||
name: (identifier) @function.method)
|
||||
|
||||
(getter_signature
|
||||
(identifier) @function.method)
|
||||
|
||||
(setter_signature
|
||||
name: (identifier) @function.method)
|
||||
(enum_declaration
|
||||
name: (identifier) @type)
|
||||
(enum_constant
|
||||
name: (identifier) @type)
|
||||
(void_type) @type
|
||||
|
||||
((scoped_identifier
|
||||
scope: (identifier) @type
|
||||
name: (identifier) @type)
|
||||
(#match? @type "^[a-zA-Z]"))
|
||||
|
||||
(type_identifier) @type
|
||||
|
||||
(type_alias
|
||||
(type_identifier) @type.definition)
|
||||
|
||||
; Variables
|
||||
; --------------------
|
||||
; var keyword
|
||||
(inferred_type) @keyword
|
||||
|
||||
((identifier) @type
|
||||
(#match? @type "^_?[A-Z].*[a-z]"))
|
||||
|
||||
("Function" @type)
|
||||
|
||||
; properties
|
||||
(unconditional_assignable_selector
|
||||
(identifier) @property)
|
||||
|
||||
(conditional_assignable_selector
|
||||
(identifier) @property)
|
||||
|
||||
; assignments
|
||||
(assignment_expression
|
||||
left: (assignable_expression) @variable)
|
||||
|
||||
(this) @variable.builtin
|
||||
|
||||
; Parameters
|
||||
; --------------------
|
||||
(formal_parameter
|
||||
name: (identifier) @variable.parameter)
|
||||
|
||||
(named_argument
|
||||
(label
|
||||
(identifier) @variable.parameter))
|
||||
|
||||
; Literals
|
||||
; --------------------
|
||||
[
|
||||
(hex_integer_literal)
|
||||
(decimal_integer_literal)
|
||||
(decimal_floating_point_literal)
|
||||
; TODO: inaccessible nodes
|
||||
; (octal_integer_literal)
|
||||
; (hex_floating_point_literal)
|
||||
] @number
|
||||
|
||||
(symbol_literal) @string.special.symbol
|
||||
|
||||
(string_literal) @string
|
||||
(true) @boolean
|
||||
(false) @boolean
|
||||
(null_literal) @constant.builtin
|
||||
|
||||
(comment) @comment
|
||||
|
||||
(documentation_comment) @comment.documentation
|
||||
|
||||
; Keywords
|
||||
; --------------------
|
||||
[
|
||||
"import"
|
||||
"library"
|
||||
"export"
|
||||
"as"
|
||||
"show"
|
||||
"hide"
|
||||
] @keyword.import
|
||||
|
||||
; Reserved words (cannot be used as identifiers)
|
||||
[
|
||||
(case_builtin)
|
||||
"late"
|
||||
"required"
|
||||
"extension"
|
||||
"on"
|
||||
"class"
|
||||
"enum"
|
||||
"extends"
|
||||
"in"
|
||||
"is"
|
||||
"new"
|
||||
"super"
|
||||
"with"
|
||||
] @keyword
|
||||
|
||||
"return" @keyword.return
|
||||
|
||||
; Built in identifiers:
|
||||
; alone these are marked as keywords
|
||||
[
|
||||
"deferred"
|
||||
"factory"
|
||||
"get"
|
||||
"implements"
|
||||
"interface"
|
||||
"library"
|
||||
"operator"
|
||||
"mixin"
|
||||
"part"
|
||||
"set"
|
||||
"typedef"
|
||||
] @keyword
|
||||
|
||||
[
|
||||
"async"
|
||||
"async*"
|
||||
"sync*"
|
||||
"await"
|
||||
"yield"
|
||||
] @keyword.coroutine
|
||||
|
||||
[
|
||||
(const_builtin)
|
||||
(final_builtin)
|
||||
"abstract"
|
||||
"covariant"
|
||||
"dynamic"
|
||||
"external"
|
||||
"static"
|
||||
"final"
|
||||
"base"
|
||||
"sealed"
|
||||
] @type.qualifier
|
||||
|
||||
; when used as an identifier:
|
||||
((identifier) @variable.builtin
|
||||
(#any-of? @variable.builtin
|
||||
"abstract"
|
||||
"as"
|
||||
"covariant"
|
||||
"deferred"
|
||||
"dynamic"
|
||||
"export"
|
||||
"external"
|
||||
"factory"
|
||||
"Function"
|
||||
"get"
|
||||
"implements"
|
||||
"import"
|
||||
"interface"
|
||||
"library"
|
||||
"operator"
|
||||
"mixin"
|
||||
"part"
|
||||
"set"
|
||||
"static"
|
||||
"typedef"))
|
||||
|
||||
[
|
||||
"if"
|
||||
"else"
|
||||
"switch"
|
||||
"default"
|
||||
] @keyword.conditional
|
||||
|
||||
[
|
||||
"try"
|
||||
"throw"
|
||||
"catch"
|
||||
"finally"
|
||||
(break_statement)
|
||||
] @keyword.exception
|
||||
|
||||
[
|
||||
"do"
|
||||
"while"
|
||||
"continue"
|
||||
"for"
|
||||
] @keyword.repeat
|
||||
@@ -1,3 +0,0 @@
|
||||
(_ "[" "]" @end) @indent
|
||||
(_ "{" "}" @end) @indent
|
||||
(_ "(" ")" @end) @indent
|
||||
@@ -1,18 +0,0 @@
|
||||
(class_definition
|
||||
"class" @context
|
||||
name: (_) @name) @item
|
||||
|
||||
(function_signature
|
||||
name: (_) @name) @item
|
||||
|
||||
(getter_signature
|
||||
"get" @context
|
||||
name: (_) @name) @item
|
||||
|
||||
(setter_signature
|
||||
"set" @context
|
||||
name: (_) @name) @item
|
||||
|
||||
(enum_declaration
|
||||
"enum" @context
|
||||
name: (_) @name) @item
|
||||
@@ -1,45 +0,0 @@
|
||||
; Flutter main
|
||||
(
|
||||
(
|
||||
(import_or_export
|
||||
(library_import
|
||||
(import_specification
|
||||
("import"
|
||||
(configurable_uri
|
||||
(uri
|
||||
(string_literal) @_import
|
||||
(#match? @_import "package:flutter/(material|widgets|cupertino).dart")
|
||||
(#not-match? @_import "package:flutter_test/flutter_test.dart")
|
||||
(#not-match? @_import "package:test/test.dart")
|
||||
))))))
|
||||
(
|
||||
(function_signature
|
||||
name: (_) @run
|
||||
)
|
||||
(#eq? @run "main")
|
||||
)
|
||||
(#set! tag flutter-main)
|
||||
)
|
||||
)
|
||||
|
||||
; Flutter test main
|
||||
(
|
||||
(
|
||||
(import_or_export
|
||||
(library_import
|
||||
(import_specification
|
||||
("import"
|
||||
(configurable_uri
|
||||
(uri
|
||||
(string_literal) @_import
|
||||
(#match? @_import "package:flutter_test/flutter_test.dart")
|
||||
))))))
|
||||
(
|
||||
(function_signature
|
||||
name: (_) @run
|
||||
)
|
||||
(#eq? @run "main")
|
||||
)
|
||||
(#set! tag flutter-test-main)
|
||||
)
|
||||
)
|
||||
@@ -1,26 +0,0 @@
|
||||
[
|
||||
{
|
||||
"label": "flutter run",
|
||||
"command": "flutter",
|
||||
"args": ["run"],
|
||||
"tags": ["flutter-main"]
|
||||
},
|
||||
{
|
||||
"label": "fvm flutter run",
|
||||
"command": "fvm flutter",
|
||||
"args": ["run"],
|
||||
"tags": ["flutter-main"]
|
||||
},
|
||||
{
|
||||
"label": "flutter test $ZED_STEM",
|
||||
"command": "flutter",
|
||||
"args": ["test", "$ZED_FILE"],
|
||||
"tags": ["flutter-test-main"]
|
||||
},
|
||||
{
|
||||
"label": "fvm flutter test $ZED_STEM",
|
||||
"command": "fvm flutter",
|
||||
"args": ["test", "$ZED_FILE"],
|
||||
"tags": ["flutter-test-main"]
|
||||
}
|
||||
]
|
||||
@@ -1,162 +0,0 @@
|
||||
use zed::lsp::CompletionKind;
|
||||
use zed::settings::LspSettings;
|
||||
use zed::{CodeLabel, CodeLabelSpan};
|
||||
use zed_extension_api::{self as zed, serde_json, Result};
|
||||
|
||||
struct DartBinary {
|
||||
pub path: String,
|
||||
pub args: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
struct DartExtension;
|
||||
|
||||
impl DartExtension {
|
||||
fn language_server_binary(
|
||||
&mut self,
|
||||
_language_server_id: &zed::LanguageServerId,
|
||||
worktree: &zed::Worktree,
|
||||
) -> Result<DartBinary> {
|
||||
let binary_settings = LspSettings::for_worktree("dart", worktree)
|
||||
.ok()
|
||||
.and_then(|lsp_settings| lsp_settings.binary);
|
||||
let binary_args = binary_settings
|
||||
.as_ref()
|
||||
.and_then(|binary_settings| binary_settings.arguments.clone());
|
||||
|
||||
if let Some(path) = binary_settings.and_then(|binary_settings| binary_settings.path) {
|
||||
return Ok(DartBinary {
|
||||
path,
|
||||
args: binary_args,
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(path) = worktree.which("dart") {
|
||||
return Ok(DartBinary {
|
||||
path,
|
||||
args: binary_args,
|
||||
});
|
||||
}
|
||||
|
||||
Err(
|
||||
"dart must be installed from dart.dev/get-dart or pointed to by the LSP binary settings"
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl zed::Extension for DartExtension {
|
||||
fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
fn language_server_command(
|
||||
&mut self,
|
||||
language_server_id: &zed::LanguageServerId,
|
||||
worktree: &zed::Worktree,
|
||||
) -> Result<zed::Command> {
|
||||
let dart_binary = self.language_server_binary(language_server_id, worktree)?;
|
||||
|
||||
Ok(zed::Command {
|
||||
command: dart_binary.path,
|
||||
args: dart_binary.args.unwrap_or_else(|| {
|
||||
vec!["language-server".to_string(), "--protocol=lsp".to_string()]
|
||||
}),
|
||||
env: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
fn language_server_workspace_configuration(
|
||||
&mut self,
|
||||
_language_server_id: &zed::LanguageServerId,
|
||||
worktree: &zed::Worktree,
|
||||
) -> Result<Option<serde_json::Value>> {
|
||||
let settings = LspSettings::for_worktree("dart", worktree)
|
||||
.ok()
|
||||
.and_then(|lsp_settings| lsp_settings.settings.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok(Some(serde_json::json!({
|
||||
"dart": settings
|
||||
})))
|
||||
}
|
||||
|
||||
fn label_for_completion(
|
||||
&self,
|
||||
_language_server_id: &zed::LanguageServerId,
|
||||
completion: zed::lsp::Completion,
|
||||
) -> Option<CodeLabel> {
|
||||
let arrow = " → ";
|
||||
|
||||
match completion.kind? {
|
||||
CompletionKind::Class => Some(CodeLabel {
|
||||
filter_range: (0..completion.label.len()).into(),
|
||||
spans: vec![CodeLabelSpan::literal(
|
||||
completion.label,
|
||||
Some("type".into()),
|
||||
)],
|
||||
code: String::new(),
|
||||
}),
|
||||
CompletionKind::Function | CompletionKind::Constructor | CompletionKind::Method => {
|
||||
let mut parts = completion.detail.as_ref()?.split(arrow);
|
||||
let (name, _) = completion.label.split_once('(')?;
|
||||
let parameter_list = parts.next()?;
|
||||
let return_type = parts.next()?;
|
||||
let fn_name = " a";
|
||||
let fat_arrow = " => ";
|
||||
let call_expr = "();";
|
||||
|
||||
let code =
|
||||
format!("{return_type}{fn_name}{parameter_list}{fat_arrow}{name}{call_expr}");
|
||||
|
||||
let parameter_list_start = return_type.len() + fn_name.len();
|
||||
|
||||
Some(CodeLabel {
|
||||
spans: vec![
|
||||
CodeLabelSpan::code_range(
|
||||
code.len() - call_expr.len() - name.len()..code.len() - call_expr.len(),
|
||||
),
|
||||
CodeLabelSpan::code_range(
|
||||
parameter_list_start..parameter_list_start + parameter_list.len(),
|
||||
),
|
||||
CodeLabelSpan::literal(arrow, None),
|
||||
CodeLabelSpan::code_range(0..return_type.len()),
|
||||
],
|
||||
filter_range: (0..name.len()).into(),
|
||||
code,
|
||||
})
|
||||
}
|
||||
CompletionKind::Property => {
|
||||
let class_start = "class A {";
|
||||
let get = " get ";
|
||||
let property_end = " => a; }";
|
||||
let ty = completion.detail?;
|
||||
let name = completion.label;
|
||||
|
||||
let code = format!("{class_start}{ty}{get}{name}{property_end}");
|
||||
let name_start = class_start.len() + ty.len() + get.len();
|
||||
|
||||
Some(CodeLabel {
|
||||
spans: vec![
|
||||
CodeLabelSpan::code_range(name_start..name_start + name.len()),
|
||||
CodeLabelSpan::literal(arrow, None),
|
||||
CodeLabelSpan::code_range(class_start.len()..class_start.len() + ty.len()),
|
||||
],
|
||||
filter_range: (0..name.len()).into(),
|
||||
code,
|
||||
})
|
||||
}
|
||||
CompletionKind::Variable => {
|
||||
let name = completion.label;
|
||||
|
||||
Some(CodeLabel {
|
||||
filter_range: (0..name.len()).into(),
|
||||
spans: vec![CodeLabelSpan::literal(name, Some("variable".into()))],
|
||||
code: String::new(),
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
zed::register_extension!(DartExtension);
|
||||
Reference in New Issue
Block a user