From 07e808d16f59e46c45d8f7c84e44974457e6a41d Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Fri, 4 Oct 2024 12:07:43 -0400 Subject: [PATCH 01/28] Document File Scan Exclusions (#18738) Release Notes: - N/A --- docs/src/configuring-zed.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index 230255597e..6740d51364 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -844,6 +844,27 @@ If the setting is set to `true`: The result is still `)))` and not `))))))`, which is what it would be by default. +## File Scan Exclusions + +- Setting: `file_scan_exclusions` +- Description: Configure how Add filename or directory globs that will be excluded by Zed entirely. They will be skipped during file scans, file searches and hidden from project file tree. +- Default: + +```json +"file_scan_exclusions": [ + "**/.git", + "**/.svn", + "**/.hg", + "**/CVS", + "**/.DS_Store", + "**/Thumbs.db", + "**/.classpath", + "**/.settings" +], +``` + +Note, specifying `file_scan_exclusions` in settings.json will override the defaults (shown above). If you are looking to exclude additional items you will need to include all the default values in your settings. + ## File Types - Setting: `file_types` From e3a6f89e2dc7d9323b6ceb09280cfe515f6dd6e2 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 4 Oct 2024 13:19:18 -0400 Subject: [PATCH 02/28] Make `report_assistant_event` take an `AssistantEvent` struct (#18741) This PR makes the `report_assistant_event` method take an `AssistantEvent` struct instead of all of the struct fields as individual parameters. Release Notes: - N/A --- crates/assistant/src/context.rs | 14 +++--- crates/assistant/src/inline_assistant.rs | 49 ++++++++++--------- .../src/terminal_inline_assistant.rs | 13 ++--- crates/client/src/telemetry.rs | 27 ++-------- 4 files changed, 44 insertions(+), 59 deletions(-) diff --git a/crates/assistant/src/context.rs b/crates/assistant/src/context.rs index 4f1f885b33..c30770cfed 100644 --- a/crates/assistant/src/context.rs +++ b/crates/assistant/src/context.rs @@ -46,7 +46,7 @@ use std::{ sync::Arc, time::{Duration, Instant}, }; -use telemetry_events::{AssistantKind, AssistantPhase}; +use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase}; use text::BufferSnapshot; use util::{post_inc, ResultExt, TryFutureExt}; use uuid::Uuid; @@ -2133,14 +2133,14 @@ impl Context { }); if let Some(telemetry) = this.telemetry.as_ref() { - telemetry.report_assistant_event( - Some(this.id.0.clone()), - AssistantKind::Panel, - AssistantPhase::Response, - model.telemetry_id(), + telemetry.report_assistant_event(AssistantEvent { + conversation_id: Some(this.id.0.clone()), + kind: AssistantKind::Panel, + phase: AssistantPhase::Response, + model: model.telemetry_id(), response_latency, error_message, - ); + }); } if let Ok(stop_reason) = result { diff --git a/crates/assistant/src/inline_assistant.rs b/crates/assistant/src/inline_assistant.rs index fac70f233c..94ae53ab54 100644 --- a/crates/assistant/src/inline_assistant.rs +++ b/crates/assistant/src/inline_assistant.rs @@ -50,6 +50,7 @@ use std::{ task::{self, Poll}, time::{Duration, Instant}, }; +use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase}; use terminal_view::terminal_panel::TerminalPanel; use text::{OffsetRangeExt, ToPoint as _}; use theme::ThemeSettings; @@ -211,14 +212,14 @@ impl InlineAssistant { ) { if let Some(telemetry) = self.telemetry.as_ref() { if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() { - telemetry.report_assistant_event( - None, - telemetry_events::AssistantKind::Inline, - telemetry_events::AssistantPhase::Invoked, - model.telemetry_id(), - None, - None, - ); + telemetry.report_assistant_event(AssistantEvent { + conversation_id: None, + kind: AssistantKind::Inline, + phase: AssistantPhase::Invoked, + model: model.telemetry_id(), + response_latency: None, + error_message: None, + }); } } let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx); @@ -763,18 +764,18 @@ impl InlineAssistant { pub fn finish_assist(&mut self, assist_id: InlineAssistId, undo: bool, cx: &mut WindowContext) { if let Some(telemetry) = self.telemetry.as_ref() { if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() { - telemetry.report_assistant_event( - None, - telemetry_events::AssistantKind::Inline, - if undo { - telemetry_events::AssistantPhase::Rejected + telemetry.report_assistant_event(AssistantEvent { + conversation_id: None, + kind: AssistantKind::Inline, + phase: if undo { + AssistantPhase::Rejected } else { - telemetry_events::AssistantPhase::Accepted + AssistantPhase::Accepted }, - model.telemetry_id(), - None, - None, - ); + model: model.telemetry_id(), + response_latency: None, + error_message: None, + }); } } if let Some(assist) = self.assists.get(&assist_id) { @@ -2920,14 +2921,14 @@ impl CodegenAlternative { let error_message = result.as_ref().err().map(|error| error.to_string()); if let Some(telemetry) = telemetry { - telemetry.report_assistant_event( - None, - telemetry_events::AssistantKind::Inline, - telemetry_events::AssistantPhase::Response, - model_telemetry_id, + telemetry.report_assistant_event(AssistantEvent { + conversation_id: None, + kind: AssistantKind::Inline, + phase: AssistantPhase::Response, + model: model_telemetry_id, response_latency, error_message, - ); + }); } result?; diff --git a/crates/assistant/src/terminal_inline_assistant.rs b/crates/assistant/src/terminal_inline_assistant.rs index e1a26d8510..4cd9d74fc8 100644 --- a/crates/assistant/src/terminal_inline_assistant.rs +++ b/crates/assistant/src/terminal_inline_assistant.rs @@ -25,6 +25,7 @@ use std::{ sync::Arc, time::{Duration, Instant}, }; +use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase}; use terminal::Terminal; use terminal_view::TerminalView; use theme::ThemeSettings; @@ -1063,14 +1064,14 @@ impl Codegen { let error_message = result.as_ref().err().map(|error| error.to_string()); if let Some(telemetry) = telemetry { - telemetry.report_assistant_event( - None, - telemetry_events::AssistantKind::Inline, - telemetry_events::AssistantPhase::Response, - model_telemetry_id, + telemetry.report_assistant_event(AssistantEvent { + conversation_id: None, + kind: AssistantKind::Inline, + phase: AssistantPhase::Response, + model: model_telemetry_id, response_latency, error_message, - ); + }); } result?; diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index 6c1803df3d..5ec2e5b1aa 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -16,9 +16,9 @@ use std::io::Write; use std::{env, mem, path::PathBuf, sync::Arc, time::Duration}; use sysinfo::{CpuRefreshKind, Pid, ProcessRefreshKind, RefreshKind, System}; use telemetry_events::{ - ActionEvent, AppEvent, AssistantEvent, AssistantKind, AssistantPhase, CallEvent, CpuEvent, - EditEvent, EditorEvent, Event, EventRequestBody, EventWrapper, ExtensionEvent, - InlineCompletionEvent, MemoryEvent, ReplEvent, SettingEvent, + ActionEvent, AppEvent, AssistantEvent, CallEvent, CpuEvent, EditEvent, EditorEvent, Event, + EventRequestBody, EventWrapper, ExtensionEvent, InlineCompletionEvent, MemoryEvent, ReplEvent, + SettingEvent, }; use tempfile::NamedTempFile; #[cfg(not(debug_assertions))] @@ -391,25 +391,8 @@ impl Telemetry { self.report_event(event) } - pub fn report_assistant_event( - self: &Arc, - conversation_id: Option, - kind: AssistantKind, - phase: AssistantPhase, - model: String, - response_latency: Option, - error_message: Option, - ) { - let event = Event::Assistant(AssistantEvent { - conversation_id, - kind, - phase, - model: model.to_string(), - response_latency, - error_message, - }); - - self.report_event(event) + pub fn report_assistant_event(self: &Arc, event: AssistantEvent) { + self.report_event(Event::Assistant(event)); } pub fn report_call_event( From dfe1e438325a9b3769c904ead99128d19bc2aaed Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Fri, 4 Oct 2024 14:13:07 -0400 Subject: [PATCH 03/28] docs: Linux XDG desktop secrets portals --- docs/src/linux.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/docs/src/linux.md b/docs/src/linux.md index 4abd7de8ba..6e62300ec8 100644 --- a/docs/src/linux.md +++ b/docs/src/linux.md @@ -123,17 +123,26 @@ If Vulkan is configured correctly, and Zed is still slow for you, please [file a ### I can't open any files -### Zed isn't remembering my login - ### Clicking links isn't working -All of these features are provided by XDG desktop portals, specifically: +These features are provided by XDG desktop portals, specifically: - `org.freedesktop.portal.FileChooser` - `org.freedesktop.portal.OpenURI` Some window managers, such as `Hyprland`, don't provide a file picker by default. See [this list](https://wiki.archlinux.org/title/XDG_Desktop_Portal#List_of_backends_and_interfaces) as a starting point for alternatives. +### Zed isn't remembering my API keys + +### Zed isn't remembering my login + +These feature also requires XDG desktop portals, specifically: + +- `org.freedesktop.portal.Secret` or +- `org.freedesktop.Secrets` + +Zed needs a place to securely store secrets such as your Zed login cookie or your OpenAI API Keys and we use a system provided keychain to do this. Examples of packages that provide this are `gnome-keyring`, `KWallet` and `keepassxc` among others. + ### Could not start inotify Zed relies on inotify to watch your filesystem for changes. If you cannot start inotify then Zed will not work reliably. From 01ad22683d349206518e6778febdbbb0f4ff87b7 Mon Sep 17 00:00:00 2001 From: Boris Cherny Date: Fri, 4 Oct 2024 11:37:27 -0700 Subject: [PATCH 04/28] telemetry: Add `language_name` and `model_provider` (#18640) This PR adds a bit more metadata for assistant logging. Release Notes: - Assistant: Added `language_name` and `model_provider` fields to telemetry events. --------- Co-authored-by: Marshall Bowers Co-authored-by: Max --- Cargo.lock | 1 + crates/assistant/src/context.rs | 7 ++ crates/assistant/src/inline_assistant.rs | 90 +++++++++++++------ .../src/terminal_inline_assistant.rs | 3 + crates/language_model/src/language_model.rs | 7 ++ crates/telemetry_events/Cargo.toml | 1 + .../telemetry_events/src/telemetry_events.rs | 3 + 7 files changed, 83 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e99c6ffd8a..3b2c1d2a60 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11497,6 +11497,7 @@ dependencies = [ name = "telemetry_events" version = "0.1.0" dependencies = [ + "language", "semantic_version", "serde", ] diff --git a/crates/assistant/src/context.rs b/crates/assistant/src/context.rs index c30770cfed..c360c0cb69 100644 --- a/crates/assistant/src/context.rs +++ b/crates/assistant/src/context.rs @@ -2133,13 +2133,20 @@ impl Context { }); if let Some(telemetry) = this.telemetry.as_ref() { + let language_name = this + .buffer + .read(cx) + .language() + .map(|language| language.name()); telemetry.report_assistant_event(AssistantEvent { conversation_id: Some(this.id.0.clone()), kind: AssistantKind::Panel, phase: AssistantPhase::Response, model: model.telemetry_id(), + model_provider: model.provider_id().to_string(), response_latency, error_message, + language_name, }); } diff --git a/crates/assistant/src/inline_assistant.rs b/crates/assistant/src/inline_assistant.rs index 94ae53ab54..b3427271b9 100644 --- a/crates/assistant/src/inline_assistant.rs +++ b/crates/assistant/src/inline_assistant.rs @@ -210,18 +210,6 @@ impl InlineAssistant { initial_prompt: Option, cx: &mut WindowContext, ) { - if let Some(telemetry) = self.telemetry.as_ref() { - if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() { - telemetry.report_assistant_event(AssistantEvent { - conversation_id: None, - kind: AssistantKind::Inline, - phase: AssistantPhase::Invoked, - model: model.telemetry_id(), - response_latency: None, - error_message: None, - }); - } - } let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx); let mut selections = Vec::>::new(); @@ -268,6 +256,21 @@ impl InlineAssistant { text_anchor: buffer.anchor_after(buffer_range.end), }; codegen_ranges.push(start..end); + + if let Some(telemetry) = self.telemetry.as_ref() { + if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() { + telemetry.report_assistant_event(AssistantEvent { + conversation_id: None, + kind: AssistantKind::Inline, + phase: AssistantPhase::Invoked, + model: model.telemetry_id(), + model_provider: model.provider_id().to_string(), + response_latency: None, + error_message: None, + language_name: buffer.language().map(|language| language.name()), + }); + } + } } let assist_group_id = self.next_assist_group_id.post_inc(); @@ -762,23 +765,34 @@ impl InlineAssistant { } pub fn finish_assist(&mut self, assist_id: InlineAssistId, undo: bool, cx: &mut WindowContext) { - if let Some(telemetry) = self.telemetry.as_ref() { - if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() { - telemetry.report_assistant_event(AssistantEvent { - conversation_id: None, - kind: AssistantKind::Inline, - phase: if undo { - AssistantPhase::Rejected - } else { - AssistantPhase::Accepted - }, - model: model.telemetry_id(), - response_latency: None, - error_message: None, - }); - } - } if let Some(assist) = self.assists.get(&assist_id) { + if let Some(telemetry) = self.telemetry.as_ref() { + if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() { + let language_name = assist.editor.upgrade().and_then(|editor| { + let multibuffer = editor.read(cx).buffer().read(cx); + let ranges = multibuffer.range_to_buffer_ranges(assist.range.clone(), cx); + ranges + .first() + .and_then(|(buffer, _, _)| buffer.read(cx).language()) + .map(|language| language.name()) + }); + telemetry.report_assistant_event(AssistantEvent { + conversation_id: None, + kind: AssistantKind::Inline, + phase: if undo { + AssistantPhase::Rejected + } else { + AssistantPhase::Accepted + }, + model: model.telemetry_id(), + model_provider: model.provider_id().to_string(), + response_latency: None, + error_message: None, + language_name, + }); + } + } + let assist_group_id = assist.group_id; if self.assist_groups[&assist_group_id].linked { for assist_id in self.unlink_assist_group(assist_group_id, cx) { @@ -2707,6 +2721,7 @@ impl CodegenAlternative { self.edit_position = Some(self.range.start.bias_right(&self.snapshot)); let telemetry_id = model.telemetry_id(); + let provider_id = model.provider_id(); let chunks: LocalBoxFuture>>> = if user_prompt.trim().to_lowercase() == "delete" { async { Ok(stream::empty().boxed()) }.boxed_local() @@ -2717,7 +2732,7 @@ impl CodegenAlternative { .spawn(|_, cx| async move { model.stream_completion_text(request, &cx).await }); async move { Ok(chunks.await?.boxed()) }.boxed_local() }; - self.handle_stream(telemetry_id, chunks, cx); + self.handle_stream(telemetry_id, provider_id.to_string(), chunks, cx); Ok(()) } @@ -2781,6 +2796,7 @@ impl CodegenAlternative { pub fn handle_stream( &mut self, model_telemetry_id: String, + model_provider_id: String, stream: impl 'static + Future>>>, cx: &mut ModelContext, ) { @@ -2811,6 +2827,15 @@ impl CodegenAlternative { } let telemetry = self.telemetry.clone(); + let language_name = { + let multibuffer = self.buffer.read(cx); + let ranges = multibuffer.range_to_buffer_ranges(self.range.clone(), cx); + ranges + .first() + .and_then(|(buffer, _, _)| buffer.read(cx).language()) + .map(|language| language.name()) + }; + self.diff = Diff::default(); self.status = CodegenStatus::Pending; let mut edit_start = self.range.start.to_offset(&snapshot); @@ -2926,8 +2951,10 @@ impl CodegenAlternative { kind: AssistantKind::Inline, phase: AssistantPhase::Response, model: model_telemetry_id, + model_provider: model_provider_id.to_string(), response_latency, error_message, + language_name, }); } @@ -3540,6 +3567,7 @@ mod tests { let (chunks_tx, chunks_rx) = mpsc::unbounded(); codegen.update(cx, |codegen, cx| { codegen.handle_stream( + String::new(), String::new(), future::ready(Ok(chunks_rx.map(Ok).boxed())), cx, @@ -3611,6 +3639,7 @@ mod tests { let (chunks_tx, chunks_rx) = mpsc::unbounded(); codegen.update(cx, |codegen, cx| { codegen.handle_stream( + String::new(), String::new(), future::ready(Ok(chunks_rx.map(Ok).boxed())), cx, @@ -3685,6 +3714,7 @@ mod tests { let (chunks_tx, chunks_rx) = mpsc::unbounded(); codegen.update(cx, |codegen, cx| { codegen.handle_stream( + String::new(), String::new(), future::ready(Ok(chunks_rx.map(Ok).boxed())), cx, @@ -3758,6 +3788,7 @@ mod tests { let (chunks_tx, chunks_rx) = mpsc::unbounded(); codegen.update(cx, |codegen, cx| { codegen.handle_stream( + String::new(), String::new(), future::ready(Ok(chunks_rx.map(Ok).boxed())), cx, @@ -3821,6 +3852,7 @@ mod tests { let (chunks_tx, chunks_rx) = mpsc::unbounded(); codegen.update(cx, |codegen, cx| { codegen.handle_stream( + String::new(), String::new(), future::ready(Ok(chunks_rx.map(Ok).boxed())), cx, diff --git a/crates/assistant/src/terminal_inline_assistant.rs b/crates/assistant/src/terminal_inline_assistant.rs index 4cd9d74fc8..d30ec2df11 100644 --- a/crates/assistant/src/terminal_inline_assistant.rs +++ b/crates/assistant/src/terminal_inline_assistant.rs @@ -1040,6 +1040,7 @@ impl Codegen { self.transaction = Some(TerminalTransaction::start(self.terminal.clone())); self.generation = cx.spawn(|this, mut cx| async move { let model_telemetry_id = model.telemetry_id(); + let model_provider_id = model.provider_id(); let response = model.stream_completion_text(prompt, &cx).await; let generate = async { let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1); @@ -1069,8 +1070,10 @@ impl Codegen { kind: AssistantKind::Inline, phase: AssistantPhase::Response, model: model_telemetry_id, + model_provider: model_provider_id.to_string(), response_latency, error_message, + language_name: None, }); } diff --git a/crates/language_model/src/language_model.rs b/crates/language_model/src/language_model.rs index 171f5fa819..81d0c874dc 100644 --- a/crates/language_model/src/language_model.rs +++ b/crates/language_model/src/language_model.rs @@ -22,6 +22,7 @@ pub use request::*; pub use role::*; use schemars::JsonSchema; use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use std::fmt; use std::{future::Future, sync::Arc}; use ui::IconName; @@ -231,6 +232,12 @@ pub struct LanguageModelProviderId(pub SharedString); #[derive(Clone, Eq, PartialEq, Hash, Debug, Ord, PartialOrd)] pub struct LanguageModelProviderName(pub SharedString); +impl fmt::Display for LanguageModelProviderId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + impl From for LanguageModelId { fn from(value: String) -> Self { Self(SharedString::from(value)) diff --git a/crates/telemetry_events/Cargo.toml b/crates/telemetry_events/Cargo.toml index 01145549b1..34ccc20c3a 100644 --- a/crates/telemetry_events/Cargo.toml +++ b/crates/telemetry_events/Cargo.toml @@ -12,5 +12,6 @@ workspace = true path = "src/telemetry_events.rs" [dependencies] +language.workspace = true semantic_version.workspace = true serde.workspace = true diff --git a/crates/telemetry_events/src/telemetry_events.rs b/crates/telemetry_events/src/telemetry_events.rs index f7b18523df..56e94edb44 100644 --- a/crates/telemetry_events/src/telemetry_events.rs +++ b/crates/telemetry_events/src/telemetry_events.rs @@ -1,5 +1,6 @@ //! See [Telemetry in Zed](https://zed.dev/docs/telemetry) for additional information. +use language::LanguageName; use semantic_version::SemanticVersion; use serde::{Deserialize, Serialize}; use std::{fmt::Display, sync::Arc, time::Duration}; @@ -153,8 +154,10 @@ pub struct AssistantEvent { pub phase: AssistantPhase, /// Name of the AI model used (gpt-4o, claude-3-5-sonnet, etc) pub model: String, + pub model_provider: String, pub response_latency: Option, pub error_message: Option, + pub language_name: Option, } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] From cee019b1ea138c6efce131c4ae955e26a62e62aa Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 4 Oct 2024 15:06:05 -0400 Subject: [PATCH 05/28] editor: Qualify `RangeExt::overlaps` call to prevent phantom diagnostics (#18743) This PR qualifies a call to `RangeExt::overlaps` to avoid some confusion in rust-analyzer not being able to distinguish between `RangeExt::overlaps` and `AnchorRangeExt::overlaps` and producing phantom diagnostics. We may also want to consider renaming the method on `AnchorRangeExt` to disambiguate them. Release Notes: - N/A --- crates/editor/src/editor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 6fdcb22605..62c2240828 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -10792,7 +10792,7 @@ impl Editor { .selections .all::(cx) .iter() - .any(|selection| selection.range().overlaps(&intersection_range)); + .any(|selection| RangeExt::overlaps(&selection.range(), &intersection_range)); self.unfold_ranges(std::iter::once(intersection_range), true, autoscroll, cx) } From 8f27ffda4d99f31b323e523bc3490339c7864bfe Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Fri, 4 Oct 2024 22:07:58 +0200 Subject: [PATCH 06/28] gpui: Fix uniform list horizon offset for non-horizontal scrollable lists (#18748) Closes #18739 /cc @osiewicz /cc @maxdeviant I'm not sure why the `+ padding.left` was added, but this was the cause of the issue. I also tested removing the extra left padding but didn't seem to see a difference inside the project panel. So we can maybe even remove it? **Before:** ![Screenshot 2024-10-04 at 21 43 34](https://github.com/user-attachments/assets/b5d67cd9-f92b-4301-880c-d351fe156c98) **After:** Screenshot 2024-10-04 at 21 49 05 Release Notes: - Fix code action list not horizontal aligned correctly --- crates/gpui/src/elements/uniform_list.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/gpui/src/elements/uniform_list.rs b/crates/gpui/src/elements/uniform_list.rs index 54297d1214..d17d078184 100644 --- a/crates/gpui/src/elements/uniform_list.rs +++ b/crates/gpui/src/elements/uniform_list.rs @@ -297,7 +297,11 @@ impl Element for UniformList { for (mut item, ix) in items.into_iter().zip(visible_range) { let item_origin = padded_bounds.origin + point( - scroll_offset.x + padding.left, + if can_scroll_horizontally { + scroll_offset.x + padding.left + } else { + scroll_offset.x + }, item_height * ix + scroll_offset.y + padding.top, ); let available_width = if can_scroll_horizontally { From 7608000df8c2e70b0381c7d933197242a3cd24ee Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Fri, 4 Oct 2024 16:56:01 -0400 Subject: [PATCH 07/28] Fix option-t and option-shift-t in terminal (#18749) --- assets/keymaps/default-macos.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index b405ee1852..234485bc38 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -440,7 +440,12 @@ "cmd-k shift-right": ["workspace::SwapPaneInDirection", "Right"], "cmd-k shift-up": ["workspace::SwapPaneInDirection", "Up"], "cmd-k shift-down": ["workspace::SwapPaneInDirection", "Down"], - "cmd-shift-x": "zed::Extensions", + "cmd-shift-x": "zed::Extensions" + } + }, + { + "context": "Editor && !Terminal", + "bindings": { "alt-t": "task::Rerun", "alt-shift-t": "task::Spawn" } From 1f31022cbe1d9811682986cd82bac9785b3aa266 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 5 Oct 2024 12:58:45 +0300 Subject: [PATCH 08/28] Compare migrations formatted uniformly (#18760) Otherwise old migrations may be formatted differently than new migrations, causing comparison errors. Follow-up of https://github.com/zed-industries/zed/pull/18676 Release Notes: - N/A --- Cargo.lock | 1 + Cargo.toml | 1 + crates/sqlez/Cargo.toml | 1 + crates/sqlez/src/migrations.rs | 31 ++++++++++++++----------------- crates/sqlez_macros/Cargo.toml | 2 +- 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3b2c1d2a60..9945b2df9a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10710,6 +10710,7 @@ dependencies = [ "libsqlite3-sys", "parking_lot", "smol", + "sqlformat", "thread_local", "util", "uuid", diff --git a/Cargo.toml b/Cargo.toml index a23663f5c8..d6f4279449 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -419,6 +419,7 @@ similar = "1.3" simplelog = "0.12.2" smallvec = { version = "1.6", features = ["union"] } smol = "1.2" +sqlformat = "0.2" strsim = "0.11" strum = { version = "0.25.0", features = ["derive"] } subtle = "2.5.0" diff --git a/crates/sqlez/Cargo.toml b/crates/sqlez/Cargo.toml index 461017dd8d..43626d7747 100644 --- a/crates/sqlez/Cargo.toml +++ b/crates/sqlez/Cargo.toml @@ -16,6 +16,7 @@ indoc.workspace = true libsqlite3-sys = { version = "0.28", features = ["bundled"] } parking_lot.workspace = true smol.workspace = true +sqlformat.workspace = true thread_local = "1.1.4" util.workspace = true uuid.workspace = true diff --git a/crates/sqlez/src/migrations.rs b/crates/sqlez/src/migrations.rs index f97706d03b..72187d0eb2 100644 --- a/crates/sqlez/src/migrations.rs +++ b/crates/sqlez/src/migrations.rs @@ -55,7 +55,16 @@ impl Connection { .exec_bound("INSERT INTO migrations (domain, step, migration) VALUES (?, ?, ?)")?; for (index, migration) in migrations.iter().enumerate() { + let migration = + sqlformat::format(migration, &sqlformat::QueryParams::None, Default::default()); if let Some((_, _, completed_migration)) = completed_migrations.get(index) { + // Reformat completed migrations with the current `sqlformat` version, so that past migrations stored + // conform to the new formatting rules. + let completed_migration = sqlformat::format( + completed_migration, + &sqlformat::QueryParams::None, + Default::default(), + ); if completed_migration == migration { // Migration already run. Continue continue; @@ -71,8 +80,8 @@ impl Connection { } } - self.eager_exec(migration)?; - store_completed_migration((domain, index, *migration))?; + self.eager_exec(&migration)?; + store_completed_migration((domain, index, migration))?; } Ok(()) @@ -108,11 +117,7 @@ mod test { .select::("SELECT (migration) FROM migrations") .unwrap()() .unwrap()[..], - &[indoc! {" - CREATE TABLE test1 ( - a TEXT, - b TEXT - )"}], + &[indoc! {"CREATE TABLE test1 (a TEXT, b TEXT)"}], ); // Add another step to the migration and run it again @@ -141,16 +146,8 @@ mod test { .unwrap()() .unwrap()[..], &[ - indoc! {" - CREATE TABLE test1 ( - a TEXT, - b TEXT - )"}, - indoc! {" - CREATE TABLE test2 ( - c TEXT, - d TEXT - )"}, + indoc! {"CREATE TABLE test1 (a TEXT, b TEXT)"}, + indoc! {"CREATE TABLE test2 (c TEXT, d TEXT)"}, ], ); } diff --git a/crates/sqlez_macros/Cargo.toml b/crates/sqlez_macros/Cargo.toml index 93fcac42ec..3a0ae19c46 100644 --- a/crates/sqlez_macros/Cargo.toml +++ b/crates/sqlez_macros/Cargo.toml @@ -15,5 +15,5 @@ doctest = false [dependencies] sqlez.workspace = true -sqlformat = "0.2" +sqlformat.workspace = true syn = "1.0" From c9bee9f81f4254441583a426c7b8c7d3fb44e2ec Mon Sep 17 00:00:00 2001 From: Chris Boette Date: Sat, 5 Oct 2024 12:26:28 -0400 Subject: [PATCH 09/28] docs: Note the need for Rust when developing extensions (#18753) --- docs/src/extensions/developing-extensions.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/src/extensions/developing-extensions.md b/docs/src/extensions/developing-extensions.md index 503d253fc3..1ab1676e36 100644 --- a/docs/src/extensions/developing-extensions.md +++ b/docs/src/extensions/developing-extensions.md @@ -76,6 +76,8 @@ zed::register_extension!(MyExtension); ## Developing an Extension Locally +Before starting to develop an extension for Zed, be sure to [install Rust](https://www.rust-lang.org/tools/install). + When developing an extension, you can use it in Zed without needing to publish it by installing it as a _dev extension_. From the extensions page, click the `Install Dev Extension` button and select the directory containing your extension. From 8376dd2011099238ef7f6c6010a9953d3a6df97d Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Sat, 5 Oct 2024 23:28:34 -0400 Subject: [PATCH 10/28] `ui` crate docs & spring cleaning (#18768) Similar to https://github.com/zed-industries/zed/pull/18690 & https://github.com/zed-industries/zed/pull/18695, this PR enables required docs for `ui` and does some cleanup. Changes: - Enables the `deny(missing_docs)` crate-wide. - Adds `allow(missing_docs)` on many modules until folks pick them up to document them - Documents some modules (all in `ui/src/styles`) - Crate root-level organization: Traits move to `traits`, other misc organization - Cleaned out a bunch of unused code. Note: I'd like to remove `utils/format_distance` but the assistant panel uses it. To move it over to use the `time_format` crate we may need to update it to use `time` instead of `chrono`. Needs more investigation. Release Notes: - N/A --- crates/assistant/src/assistant_panel.rs | 6 +- crates/assistant/src/prompt_library.rs | 2 +- crates/storybook/src/stories/with_rem_size.rs | 2 +- crates/ui/src/components/avatar/avatar.rs | 42 +---- .../avatar/avatar_audio_status_indicator.rs | 7 + .../avatar/avatar_availability_indicator.rs | 1 + crates/ui/src/components/button/button.rs | 1 + .../ui/src/components/button/button_icon.rs | 1 + .../ui/src/components/button/button_like.rs | 31 ++-- .../ui/src/components/button/icon_button.rs | 1 + .../ui/src/components/button/toggle_button.rs | 1 + crates/ui/src/components/checkbox.rs | 1 + crates/ui/src/components/checkbox/checkbox.rs | 2 + .../checkbox/checkbox_with_label.rs | 2 + crates/ui/src/components/context_menu.rs | 5 +- crates/ui/src/components/disclosure.rs | 1 + crates/ui/src/components/divider.rs | 1 + crates/ui/src/components/dropdown_menu.rs | 1 + crates/ui/src/components/facepile.rs | 1 + crates/ui/src/components/icon.rs | 1 + crates/ui/src/components/image.rs | 1 + crates/ui/src/components/indicator.rs | 1 + crates/ui/src/components/keybinding.rs | 174 +++++++++++++++++- .../src/components/label/highlighted_label.rs | 2 + crates/ui/src/components/label/label.rs | 2 + crates/ui/src/components/label/label_like.rs | 2 + crates/ui/src/components/list/list.rs | 2 + crates/ui/src/components/list/list_header.rs | 2 + crates/ui/src/components/list/list_item.rs | 2 + .../ui/src/components/list/list_separator.rs | 2 + .../ui/src/components/list/list_sub_header.rs | 2 + crates/ui/src/components/modal.rs | 2 + crates/ui/src/components/numeric_stepper.rs | 2 + crates/ui/src/components/popover.rs | 2 + crates/ui/src/components/popover_menu.rs | 2 + crates/ui/src/components/radio.rs | 2 + crates/ui/src/components/right_click_menu.rs | 2 + .../ui/src/components/settings_container.rs | 2 + crates/ui/src/components/settings_group.rs | 2 + crates/ui/src/components/stack.rs | 2 + crates/ui/src/components/stories.rs | 3 + crates/ui/src/components/tab.rs | 1 + crates/ui/src/components/tab_bar.rs | 1 + crates/ui/src/components/tool_strip.rs | 2 + crates/ui/src/components/tooltip.rs | 2 + crates/ui/src/key_bindings.rs | 170 ----------------- crates/ui/src/prelude.rs | 13 +- crates/ui/src/styles/appearance.rs | 8 +- crates/ui/src/styles/color.rs | 65 +++++-- crates/ui/src/styles/docs/elevation.md | 44 ----- crates/ui/src/styles/elevation.rs | 47 ++--- crates/ui/src/styles/spacing.rs | 14 +- crates/ui/src/styles/typography.rs | 21 ++- crates/ui/src/tests.rs | 1 + crates/ui/src/{ => tests}/path_str.rs | 16 +- crates/ui/src/traits.rs | 6 + crates/ui/src/{ => traits}/clickable.rs | 0 crates/ui/src/{ => traits}/disableable.rs | 0 crates/ui/src/{ => traits}/fixed.rs | 0 crates/ui/src/{ => traits}/selectable.rs | 0 crates/ui/src/{ => traits}/styled_ext.rs | 0 .../ui/src/{ => traits}/visible_on_hover.rs | 2 + crates/ui/src/ui.rs | 24 +-- crates/ui/src/utils.rs | 4 +- crates/ui/src/utils/format_distance.rs | 3 + crates/ui/src/{ => utils}/with_rem_size.rs | 2 + 66 files changed, 405 insertions(+), 364 deletions(-) delete mode 100644 crates/ui/src/key_bindings.rs delete mode 100644 crates/ui/src/styles/docs/elevation.md create mode 100644 crates/ui/src/tests.rs rename crates/ui/src/{ => tests}/path_str.rs (54%) create mode 100644 crates/ui/src/traits.rs rename crates/ui/src/{ => traits}/clickable.rs (100%) rename crates/ui/src/{ => traits}/disableable.rs (100%) rename crates/ui/src/{ => traits}/fixed.rs (100%) rename crates/ui/src/{ => traits}/selectable.rs (100%) rename crates/ui/src/{ => traits}/styled_ext.rs (100%) rename crates/ui/src/{ => traits}/visible_on_hover.rs (85%) rename crates/ui/src/{ => utils}/with_rem_size.rs (95%) diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 15f9e5c59d..20e6347d01 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -77,7 +77,7 @@ use ui::TintColor; use ui::{ prelude::*, utils::{format_distance_from_now, DateTimeType}, - Avatar, AvatarShape, ButtonLike, ContextMenu, Disclosure, ElevationIndex, KeyBinding, ListItem, + Avatar, ButtonLike, ContextMenu, Disclosure, ElevationIndex, KeyBinding, ListItem, ListItemSpacing, PopoverMenu, PopoverMenuHandle, Tooltip, }; use util::{maybe, ResultExt}; @@ -262,9 +262,7 @@ impl PickerDelegate for SavedContextPickerDelegate { .gap_2() .children(if let Some(host_user) = host_user { vec![ - Avatar::new(host_user.avatar_uri.clone()) - .shape(AvatarShape::Circle) - .into_any_element(), + Avatar::new(host_user.avatar_uri.clone()).into_any_element(), Label::new(format!("Shared by @{}", host_user.github_login)) .color(Color::Muted) .size(LabelSize::Small) diff --git a/crates/assistant/src/prompt_library.rs b/crates/assistant/src/prompt_library.rs index 24e20a18a7..6c43579d6e 100644 --- a/crates/assistant/src/prompt_library.rs +++ b/crates/assistant/src/prompt_library.rs @@ -910,7 +910,7 @@ impl PromptLibrary { .features .clone(), font_size: HeadlineSize::Large - .size() + .rems() .into(), font_weight: settings.ui_font.weight, line_height: relative( diff --git a/crates/storybook/src/stories/with_rem_size.rs b/crates/storybook/src/stories/with_rem_size.rs index 11add24955..558dab6c54 100644 --- a/crates/storybook/src/stories/with_rem_size.rs +++ b/crates/storybook/src/stories/with_rem_size.rs @@ -1,7 +1,7 @@ use gpui::{AnyElement, Hsla, Render}; use story::Story; -use ui::{prelude::*, WithRemSize}; +use ui::{prelude::*, utils::WithRemSize}; pub struct WithRemSizeStory; diff --git a/crates/ui/src/components/avatar/avatar.rs b/crates/ui/src/components/avatar/avatar.rs index 27cf86a01f..4106b17452 100644 --- a/crates/ui/src/components/avatar/avatar.rs +++ b/crates/ui/src/components/avatar/avatar.rs @@ -2,16 +2,6 @@ use crate::prelude::*; use gpui::{img, AnyElement, Hsla, ImageSource, Img, IntoElement, Styled}; -/// The shape of an [`Avatar`]. -#[derive(Debug, Default, PartialEq, Clone)] -pub enum AvatarShape { - /// The avatar is shown in a circle. - #[default] - Circle, - /// The avatar is shown in a rectangle with rounded corners. - RoundedRectangle, -} - /// An element that renders a user avatar with customizable appearance options. /// /// # Examples @@ -20,7 +10,6 @@ pub enum AvatarShape { /// use ui::{Avatar, AvatarShape}; /// /// Avatar::new("path/to/image.png") -/// .shape(AvatarShape::Circle) /// .grayscale(true) /// .border_color(gpui::red()); /// ``` @@ -33,6 +22,7 @@ pub struct Avatar { } impl Avatar { + /// Creates a new avatar element with the specified image source. pub fn new(src: impl Into) -> Self { Avatar { image: img(src), @@ -42,26 +32,6 @@ impl Avatar { } } - /// Sets the shape of the avatar image. - /// - /// This method allows the shape of the avatar to be specified using an [`AvatarShape`]. - /// It modifies the corner radius of the image to match the specified shape. - /// - /// # Examples - /// - /// ``` - /// use ui::{Avatar, AvatarShape}; - /// - /// Avatar::new("path/to/image.png").shape(AvatarShape::Circle); - /// ``` - pub fn shape(mut self, shape: AvatarShape) -> Self { - self.image = match shape { - AvatarShape::Circle => self.image.rounded_full(), - AvatarShape::RoundedRectangle => self.image.rounded_md(), - }; - self - } - /// Applies a grayscale filter to the avatar image. /// /// # Examples @@ -76,6 +46,11 @@ impl Avatar { self } + /// Sets the border color of the avatar. + /// + /// This might be used to match the border to the background color of + /// the parent element to create the illusion of cropping another + /// shape underneath (for example in face piles.) pub fn border_color(mut self, color: impl Into) -> Self { self.border_color = Some(color.into()); self @@ -87,6 +62,7 @@ impl Avatar { self } + /// Sets the current indicator to be displayed on the avatar, if any. pub fn indicator(mut self, indicator: impl Into>) -> Self { self.indicator = indicator.into().map(IntoElement::into_any_element); self @@ -95,10 +71,6 @@ impl Avatar { impl RenderOnce for Avatar { fn render(mut self, cx: &mut WindowContext) -> impl IntoElement { - if self.image.style().corner_radii.top_left.is_none() { - self = self.shape(AvatarShape::Circle); - } - let border_width = if self.border_color.is_some() { px(2.) } else { diff --git a/crates/ui/src/components/avatar/avatar_audio_status_indicator.rs b/crates/ui/src/components/avatar/avatar_audio_status_indicator.rs index e249727a0c..8ff6491db7 100644 --- a/crates/ui/src/components/avatar/avatar_audio_status_indicator.rs +++ b/crates/ui/src/components/avatar/avatar_audio_status_indicator.rs @@ -2,12 +2,17 @@ use gpui::AnyView; use crate::prelude::*; +/// The audio status of an player, for use in representing +/// their status visually on their avatar. #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] pub enum AudioStatus { + /// The player's microphone is muted. Muted, + /// The player's microphone is muted, and collaboration audio is disabled. Deafened, } +/// An indicator that shows the audio status of a player. #[derive(IntoElement)] pub struct AvatarAudioStatusIndicator { audio_status: AudioStatus, @@ -15,6 +20,7 @@ pub struct AvatarAudioStatusIndicator { } impl AvatarAudioStatusIndicator { + /// Creates a new `AvatarAudioStatusIndicator` pub fn new(audio_status: AudioStatus) -> Self { Self { audio_status, @@ -22,6 +28,7 @@ impl AvatarAudioStatusIndicator { } } + /// Sets the tooltip for the indicator. pub fn tooltip(mut self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self { self.tooltip = Some(Box::new(tooltip)); self diff --git a/crates/ui/src/components/avatar/avatar_availability_indicator.rs b/crates/ui/src/components/avatar/avatar_availability_indicator.rs index 3e4f9b2d1b..4690f8fccd 100644 --- a/crates/ui/src/components/avatar/avatar_availability_indicator.rs +++ b/crates/ui/src/components/avatar/avatar_availability_indicator.rs @@ -1,3 +1,4 @@ +#![allow(missing_docs)] use crate::prelude::*; #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] diff --git a/crates/ui/src/components/button/button.rs b/crates/ui/src/components/button/button.rs index 1e11d25cc6..f81de83a2b 100644 --- a/crates/ui/src/components/button/button.rs +++ b/crates/ui/src/components/button/button.rs @@ -1,3 +1,4 @@ +#![allow(missing_docs)] use gpui::{AnyView, DefiniteLength}; use crate::{prelude::*, ElevationIndex, IconPosition, KeyBinding, Spacing}; diff --git a/crates/ui/src/components/button/button_icon.rs b/crates/ui/src/components/button/button_icon.rs index 54ca0f7921..f3aebe7f76 100644 --- a/crates/ui/src/components/button/button_icon.rs +++ b/crates/ui/src/components/button/button_icon.rs @@ -1,3 +1,4 @@ +#![allow(missing_docs)] use crate::{prelude::*, Icon, IconName, IconSize}; /// An icon that appears within a button. diff --git a/crates/ui/src/components/button/button_like.rs b/crates/ui/src/components/button/button_like.rs index a22c27d241..22e8421391 100644 --- a/crates/ui/src/components/button/button_like.rs +++ b/crates/ui/src/components/button/button_like.rs @@ -1,8 +1,9 @@ +#![allow(missing_docs)] use gpui::{relative, CursorStyle, DefiniteLength, MouseButton}; use gpui::{transparent_black, AnyElement, AnyView, ClickEvent, Hsla, Rems}; use smallvec::SmallVec; -use crate::{prelude::*, Elevation, ElevationIndex, Spacing}; +use crate::{prelude::*, ElevationIndex, Spacing}; /// A trait for buttons that can be Selected. Enables setting the [`ButtonStyle`] of a button when it is selected. pub trait SelectableButton: Selectable { @@ -145,20 +146,12 @@ pub(crate) struct ButtonLikeStyles { pub icon_color: Hsla, } -fn element_bg_from_elevation(elevation: Option, cx: &mut WindowContext) -> Hsla { +fn element_bg_from_elevation(elevation: Option, cx: &mut WindowContext) -> Hsla { match elevation { - Some(Elevation::ElevationIndex(ElevationIndex::Background)) => { - cx.theme().colors().element_background - } - Some(Elevation::ElevationIndex(ElevationIndex::ElevatedSurface)) => { - cx.theme().colors().surface_background - } - Some(Elevation::ElevationIndex(ElevationIndex::Surface)) => { - cx.theme().colors().elevated_surface_background - } - Some(Elevation::ElevationIndex(ElevationIndex::ModalSurface)) => { - cx.theme().colors().background - } + Some(ElevationIndex::Background) => cx.theme().colors().element_background, + Some(ElevationIndex::ElevatedSurface) => cx.theme().colors().surface_background, + Some(ElevationIndex::Surface) => cx.theme().colors().elevated_surface_background, + Some(ElevationIndex::ModalSurface) => cx.theme().colors().background, _ => cx.theme().colors().element_background, } } @@ -166,7 +159,7 @@ fn element_bg_from_elevation(elevation: Option, cx: &mut WindowContex impl ButtonStyle { pub(crate) fn enabled( self, - elevation: Option, + elevation: Option, cx: &mut WindowContext, ) -> ButtonLikeStyles { let filled_background = element_bg_from_elevation(elevation, cx); @@ -196,7 +189,7 @@ impl ButtonStyle { pub(crate) fn hovered( self, - elevation: Option, + elevation: Option, cx: &mut WindowContext, ) -> ButtonLikeStyles { let mut filled_background = element_bg_from_elevation(elevation, cx); @@ -281,7 +274,7 @@ impl ButtonStyle { #[allow(unused)] pub(crate) fn disabled( self, - elevation: Option, + elevation: Option, cx: &mut WindowContext, ) -> ButtonLikeStyles { element_bg_from_elevation(elevation, cx).fade_out(0.82); @@ -348,7 +341,7 @@ pub struct ButtonLike { pub(super) selected_style: Option, pub(super) width: Option, pub(super) height: Option, - pub(super) layer: Option, + pub(super) layer: Option, size: ButtonSize, rounding: Option, tooltip: Option AnyView>>, @@ -463,7 +456,7 @@ impl ButtonCommon for ButtonLike { } fn layer(mut self, elevation: ElevationIndex) -> Self { - self.layer = Some(elevation.into()); + self.layer = Some(elevation); self } } diff --git a/crates/ui/src/components/button/icon_button.rs b/crates/ui/src/components/button/icon_button.rs index cf564efabb..bad10d6fb4 100644 --- a/crates/ui/src/components/button/icon_button.rs +++ b/crates/ui/src/components/button/icon_button.rs @@ -1,3 +1,4 @@ +#![allow(missing_docs)] use gpui::{AnyView, DefiniteLength}; use super::button_like::{ButtonCommon, ButtonLike, ButtonSize, ButtonStyle}; diff --git a/crates/ui/src/components/button/toggle_button.rs b/crates/ui/src/components/button/toggle_button.rs index 3ad47ed166..33577fc4e8 100644 --- a/crates/ui/src/components/button/toggle_button.rs +++ b/crates/ui/src/components/button/toggle_button.rs @@ -1,3 +1,4 @@ +#![allow(missing_docs)] use gpui::{AnyView, ClickEvent}; use crate::{prelude::*, ButtonLike, ButtonLikeRounding, ElevationIndex}; diff --git a/crates/ui/src/components/checkbox.rs b/crates/ui/src/components/checkbox.rs index d750d6c036..d3c4d377ae 100644 --- a/crates/ui/src/components/checkbox.rs +++ b/crates/ui/src/components/checkbox.rs @@ -1,3 +1,4 @@ +#![allow(missing_docs)] mod checkbox_with_label; pub use checkbox_with_label::*; diff --git a/crates/ui/src/components/checkbox/checkbox.rs b/crates/ui/src/components/checkbox/checkbox.rs index cd1ea97ea9..472bfb23f1 100644 --- a/crates/ui/src/components/checkbox/checkbox.rs +++ b/crates/ui/src/components/checkbox/checkbox.rs @@ -1,3 +1,5 @@ +#![allow(missing_docs)] + use gpui::{div, prelude::*, ElementId, IntoElement, Styled, WindowContext}; use crate::prelude::*; diff --git a/crates/ui/src/components/checkbox/checkbox_with_label.rs b/crates/ui/src/components/checkbox/checkbox_with_label.rs index 2cf8fc2832..2b92e47938 100644 --- a/crates/ui/src/components/checkbox/checkbox_with_label.rs +++ b/crates/ui/src/components/checkbox/checkbox_with_label.rs @@ -1,3 +1,5 @@ +#![allow(missing_docs)] + use std::sync::Arc; use crate::{prelude::*, Checkbox}; diff --git a/crates/ui/src/components/context_menu.rs b/crates/ui/src/components/context_menu.rs index 644cb1dd24..92884b0182 100644 --- a/crates/ui/src/components/context_menu.rs +++ b/crates/ui/src/components/context_menu.rs @@ -1,6 +1,7 @@ +#![allow(missing_docs)] use crate::{ - h_flex, prelude::*, v_flex, Icon, IconName, KeyBinding, Label, List, ListItem, ListSeparator, - ListSubHeader, WithRemSize, + h_flex, prelude::*, utils::WithRemSize, v_flex, Icon, IconName, KeyBinding, Label, List, + ListItem, ListSeparator, ListSubHeader, }; use gpui::{ px, Action, AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, diff --git a/crates/ui/src/components/disclosure.rs b/crates/ui/src/components/disclosure.rs index 41ff0a4c3a..9e8ab48221 100644 --- a/crates/ui/src/components/disclosure.rs +++ b/crates/ui/src/components/disclosure.rs @@ -1,3 +1,4 @@ +#![allow(missing_docs)] use std::sync::Arc; use gpui::{ClickEvent, CursorStyle}; diff --git a/crates/ui/src/components/divider.rs b/crates/ui/src/components/divider.rs index 772fc1a81a..71234057b2 100644 --- a/crates/ui/src/components/divider.rs +++ b/crates/ui/src/components/divider.rs @@ -1,3 +1,4 @@ +#![allow(missing_docs)] use gpui::{Hsla, IntoElement}; use crate::prelude::*; diff --git a/crates/ui/src/components/dropdown_menu.rs b/crates/ui/src/components/dropdown_menu.rs index ab2f6a372e..8d930a63ac 100644 --- a/crates/ui/src/components/dropdown_menu.rs +++ b/crates/ui/src/components/dropdown_menu.rs @@ -1,3 +1,4 @@ +#![allow(missing_docs)] use gpui::{AnchorCorner, ClickEvent, CursorStyle, MouseButton, View}; use crate::{prelude::*, ContextMenu, PopoverMenu}; diff --git a/crates/ui/src/components/facepile.rs b/crates/ui/src/components/facepile.rs index f391fc60bb..c3a56d8e20 100644 --- a/crates/ui/src/components/facepile.rs +++ b/crates/ui/src/components/facepile.rs @@ -1,3 +1,4 @@ +#![allow(missing_docs)] use crate::prelude::*; use gpui::{AnyElement, StyleRefinement}; use smallvec::SmallVec; diff --git a/crates/ui/src/components/icon.rs b/crates/ui/src/components/icon.rs index 323181e841..693caaaafd 100644 --- a/crates/ui/src/components/icon.rs +++ b/crates/ui/src/components/icon.rs @@ -1,3 +1,4 @@ +#![allow(missing_docs)] use gpui::{svg, AnimationElement, Hsla, IntoElement, Rems, Transformation}; use serde::{Deserialize, Serialize}; use strum::{EnumIter, EnumString, IntoStaticStr}; diff --git a/crates/ui/src/components/image.rs b/crates/ui/src/components/image.rs index e7eefe5fea..a510b4d7a2 100644 --- a/crates/ui/src/components/image.rs +++ b/crates/ui/src/components/image.rs @@ -1,3 +1,4 @@ +#![allow(missing_docs)] use gpui::{svg, IntoElement, Rems, RenderOnce, Size, Styled, WindowContext}; use serde::{Deserialize, Serialize}; use strum::{EnumIter, EnumString, IntoStaticStr}; diff --git a/crates/ui/src/components/indicator.rs b/crates/ui/src/components/indicator.rs index 0b4a3147c1..009fe44dfe 100644 --- a/crates/ui/src/components/indicator.rs +++ b/crates/ui/src/components/indicator.rs @@ -1,3 +1,4 @@ +#![allow(missing_docs)] use crate::{prelude::*, AnyIcon}; #[derive(Default)] diff --git a/crates/ui/src/components/keybinding.rs b/crates/ui/src/components/keybinding.rs index 1e4983f5ad..135599b914 100644 --- a/crates/ui/src/components/keybinding.rs +++ b/crates/ui/src/components/keybinding.rs @@ -1,5 +1,7 @@ +#![allow(missing_docs)] +use crate::PlatformStyle; use crate::{h_flex, prelude::*, Icon, IconName, IconSize}; -use gpui::{relative, Action, FocusHandle, IntoElement, Keystroke}; +use gpui::{relative, Action, FocusHandle, IntoElement, Keystroke, WindowContext}; #[derive(IntoElement, Clone)] pub struct KeyBinding { @@ -192,3 +194,173 @@ impl KeyIcon { Self { icon } } } + +/// Returns a textual representation of the key binding for the given [`Action`]. +pub fn text_for_action(action: &dyn Action, cx: &mut WindowContext) -> Option { + let key_binding = cx.bindings_for_action(action).last().cloned()?; + Some(text_for_key_binding(key_binding, PlatformStyle::platform())) +} + +/// Returns a textual representation of the key binding for the given [`Action`] +/// as if the provided [`FocusHandle`] was focused. +pub fn text_for_action_in( + action: &dyn Action, + focus: &FocusHandle, + cx: &mut WindowContext, +) -> Option { + let key_binding = cx.bindings_for_action_in(action, focus).last().cloned()?; + Some(text_for_key_binding(key_binding, PlatformStyle::platform())) +} + +/// Returns a textual representation of the given key binding for the specified platform. +pub fn text_for_key_binding( + key_binding: gpui::KeyBinding, + platform_style: PlatformStyle, +) -> String { + key_binding + .keystrokes() + .iter() + .map(|keystroke| text_for_keystroke(keystroke, platform_style)) + .collect::>() + .join(" ") +} + +/// Returns a textual representation of the given [`Keystroke`]. +pub fn text_for_keystroke(keystroke: &Keystroke, platform_style: PlatformStyle) -> String { + let mut text = String::new(); + + let delimiter = match platform_style { + PlatformStyle::Mac => '-', + PlatformStyle::Linux | PlatformStyle::Windows => '+', + }; + + if keystroke.modifiers.function { + match platform_style { + PlatformStyle::Mac => text.push_str("fn"), + PlatformStyle::Linux | PlatformStyle::Windows => text.push_str("Fn"), + } + + text.push(delimiter); + } + + if keystroke.modifiers.control { + match platform_style { + PlatformStyle::Mac => text.push_str("Control"), + PlatformStyle::Linux | PlatformStyle::Windows => text.push_str("Ctrl"), + } + + text.push(delimiter); + } + + if keystroke.modifiers.alt { + match platform_style { + PlatformStyle::Mac => text.push_str("Option"), + PlatformStyle::Linux | PlatformStyle::Windows => text.push_str("Alt"), + } + + text.push(delimiter); + } + + if keystroke.modifiers.platform { + match platform_style { + PlatformStyle::Mac => text.push_str("Command"), + PlatformStyle::Linux => text.push_str("Super"), + PlatformStyle::Windows => text.push_str("Win"), + } + + text.push(delimiter); + } + + if keystroke.modifiers.shift { + match platform_style { + PlatformStyle::Mac | PlatformStyle::Linux | PlatformStyle::Windows => { + text.push_str("Shift") + } + } + + text.push(delimiter); + } + + fn capitalize(str: &str) -> String { + let mut chars = str.chars(); + match chars.next() { + None => String::new(), + Some(first_char) => first_char.to_uppercase().collect::() + chars.as_str(), + } + } + + let key = match keystroke.key.as_str() { + "pageup" => "PageUp", + "pagedown" => "PageDown", + key => &capitalize(key), + }; + + text.push_str(key); + + text +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_text_for_keystroke() { + assert_eq!( + text_for_keystroke(&Keystroke::parse("cmd-c").unwrap(), PlatformStyle::Mac), + "Command-C".to_string() + ); + assert_eq!( + text_for_keystroke(&Keystroke::parse("cmd-c").unwrap(), PlatformStyle::Linux), + "Super+C".to_string() + ); + assert_eq!( + text_for_keystroke(&Keystroke::parse("cmd-c").unwrap(), PlatformStyle::Windows), + "Win+C".to_string() + ); + + assert_eq!( + text_for_keystroke( + &Keystroke::parse("ctrl-alt-delete").unwrap(), + PlatformStyle::Mac + ), + "Control-Option-Delete".to_string() + ); + assert_eq!( + text_for_keystroke( + &Keystroke::parse("ctrl-alt-delete").unwrap(), + PlatformStyle::Linux + ), + "Ctrl+Alt+Delete".to_string() + ); + assert_eq!( + text_for_keystroke( + &Keystroke::parse("ctrl-alt-delete").unwrap(), + PlatformStyle::Windows + ), + "Ctrl+Alt+Delete".to_string() + ); + + assert_eq!( + text_for_keystroke( + &Keystroke::parse("shift-pageup").unwrap(), + PlatformStyle::Mac + ), + "Shift-PageUp".to_string() + ); + assert_eq!( + text_for_keystroke( + &Keystroke::parse("shift-pageup").unwrap(), + PlatformStyle::Linux + ), + "Shift+PageUp".to_string() + ); + assert_eq!( + text_for_keystroke( + &Keystroke::parse("shift-pageup").unwrap(), + PlatformStyle::Windows + ), + "Shift+PageUp".to_string() + ); + } +} diff --git a/crates/ui/src/components/label/highlighted_label.rs b/crates/ui/src/components/label/highlighted_label.rs index 6b170bb810..f961713956 100644 --- a/crates/ui/src/components/label/highlighted_label.rs +++ b/crates/ui/src/components/label/highlighted_label.rs @@ -1,3 +1,5 @@ +#![allow(missing_docs)] + use std::ops::Range; use gpui::{FontWeight, HighlightStyle, StyledText}; diff --git a/crates/ui/src/components/label/label.rs b/crates/ui/src/components/label/label.rs index 898a59de77..f655961841 100644 --- a/crates/ui/src/components/label/label.rs +++ b/crates/ui/src/components/label/label.rs @@ -1,3 +1,5 @@ +#![allow(missing_docs)] + use gpui::{StyleRefinement, WindowContext}; use crate::{prelude::*, LabelCommon, LabelLike, LabelSize, LineHeightStyle}; diff --git a/crates/ui/src/components/label/label_like.rs b/crates/ui/src/components/label/label_like.rs index bc2fae15a7..0bd21eda4f 100644 --- a/crates/ui/src/components/label/label_like.rs +++ b/crates/ui/src/components/label/label_like.rs @@ -1,3 +1,5 @@ +#![allow(missing_docs)] + use gpui::{relative, AnyElement, FontWeight, StyleRefinement, Styled, UnderlineStyle}; use settings::Settings; use smallvec::SmallVec; diff --git a/crates/ui/src/components/list/list.rs b/crates/ui/src/components/list/list.rs index 4bf157ef40..e112a558ee 100644 --- a/crates/ui/src/components/list/list.rs +++ b/crates/ui/src/components/list/list.rs @@ -1,3 +1,5 @@ +#![allow(missing_docs)] + use gpui::AnyElement; use smallvec::SmallVec; diff --git a/crates/ui/src/components/list/list_header.rs b/crates/ui/src/components/list/list_header.rs index 3b15f8cd3d..b69426b6d1 100644 --- a/crates/ui/src/components/list/list_header.rs +++ b/crates/ui/src/components/list/list_header.rs @@ -1,3 +1,5 @@ +#![allow(missing_docs)] + use std::sync::Arc; use crate::{h_flex, prelude::*, Disclosure, Label}; diff --git a/crates/ui/src/components/list/list_item.rs b/crates/ui/src/components/list/list_item.rs index e13fb8ef26..37076737a6 100644 --- a/crates/ui/src/components/list/list_item.rs +++ b/crates/ui/src/components/list/list_item.rs @@ -1,3 +1,5 @@ +#![allow(missing_docs)] + use std::sync::Arc; use gpui::{px, AnyElement, AnyView, ClickEvent, MouseButton, MouseDownEvent, Pixels}; diff --git a/crates/ui/src/components/list/list_separator.rs b/crates/ui/src/components/list/list_separator.rs index 0d5fdf8d49..aa53efab37 100644 --- a/crates/ui/src/components/list/list_separator.rs +++ b/crates/ui/src/components/list/list_separator.rs @@ -1,3 +1,5 @@ +#![allow(missing_docs)] + use crate::prelude::*; #[derive(IntoElement)] diff --git a/crates/ui/src/components/list/list_sub_header.rs b/crates/ui/src/components/list/list_sub_header.rs index 0ed072ebbf..25132b7668 100644 --- a/crates/ui/src/components/list/list_sub_header.rs +++ b/crates/ui/src/components/list/list_sub_header.rs @@ -1,3 +1,5 @@ +#![allow(missing_docs)] + use crate::prelude::*; use crate::{h_flex, Icon, IconName, IconSize, Label}; diff --git a/crates/ui/src/components/modal.rs b/crates/ui/src/components/modal.rs index dec7a14a52..11611f9c0f 100644 --- a/crates/ui/src/components/modal.rs +++ b/crates/ui/src/components/modal.rs @@ -1,3 +1,5 @@ +#![allow(missing_docs)] + use crate::{ h_flex, v_flex, Clickable, Color, Headline, HeadlineSize, IconButton, IconButtonShape, IconName, Label, LabelCommon, LabelSize, Spacing, diff --git a/crates/ui/src/components/numeric_stepper.rs b/crates/ui/src/components/numeric_stepper.rs index 64ea79f21c..39be56a455 100644 --- a/crates/ui/src/components/numeric_stepper.rs +++ b/crates/ui/src/components/numeric_stepper.rs @@ -1,3 +1,5 @@ +#![allow(missing_docs)] + use gpui::ClickEvent; use crate::{prelude::*, IconButtonShape}; diff --git a/crates/ui/src/components/popover.rs b/crates/ui/src/components/popover.rs index af20f38186..5bd6c1ed7c 100644 --- a/crates/ui/src/components/popover.rs +++ b/crates/ui/src/components/popover.rs @@ -1,3 +1,5 @@ +#![allow(missing_docs)] + use crate::prelude::*; use crate::v_flex; use gpui::{ diff --git a/crates/ui/src/components/popover_menu.rs b/crates/ui/src/components/popover_menu.rs index 02604b2cc6..e05888bdf6 100644 --- a/crates/ui/src/components/popover_menu.rs +++ b/crates/ui/src/components/popover_menu.rs @@ -1,3 +1,5 @@ +#![allow(missing_docs)] + use std::{cell::RefCell, rc::Rc}; use gpui::{ diff --git a/crates/ui/src/components/radio.rs b/crates/ui/src/components/radio.rs index f3eeb9dac0..c4ceef5f83 100644 --- a/crates/ui/src/components/radio.rs +++ b/crates/ui/src/components/radio.rs @@ -1,3 +1,5 @@ +#![allow(missing_docs)] + use std::sync::Arc; use crate::prelude::*; diff --git a/crates/ui/src/components/right_click_menu.rs b/crates/ui/src/components/right_click_menu.rs index c340a4ad89..f68b85016c 100644 --- a/crates/ui/src/components/right_click_menu.rs +++ b/crates/ui/src/components/right_click_menu.rs @@ -1,3 +1,5 @@ +#![allow(missing_docs)] + use std::{cell::RefCell, rc::Rc}; use gpui::{ diff --git a/crates/ui/src/components/settings_container.rs b/crates/ui/src/components/settings_container.rs index df1c355e06..56248ce528 100644 --- a/crates/ui/src/components/settings_container.rs +++ b/crates/ui/src/components/settings_container.rs @@ -1,3 +1,5 @@ +#![allow(missing_docs)] + use gpui::AnyElement; use smallvec::SmallVec; diff --git a/crates/ui/src/components/settings_group.rs b/crates/ui/src/components/settings_group.rs index 6093964057..77b4416a4a 100644 --- a/crates/ui/src/components/settings_group.rs +++ b/crates/ui/src/components/settings_group.rs @@ -1,3 +1,5 @@ +#![allow(missing_docs)] + use gpui::AnyElement; use smallvec::SmallVec; diff --git a/crates/ui/src/components/stack.rs b/crates/ui/src/components/stack.rs index 74a5e80575..2af0a5d3f9 100644 --- a/crates/ui/src/components/stack.rs +++ b/crates/ui/src/components/stack.rs @@ -1,3 +1,5 @@ +#![allow(missing_docs)] + use gpui::{div, Div}; use crate::StyledExt; diff --git a/crates/ui/src/components/stories.rs b/crates/ui/src/components/stories.rs index 9014f5a470..88325f2201 100644 --- a/crates/ui/src/components/stories.rs +++ b/crates/ui/src/components/stories.rs @@ -1,3 +1,6 @@ +// We allow missing docs for stories as the docs will more or less be +// "This is the ___ story", which is not very useful. +#![allow(missing_docs)] mod avatar; mod button; mod checkbox; diff --git a/crates/ui/src/components/tab.rs b/crates/ui/src/components/tab.rs index 1ec4cd4aec..9540063c78 100644 --- a/crates/ui/src/components/tab.rs +++ b/crates/ui/src/components/tab.rs @@ -1,3 +1,4 @@ +#![allow(missing_docs)] use std::cmp::Ordering; use gpui::{AnyElement, IntoElement, Stateful}; diff --git a/crates/ui/src/components/tab_bar.rs b/crates/ui/src/components/tab_bar.rs index 0012c53ed8..5657721e17 100644 --- a/crates/ui/src/components/tab_bar.rs +++ b/crates/ui/src/components/tab_bar.rs @@ -1,3 +1,4 @@ +#![allow(missing_docs)] use gpui::{AnyElement, ScrollHandle}; use smallvec::SmallVec; diff --git a/crates/ui/src/components/tool_strip.rs b/crates/ui/src/components/tool_strip.rs index ced4b271ad..78767aadf7 100644 --- a/crates/ui/src/components/tool_strip.rs +++ b/crates/ui/src/components/tool_strip.rs @@ -1,3 +1,5 @@ +#![allow(missing_docs)] + use crate::prelude::*; use gpui::*; diff --git a/crates/ui/src/components/tooltip.rs b/crates/ui/src/components/tooltip.rs index dc02d60b36..89b89786b0 100644 --- a/crates/ui/src/components/tooltip.rs +++ b/crates/ui/src/components/tooltip.rs @@ -1,3 +1,5 @@ +#![allow(missing_docs)] + use gpui::{Action, AnyView, FocusHandle, IntoElement, Render, VisualContext}; use settings::Settings; use theme::ThemeSettings; diff --git a/crates/ui/src/key_bindings.rs b/crates/ui/src/key_bindings.rs deleted file mode 100644 index 7746d096fe..0000000000 --- a/crates/ui/src/key_bindings.rs +++ /dev/null @@ -1,170 +0,0 @@ -use gpui::{Action, FocusHandle, KeyBinding, Keystroke, WindowContext}; - -use crate::PlatformStyle; - -/// Returns a textual representation of the key binding for the given [`Action`]. -pub fn text_for_action(action: &dyn Action, cx: &mut WindowContext) -> Option { - let key_binding = cx.bindings_for_action(action).last().cloned()?; - Some(text_for_key_binding(key_binding, PlatformStyle::platform())) -} - -/// Returns a textual representation of the key binding for the given [`Action`] -/// as if the provided [`FocusHandle`] was focused. -pub fn text_for_action_in( - action: &dyn Action, - focus: &FocusHandle, - cx: &mut WindowContext, -) -> Option { - let key_binding = cx.bindings_for_action_in(action, focus).last().cloned()?; - Some(text_for_key_binding(key_binding, PlatformStyle::platform())) -} - -/// Returns a textual representation of the given key binding for the specified platform. -pub fn text_for_key_binding(key_binding: KeyBinding, platform_style: PlatformStyle) -> String { - key_binding - .keystrokes() - .iter() - .map(|keystroke| text_for_keystroke(keystroke, platform_style)) - .collect::>() - .join(" ") -} - -/// Returns a textual representation of the given [`Keystroke`]. -pub fn text_for_keystroke(keystroke: &Keystroke, platform_style: PlatformStyle) -> String { - let mut text = String::new(); - - let delimiter = match platform_style { - PlatformStyle::Mac => '-', - PlatformStyle::Linux | PlatformStyle::Windows => '+', - }; - - if keystroke.modifiers.function { - match platform_style { - PlatformStyle::Mac => text.push_str("fn"), - PlatformStyle::Linux | PlatformStyle::Windows => text.push_str("Fn"), - } - - text.push(delimiter); - } - - if keystroke.modifiers.control { - match platform_style { - PlatformStyle::Mac => text.push_str("Control"), - PlatformStyle::Linux | PlatformStyle::Windows => text.push_str("Ctrl"), - } - - text.push(delimiter); - } - - if keystroke.modifiers.alt { - match platform_style { - PlatformStyle::Mac => text.push_str("Option"), - PlatformStyle::Linux | PlatformStyle::Windows => text.push_str("Alt"), - } - - text.push(delimiter); - } - - if keystroke.modifiers.platform { - match platform_style { - PlatformStyle::Mac => text.push_str("Command"), - PlatformStyle::Linux => text.push_str("Super"), - PlatformStyle::Windows => text.push_str("Win"), - } - - text.push(delimiter); - } - - if keystroke.modifiers.shift { - match platform_style { - PlatformStyle::Mac | PlatformStyle::Linux | PlatformStyle::Windows => { - text.push_str("Shift") - } - } - - text.push(delimiter); - } - - fn capitalize(str: &str) -> String { - let mut chars = str.chars(); - match chars.next() { - None => String::new(), - Some(first_char) => first_char.to_uppercase().collect::() + chars.as_str(), - } - } - - let key = match keystroke.key.as_str() { - "pageup" => "PageUp", - "pagedown" => "PageDown", - key => &capitalize(key), - }; - - text.push_str(key); - - text -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_text_for_keystroke() { - assert_eq!( - text_for_keystroke(&Keystroke::parse("cmd-c").unwrap(), PlatformStyle::Mac), - "Command-C".to_string() - ); - assert_eq!( - text_for_keystroke(&Keystroke::parse("cmd-c").unwrap(), PlatformStyle::Linux), - "Super+C".to_string() - ); - assert_eq!( - text_for_keystroke(&Keystroke::parse("cmd-c").unwrap(), PlatformStyle::Windows), - "Win+C".to_string() - ); - - assert_eq!( - text_for_keystroke( - &Keystroke::parse("ctrl-alt-delete").unwrap(), - PlatformStyle::Mac - ), - "Control-Option-Delete".to_string() - ); - assert_eq!( - text_for_keystroke( - &Keystroke::parse("ctrl-alt-delete").unwrap(), - PlatformStyle::Linux - ), - "Ctrl+Alt+Delete".to_string() - ); - assert_eq!( - text_for_keystroke( - &Keystroke::parse("ctrl-alt-delete").unwrap(), - PlatformStyle::Windows - ), - "Ctrl+Alt+Delete".to_string() - ); - - assert_eq!( - text_for_keystroke( - &Keystroke::parse("shift-pageup").unwrap(), - PlatformStyle::Mac - ), - "Shift-PageUp".to_string() - ); - assert_eq!( - text_for_keystroke( - &Keystroke::parse("shift-pageup").unwrap(), - PlatformStyle::Linux - ), - "Shift+PageUp".to_string() - ); - assert_eq!( - text_for_keystroke( - &Keystroke::parse("shift-pageup").unwrap(), - PlatformStyle::Windows - ), - "Shift+PageUp".to_string() - ); - } -} diff --git a/crates/ui/src/prelude.rs b/crates/ui/src/prelude.rs index 895c755c78..d01ac11bc3 100644 --- a/crates/ui/src/prelude.rs +++ b/crates/ui/src/prelude.rs @@ -7,16 +7,17 @@ pub use gpui::{ WindowContext, }; -pub use crate::clickable::*; -pub use crate::disableable::*; -pub use crate::fixed::*; -pub use crate::selectable::*; pub use crate::styles::{rems_from_px, vh, vw, PlatformStyle, StyledTypography, TextSize}; -pub use crate::visible_on_hover::*; +pub use crate::traits::clickable::*; +pub use crate::traits::disableable::*; +pub use crate::traits::fixed::*; +pub use crate::traits::selectable::*; +pub use crate::traits::styled_ext::*; +pub use crate::traits::visible_on_hover::*; pub use crate::Spacing; pub use crate::{h_flex, v_flex}; pub use crate::{Button, ButtonSize, ButtonStyle, IconButton, SelectableButton}; -pub use crate::{ButtonCommon, Color, StyledExt}; +pub use crate::{ButtonCommon, Color}; pub use crate::{Headline, HeadlineSize}; pub use crate::{Icon, IconName, IconPosition, IconSize}; pub use crate::{Label, LabelCommon, LabelSize, LineHeightStyle}; diff --git a/crates/ui/src/styles/appearance.rs b/crates/ui/src/styles/appearance.rs index 57d4757a36..3b014b733c 100644 --- a/crates/ui/src/styles/appearance.rs +++ b/crates/ui/src/styles/appearance.rs @@ -1,14 +1,8 @@ use crate::prelude::*; use gpui::{WindowBackgroundAppearance, WindowContext}; -use theme::Appearance; - -/// Returns the current [Appearance]. -pub fn appearance(cx: &WindowContext) -> Appearance { - cx.theme().appearance -} /// Returns the [WindowBackgroundAppearance]. -pub fn window_appearance(cx: &WindowContext) -> WindowBackgroundAppearance { +fn window_appearance(cx: &WindowContext) -> WindowBackgroundAppearance { cx.theme().styles.window_background_appearance } diff --git a/crates/ui/src/styles/color.rs b/crates/ui/src/styles/color.rs index b35728e478..fe8de2ff73 100644 --- a/crates/ui/src/styles/color.rs +++ b/crates/ui/src/styles/color.rs @@ -5,28 +5,63 @@ use theme::ActiveTheme; #[derive(Debug, Default, PartialEq, Copy, Clone)] pub enum Color { #[default] + /// The default text color. Might be known as "foreground" or "primary" in + /// some theme systems. + /// + /// For less emphasis, consider using [`Color::Muted`] or [`Color::Hidden`]. Default, + /// A text color used for accents, such as links or highlights. Accent, - Created, - Deleted, - Disabled, - Error, - Hidden, - Hint, - Info, - Modified, + /// A color used to indicate a conflict, such as a version control merge conflict, or a conflict between a file in the editor and the file system. Conflict, - Ignored, - Muted, - Placeholder, - Player(u32), - Selected, - Success, - Warning, + /// A color used to indicate a newly created item, such as a new file in + /// version control, or a new file on disk. + Created, + /// It is highly, HIGHLY recommended not to use this! Using this color + /// means detaching it from any semantic meaning across themes. + /// + /// A custom color specified by an HSLA value. Custom(Hsla), + /// A color used to indicate a deleted item, such as a file removed from version control. + Deleted, + /// A color used for disabled UI elements or text, like a disabled button or menu item. + Disabled, + /// A color used to indicate an error condition, or something the user + /// cannot do. In very rare cases, it might be used to indicate dangerous or + /// destructive action. + Error, + /// A color used for elements that represent something that is hidden, like + /// a hidden file, or an element that should be visually de-emphasized. + Hidden, + /// A color used for hint or suggestion text, often a blue color. Use this + /// color to represent helpful, or semantically neutral information. + Hint, + /// A color used for items that are intentionally ignored, such as files ignored by version control. + Ignored, + /// A color used for informational messages or status indicators, often a blue color. + Info, + /// A color used to indicate a modified item, such as an edited file, or a modified entry in version control. + Modified, + /// A color used for text or UI elements that should be visually muted or de-emphasized. + /// + /// For more emphasis, consider using [`Color::Default`]. + /// + /// For less emphasis, consider using [`Color::Hidden`]. + Muted, + /// A color used for placeholder text in input fields. + Placeholder, + /// A color associated with a specific player number. + Player(u32), + /// A color used to indicate selected text or UI elements. + Selected, + /// A color used to indicate a successful operation or status. + Success, + /// A color used to indicate a warning condition. + Warning, } impl Color { + /// Returns the Color's HSLA value. pub fn color(&self, cx: &WindowContext) -> Hsla { match self { Color::Default => cx.theme().colors().text, diff --git a/crates/ui/src/styles/docs/elevation.md b/crates/ui/src/styles/docs/elevation.md deleted file mode 100644 index 677fd030fd..0000000000 --- a/crates/ui/src/styles/docs/elevation.md +++ /dev/null @@ -1,44 +0,0 @@ -# Elevation - -Elevation can be thought of as the physical closeness of an element to the user. Elements with lower elevations are physically further away from the user on the z-axis and appear to be underneath elements with higher elevations. - -Material Design 3 has a some great visualizations of elevation that may be helpful to understanding the mental modal of elevation. [Material Design – Elevation](https://m3.material.io/styles/elevation/overview) - -## Elevation Levels - -1. App Background (e.x.: Workspace, system window) -1. UI Surface (e.x.: Title Bar, Panel, Tab Bar) -1. Elevated Surface (e.x.: Palette, Notification, Floating Window) -1. Wash -1. Modal Surfaces (e.x.: Modal) -1. Dragged Element (This is a special case, see Layer section below) - -### App Background - -The app background constitutes the lowest elevation layer, appearing behind all other surfaces and components. It is predominantly used for the background color of the app. - -### Surface - -The Surface elevation level, located above the app background, is the standard level for all elements - -Example Elements: Title Bar, Panel, Tab Bar, Editor - -### Elevated Surface - -Non-Modal Elevated Surfaces appear above the UI surface layer and is used for things that should appear above most UI elements like an editor or panel, but not elements like popovers, context menus, modals, etc. - -Examples: Notifications, Palettes, Detached/Floating Windows, Detached/Floating Panels - -You could imagine a variant of the assistant that floats in a window above the editor on this elevation, or a floating terminal window that becomes less opaque when not focused. - -### Wash - -Wash denotes a distinct elevation reserved to isolate app UI layers from high elevation components such as modals, notifications, and overlaid panels. The wash may not consistently be visible when these components are active. This layer is often referred to as a scrim or overlay and the background color of the wash is typically deployed in its design. - -### Modal Surfaces - -Modal Surfaces are used for elements that should appear above all other UI elements and are located above the wash layer. This is the maximum elevation at which UI elements can be rendered - -Elements rendered at this layer have an enforced behavior: Any interaction outside of the modal will either dismiss the modal or prompt an action (Save your progress, etc) then dismiss the modal. - -If the element does not have this behavior, it should be rendered at the Elevated Surface layer. diff --git a/crates/ui/src/styles/elevation.rs b/crates/ui/src/styles/elevation.rs index c067802119..722111b46c 100644 --- a/crates/ui/src/styles/elevation.rs +++ b/crates/ui/src/styles/elevation.rs @@ -1,31 +1,32 @@ use gpui::{hsla, point, px, BoxShadow}; use smallvec::{smallvec, SmallVec}; -#[doc = include_str!("docs/elevation.md")] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Elevation { - ElevationIndex(ElevationIndex), - LayerIndex(LayerIndex), - ElementIndex(ElementIndex), -} - -impl From for Elevation { - fn from(val: ElevationIndex) -> Self { - Elevation::ElevationIndex(val) - } -} - +/// Today, elevation is primarily used to add shadows to elements, and set the correct background for elements like buttons. +/// +/// Elevation can be thought of as the physical closeness of an element to the +/// user. Elements with lower elevations are physically further away on the +/// z-axis and appear to be underneath elements with higher elevations. +/// +/// In the future, a more complete approach to elevation may be added. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ElevationIndex { + /// On the layer of the app background. This is under panels, panes, and + /// other surfaces. Background, + /// The primary surface – Contains panels, panes, containers, etc. Surface, + /// A surface that is elevated above the primary surface. but below washes, models, and dragged elements. ElevatedSurface, + /// A surface that is above all non-modal surfaces, and separates the app from focused intents, like dialogs, alerts, modals, etc. Wash, + /// A surface above the [ElevationIndex::Wash] that is used for dialogs, alerts, modals, etc. ModalSurface, + /// A surface above all other surfaces, reserved exclusively for dragged elements, like a dragged file, tab or other draggable element. DraggedElement, } impl ElevationIndex { + /// Returns an appropriate shadow for the given elevation index. pub fn shadow(self) -> SmallVec<[BoxShadow; 2]> { match self { ElevationIndex::Surface => smallvec![], @@ -62,21 +63,3 @@ impl ElevationIndex { } } } - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum LayerIndex { - BehindElement, - Element, - ElevatedElement, -} - -/// An appropriate z-index for the given layer based on its intended usage. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ElementIndex { - Effect, - Background, - Tint, - Highlight, - Content, - Overlay, -} diff --git a/crates/ui/src/styles/spacing.rs b/crates/ui/src/styles/spacing.rs index 282980fcaf..a2089d9586 100644 --- a/crates/ui/src/styles/spacing.rs +++ b/crates/ui/src/styles/spacing.rs @@ -4,6 +4,11 @@ use theme::{ThemeSettings, UiDensity}; use crate::{rems_from_px, BASE_REM_SIZE_IN_PX}; +/// A dynamic spacing system that adjusts spacing based on +/// [UiDensity]. +/// +/// When possible, [Spacing] should be used over manual +/// or built-in spacing values in places dynamic spacing is needed. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum Spacing { /// No spacing @@ -38,6 +43,7 @@ pub enum Spacing { } impl Spacing { + /// Returns the spacing's scaling ratio in pixels. pub fn spacing_ratio(self, cx: &WindowContext) -> f32 { match ThemeSettings::get_global(cx).ui_density { UiDensity::Compact => match self { @@ -73,10 +79,12 @@ impl Spacing { } } + /// Returns the spacing's value in rems. pub fn rems(self, cx: &WindowContext) -> Rems { rems(self.spacing_ratio(cx)) } + /// Returns the spacing's value in pixels. pub fn px(self, cx: &WindowContext) -> Pixels { let ui_font_size_f32: f32 = ThemeSettings::get_global(cx).ui_font_size.into(); @@ -84,10 +92,14 @@ impl Spacing { } } -pub fn user_spacing_style(cx: &WindowContext) -> UiDensity { +fn user_spacing_style(cx: &WindowContext) -> UiDensity { ThemeSettings::get_global(cx).ui_density } +/// Returns a custom spacing value based on the current [`UiDensity`]. +/// +/// If you use this, talk to @iamnbutler and let me know what you're doing +/// that needs custom spacing– I'd love to understand so we can extend the system further and remove the need for this. pub fn custom_spacing(cx: &WindowContext, size: f32) -> Rems { rems_from_px(size * user_spacing_style(cx).spacing_ratio()) } diff --git a/crates/ui/src/styles/typography.rs b/crates/ui/src/styles/typography.rs index 4afd3b9303..ef9c946ed5 100644 --- a/crates/ui/src/styles/typography.rs +++ b/crates/ui/src/styles/typography.rs @@ -87,6 +87,7 @@ pub trait StyledTypography: Styled + Sized { impl StyledTypography for E {} +/// A utility for getting the size of various semantic text sizes. #[derive(Debug, Default, Clone)] pub enum TextSize { /// The default size for UI text. @@ -128,6 +129,7 @@ pub enum TextSize { } impl TextSize { + /// Returns the text size in rems. pub fn rems(self, cx: &WindowContext) -> Rems { let theme_settings = ThemeSettings::get_global(cx); @@ -143,20 +145,27 @@ impl TextSize { } /// The size of a [`Headline`] element +/// +/// Defaults to a Major Second scale. #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)] pub enum HeadlineSize { + /// An extra small headline - `~14px` @16px/rem XSmall, + /// A small headline - `16px` @16px/rem Small, #[default] + /// A medium headline - `~18px` @16px/rem Medium, + /// A large headline - `~20px` @16px/rem Large, + /// An extra large headline - `~22px` @16px/rem XLarge, } impl HeadlineSize { - pub fn size(self) -> Rems { + /// Returns the headline size in rems. + pub fn rems(self) -> Rems { match self { - // Based on the Major Second scale Self::XSmall => rems(0.88), Self::Small => rems(1.0), Self::Medium => rems(1.125), @@ -165,6 +174,7 @@ impl HeadlineSize { } } + /// Returns the line height for the headline size. pub fn line_height(self) -> Rems { match self { Self::XSmall => rems(1.6), @@ -176,6 +186,8 @@ impl HeadlineSize { } } +/// A headline element, used to emphasize some text and +/// create a visual hierarchy. #[derive(IntoElement)] pub struct Headline { size: HeadlineSize, @@ -190,13 +202,14 @@ impl RenderOnce for Headline { div() .font(ui_font) .line_height(self.size.line_height()) - .text_size(self.size.size()) + .text_size(self.size.rems()) .text_color(cx.theme().colors().text) .child(self.text) } } impl Headline { + /// Create a new headline element. pub fn new(text: impl Into) -> Self { Self { size: HeadlineSize::default(), @@ -205,11 +218,13 @@ impl Headline { } } + /// Set the size of the headline. pub fn size(mut self, size: HeadlineSize) -> Self { self.size = size; self } + /// Set the color of the headline. pub fn color(mut self, color: Color) -> Self { self.color = color; self diff --git a/crates/ui/src/tests.rs b/crates/ui/src/tests.rs new file mode 100644 index 0000000000..3f26326a05 --- /dev/null +++ b/crates/ui/src/tests.rs @@ -0,0 +1 @@ +mod path_str; diff --git a/crates/ui/src/path_str.rs b/crates/ui/src/tests/path_str.rs similarity index 54% rename from crates/ui/src/path_str.rs rename to crates/ui/src/tests/path_str.rs index 2ebb3fedb3..6c1545ec71 100644 --- a/crates/ui/src/path_str.rs +++ b/crates/ui/src/tests/path_str.rs @@ -1,3 +1,5 @@ +// We need to test [ui_macros::DerivePathStr] here as we can't invoke it +// in the `ui_macros` crate. #[cfg(test)] mod tests { use strum::EnumString; @@ -8,26 +10,26 @@ mod tests { #[derive(Debug, EnumString, DerivePathStr)] #[strum(serialize_all = "snake_case")] #[path_str(prefix = "test_prefix")] - enum MyEnum { + enum SomeAsset { FooBar, Baz, } - assert_eq!(MyEnum::FooBar.path(), "test_prefix/foo_bar"); - assert_eq!(MyEnum::Baz.path(), "test_prefix/baz"); + assert_eq!(SomeAsset::FooBar.path(), "test_prefix/foo_bar"); + assert_eq!(SomeAsset::Baz.path(), "test_prefix/baz"); } #[test] fn test_derive_path_str_with_prefix_and_suffix() { #[derive(Debug, EnumString, DerivePathStr)] #[strum(serialize_all = "snake_case")] - #[path_str(prefix = "test_prefix", suffix = ".txt")] - enum MyEnum { + #[path_str(prefix = "test_prefix", suffix = ".svg")] + enum SomeAsset { FooBar, Baz, } - assert_eq!(MyEnum::FooBar.path(), "test_prefix/foo_bar.txt"); - assert_eq!(MyEnum::Baz.path(), "test_prefix/baz.txt"); + assert_eq!(SomeAsset::FooBar.path(), "test_prefix/foo_bar.svg"); + assert_eq!(SomeAsset::Baz.path(), "test_prefix/baz.svg"); } } diff --git a/crates/ui/src/traits.rs b/crates/ui/src/traits.rs new file mode 100644 index 0000000000..7e52f2a867 --- /dev/null +++ b/crates/ui/src/traits.rs @@ -0,0 +1,6 @@ +pub mod clickable; +pub mod disableable; +pub mod fixed; +pub mod selectable; +pub mod styled_ext; +pub mod visible_on_hover; diff --git a/crates/ui/src/clickable.rs b/crates/ui/src/traits/clickable.rs similarity index 100% rename from crates/ui/src/clickable.rs rename to crates/ui/src/traits/clickable.rs diff --git a/crates/ui/src/disableable.rs b/crates/ui/src/traits/disableable.rs similarity index 100% rename from crates/ui/src/disableable.rs rename to crates/ui/src/traits/disableable.rs diff --git a/crates/ui/src/fixed.rs b/crates/ui/src/traits/fixed.rs similarity index 100% rename from crates/ui/src/fixed.rs rename to crates/ui/src/traits/fixed.rs diff --git a/crates/ui/src/selectable.rs b/crates/ui/src/traits/selectable.rs similarity index 100% rename from crates/ui/src/selectable.rs rename to crates/ui/src/traits/selectable.rs diff --git a/crates/ui/src/styled_ext.rs b/crates/ui/src/traits/styled_ext.rs similarity index 100% rename from crates/ui/src/styled_ext.rs rename to crates/ui/src/traits/styled_ext.rs diff --git a/crates/ui/src/visible_on_hover.rs b/crates/ui/src/traits/visible_on_hover.rs similarity index 85% rename from crates/ui/src/visible_on_hover.rs rename to crates/ui/src/traits/visible_on_hover.rs index aefa7ac10c..fc0bb837d7 100644 --- a/crates/ui/src/visible_on_hover.rs +++ b/crates/ui/src/traits/visible_on_hover.rs @@ -1,5 +1,7 @@ use gpui::{InteractiveElement, SharedString, Styled}; +/// A trait for elements that can be made visible on hover by +/// tracking a specific group. pub trait VisibleOnHover { /// Sets the element to only be visible when the specified group is hovered. /// diff --git a/crates/ui/src/ui.rs b/crates/ui/src/ui.rs index 4f5d6314be..101de4f3ef 100644 --- a/crates/ui/src/ui.rs +++ b/crates/ui/src/ui.rs @@ -1,28 +1,22 @@ +#![deny(missing_docs)] + //! # UI – Zed UI Primitives & Components //! //! This crate provides a set of UI primitives and components that are used to build all of the elements in Zed's UI. //! +//! ## Related Crates: +//! +//! - [`ui_macros`] - proc_macros support for this crate +//! - [`ui_input`] - the single line input component +//! -mod clickable; mod components; -mod disableable; -mod fixed; -mod key_bindings; -mod path_str; pub mod prelude; -mod selectable; -mod styled_ext; mod styles; +mod tests; +mod traits; pub mod utils; -mod visible_on_hover; -mod with_rem_size; -pub use clickable::*; pub use components::*; -pub use disableable::*; -pub use fixed::*; -pub use key_bindings::*; pub use prelude::*; -pub use styled_ext::*; pub use styles::*; -pub use with_rem_size::*; diff --git a/crates/ui/src/utils.rs b/crates/ui/src/utils.rs index ed1fec690f..b68d6e6bbd 100644 --- a/crates/ui/src/utils.rs +++ b/crates/ui/src/utils.rs @@ -1,5 +1,7 @@ -//! UI-related utilities (e.g. converting dates to a human-readable form). +//! UI-related utilities mod format_distance; +mod with_rem_size; pub use format_distance::*; +pub use with_rem_size::*; diff --git a/crates/ui/src/utils/format_distance.rs b/crates/ui/src/utils/format_distance.rs index 2abcee6684..af4f0d9505 100644 --- a/crates/ui/src/utils/format_distance.rs +++ b/crates/ui/src/utils/format_distance.rs @@ -1,3 +1,6 @@ +// This won't be documented further as it is intended to be removed, or merged with the `time_format` crate. +#![allow(missing_docs)] + use chrono::{DateTime, Local, NaiveDateTime}; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] diff --git a/crates/ui/src/with_rem_size.rs b/crates/ui/src/utils/with_rem_size.rs similarity index 95% rename from crates/ui/src/with_rem_size.rs rename to crates/ui/src/utils/with_rem_size.rs index f286177efb..9011d1e28e 100644 --- a/crates/ui/src/with_rem_size.rs +++ b/crates/ui/src/utils/with_rem_size.rs @@ -10,6 +10,8 @@ pub struct WithRemSize { } impl WithRemSize { + /// Create a new [WithRemSize] element, which sets a + /// particular rem size for its children. pub fn new(rem_size: impl Into) -> Self { Self { div: div(), From 200b2bf70a548b2eec75897220d3f3e82691b499 Mon Sep 17 00:00:00 2001 From: Roman Zipp Date: Sun, 6 Oct 2024 16:11:21 +0200 Subject: [PATCH 11/28] php: Add syntax highlighting for Intelephense completions (#18774) Release Notes: - N/A This PR introduces syntax highlighting for intelephense autocomple. The styling was selected to roughly match PHPStorm's default scheme. Please note that I'm not very familiar with writing Rust, but I'm happy to adapt to any requested changes! ## Examples ### Object attributes, methods and constants ![Screenshot 2024-10-06 at 13 38 03](https://github.com/user-attachments/assets/a91634ff-0f2e-41f0-b548-ecb09c40947c) ![Screenshot 2024-10-06 at 13 38 11](https://github.com/user-attachments/assets/b6f179f4-898b-4d82-9d36-a3e82328325c) ### Typed enum members ![Screenshot 2024-10-06 at 13 38 53](https://github.com/user-attachments/assets/7133b981-4f68-4210-b233-403cdf3ec9bb) ![Screenshot 2024-10-06 at 13 38 41](https://github.com/user-attachments/assets/2e806f3d-3538-45f2-b075-b8be5902b786) ### Variables Includes altered highlighting for [reserved variable names](https://www.php.net/manual/en/reserved.variables.php). ![Screenshot 2024-10-06 at 13 39 30](https://github.com/user-attachments/assets/be426eb8-5879-432d-b302-391c2c68a7cb) --------- Co-authored-by: Marshall Bowers --- .../php/src/language_servers/intelephense.rs | 102 ++++++++++++++++++ extensions/php/src/php.rs | 14 +++ 2 files changed, 116 insertions(+) diff --git a/extensions/php/src/language_servers/intelephense.rs b/extensions/php/src/language_servers/intelephense.rs index 7bd66b24ab..23f47ac5c0 100644 --- a/extensions/php/src/language_servers/intelephense.rs +++ b/extensions/php/src/language_servers/intelephense.rs @@ -1,5 +1,6 @@ use std::{env, fs}; +use zed::{CodeLabel, CodeLabelSpan}; use zed_extension_api::settings::LspSettings; use zed_extension_api::{self as zed, serde_json, LanguageServerId, Result}; @@ -104,4 +105,105 @@ impl Intelephense { "intelephense": settings }))) } + + pub fn label_for_completion(&self, completion: zed::lsp::Completion) -> Option { + let label = &completion.label; + + match completion.kind? { + zed::lsp::CompletionKind::Method => { + // __construct method doesn't have a detail + if let Some(ref detail) = completion.detail { + if detail.is_empty() { + return Some(CodeLabel { + spans: vec![ + CodeLabelSpan::literal(label, Some("function.method".to_string())), + CodeLabelSpan::literal("()", None), + ], + filter_range: (0..label.len()).into(), + code: completion.label, + }); + } + } + + let mut parts = completion.detail.as_ref()?.split(":"); + // E.g., `foo(string $var)` + let name_and_params = parts.next()?; + let return_type = parts.next()?.trim(); + + let (_, params) = name_and_params.split_once("(")?; + let params = params.trim_end_matches(")"); + + Some(CodeLabel { + spans: vec![ + CodeLabelSpan::literal(label, Some("function.method".to_string())), + CodeLabelSpan::literal("(", None), + CodeLabelSpan::literal(params, Some("comment".to_string())), + CodeLabelSpan::literal("): ", None), + CodeLabelSpan::literal(return_type, Some("type".to_string())), + ], + filter_range: (0..label.len()).into(), + code: completion.label, + }) + } + zed::lsp::CompletionKind::Constant | zed::lsp::CompletionKind::EnumMember => { + if let Some(ref detail) = completion.detail { + if !detail.is_empty() { + return Some(CodeLabel { + spans: vec![ + CodeLabelSpan::literal(label, Some("constant".to_string())), + CodeLabelSpan::literal(" ", None), + CodeLabelSpan::literal(detail, Some("comment".to_string())), + ], + filter_range: (0..label.len()).into(), + code: completion.label, + }); + } + } + + Some(CodeLabel { + spans: vec![CodeLabelSpan::literal(label, Some("constant".to_string()))], + filter_range: (0..label.len()).into(), + code: completion.label, + }) + } + zed::lsp::CompletionKind::Property => { + let return_type = completion.detail?; + Some(CodeLabel { + spans: vec![ + CodeLabelSpan::literal(label, Some("attribute".to_string())), + CodeLabelSpan::literal(": ", None), + CodeLabelSpan::literal(return_type, Some("type".to_string())), + ], + filter_range: (0..label.len()).into(), + code: completion.label, + }) + } + zed::lsp::CompletionKind::Variable => { + // See https://www.php.net/manual/en/reserved.variables.php + const SYSTEM_VAR_NAMES: &[&str] = + &["argc", "argv", "php_errormsg", "http_response_header"]; + + let var_name = completion.label.trim_start_matches("$"); + let is_uppercase = var_name + .chars() + .filter(|c| c.is_alphabetic()) + .all(|c| c.is_uppercase()); + let is_system_constant = var_name.starts_with("_"); + let is_reserved = SYSTEM_VAR_NAMES.contains(&var_name); + + let highlight = if is_uppercase || is_system_constant || is_reserved { + Some("comment".to_string()) + } else { + None + }; + + Some(CodeLabel { + spans: vec![CodeLabelSpan::literal(label, highlight)], + filter_range: (0..label.len()).into(), + code: completion.label, + }) + } + _ => None, + } + } } diff --git a/extensions/php/src/php.rs b/extensions/php/src/php.rs index 7157bef074..53b4c29951 100644 --- a/extensions/php/src/php.rs +++ b/extensions/php/src/php.rs @@ -1,5 +1,6 @@ mod language_servers; +use zed::CodeLabel; use zed_extension_api::{self as zed, serde_json, LanguageServerId, Result}; use crate::language_servers::{Intelephense, Phpactor}; @@ -53,6 +54,19 @@ impl zed::Extension for PhpExtension { Ok(None) } + + fn label_for_completion( + &self, + language_server_id: &zed::LanguageServerId, + completion: zed::lsp::Completion, + ) -> Option { + match language_server_id.as_ref() { + Intelephense::LANGUAGE_SERVER_ID => { + self.intelephense.as_ref()?.label_for_completion(completion) + } + _ => None, + } + } } zed::register_extension!(PhpExtension); From 06bd2431d2652d7cc5950ee3b823a6ad5bd28ce5 Mon Sep 17 00:00:00 2001 From: Peter Date: Sun, 6 Oct 2024 16:12:06 +0200 Subject: [PATCH 12/28] proto: Add language server support (#18763) Closes #18762 Release Notes: - N/A --------- Co-authored-by: Marshall Bowers --- Cargo.lock | 7 ++++ Cargo.toml | 1 + extensions/proto/Cargo.toml | 16 +++++++++ extensions/proto/LICENSE-APACHE | 1 + extensions/proto/extension.toml | 4 +++ extensions/proto/src/proto.rs | 64 +++++++++++++++++++++++++++++++++ 6 files changed, 93 insertions(+) create mode 100644 extensions/proto/Cargo.toml create mode 120000 extensions/proto/LICENSE-APACHE create mode 100644 extensions/proto/src/proto.rs diff --git a/Cargo.lock b/Cargo.lock index 9945b2df9a..120f51ec93 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14863,6 +14863,13 @@ dependencies = [ "zed_extension_api 0.1.0", ] +[[package]] +name = "zed_proto" +version = "0.1.0" +dependencies = [ + "zed_extension_api 0.1.0", +] + [[package]] name = "zed_purescript" version = "0.0.1" diff --git a/Cargo.toml b/Cargo.toml index d6f4279449..6fc22029e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -154,6 +154,7 @@ members = [ "extensions/php", "extensions/perplexity", "extensions/prisma", + "extensions/proto", "extensions/purescript", "extensions/ruff", "extensions/ruby", diff --git a/extensions/proto/Cargo.toml b/extensions/proto/Cargo.toml new file mode 100644 index 0000000000..496099c526 --- /dev/null +++ b/extensions/proto/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "zed_proto" +version = "0.1.0" +edition = "2021" +publish = false +license = "Apache-2.0" + +[lints] +workspace = true + +[lib] +path = "src/proto.rs" +crate-type = ["cdylib"] + +[dependencies] +zed_extension_api = "0.1.0" diff --git a/extensions/proto/LICENSE-APACHE b/extensions/proto/LICENSE-APACHE new file mode 120000 index 0000000000..1cd601d0a3 --- /dev/null +++ b/extensions/proto/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/extensions/proto/extension.toml b/extensions/proto/extension.toml index a49ba7a4c4..6ecf9bf33f 100644 --- a/extensions/proto/extension.toml +++ b/extensions/proto/extension.toml @@ -9,3 +9,7 @@ repository = "https://github.com/zed-industries/zed" [grammars.proto] repository = "https://github.com/zed-industries/tree-sitter-proto" commit = "0848bd30a64be48772e15fbb9d5ba8c0cc5772ad" + +[language_servers.protobuf-language-server] +name = "Protobuf Language Server" +languages = ["Proto"] diff --git a/extensions/proto/src/proto.rs b/extensions/proto/src/proto.rs new file mode 100644 index 0000000000..c692a09327 --- /dev/null +++ b/extensions/proto/src/proto.rs @@ -0,0 +1,64 @@ +use zed_extension_api::{self as zed, settings::LspSettings, Result}; + +const PROTOBUF_LANGUAGE_SERVER_NAME: &str = "protobuf-language-server"; + +struct ProtobufLanguageServerBinary { + path: String, + args: Option>, +} + +struct ProtobufExtension; + +impl ProtobufExtension { + fn language_server_binary( + &self, + _language_server_id: &zed::LanguageServerId, + worktree: &zed::Worktree, + ) -> Result { + let binary_settings = LspSettings::for_worktree("protobuf-language-server", 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(ProtobufLanguageServerBinary { + path, + args: binary_args, + }); + } + + if let Some(path) = worktree.which(PROTOBUF_LANGUAGE_SERVER_NAME) { + return Ok(ProtobufLanguageServerBinary { + path, + args: binary_args, + }); + } + + Err(format!("{PROTOBUF_LANGUAGE_SERVER_NAME} not found in PATH",)) + } +} + +impl zed::Extension for ProtobufExtension { + fn new() -> Self { + Self + } + + fn language_server_command( + &mut self, + language_server_id: &zed_extension_api::LanguageServerId, + worktree: &zed_extension_api::Worktree, + ) -> zed_extension_api::Result { + let binary = self.language_server_binary(language_server_id, worktree)?; + Ok(zed::Command { + command: binary.path, + args: binary + .args + .unwrap_or_else(|| vec!["-logs".into(), "".into()]), + env: Default::default(), + }) + } +} + +zed::register_extension!(ProtobufExtension); From 1b06c70a76c5fa1ac65073b35d2e8a335757dfb4 Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Sun, 6 Oct 2024 10:26:26 -0400 Subject: [PATCH 13/28] Fix alt-t context (#18783) - Fix incorrect context introduced in https://github.com/zed-industries/zed/pull/18749/ Release Notes: - N/A --- assets/keymaps/default-macos.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 234485bc38..de929b8dd1 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -444,7 +444,7 @@ } }, { - "context": "Editor && !Terminal", + "context": "Workspace && !Terminal", "bindings": { "alt-t": "task::Rerun", "alt-shift-t": "task::Spawn" From bd746145b05a5bf7f5963ae01b40473a584c4632 Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Sun, 6 Oct 2024 10:28:39 -0400 Subject: [PATCH 14/28] ci: Make docs-only PRs only trigger docs-related tests (#18744) This should speed up any docs-only PRs so that they don't have to run the full 5 minute battery of tests. Release Notes: - N/A --- .github/workflows/ci.yml | 4 ++++ .github/workflows/docs.yml | 7 +++++-- script/check-spelling | 5 ++++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ef1570fbf2..6718b91ade 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,9 +7,13 @@ on: - "v[0-9]+.[0-9]+.x" tags: - "v*" + paths-ignore: + - "docs/**" pull_request: branches: - "**" + paths-ignore: + - "docs/**" concurrency: # Allow only one workflow per any non-`main` branch. diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 13fe0411a0..7d7b564a54 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -20,11 +20,14 @@ jobs: with: version: 9 - - run: | + - name: Prettier Check on /docs + working-directory: ./docs + run: | pnpm dlx prettier . --check || { echo "To fix, run from the root of the zed repo:" echo " cd docs && pnpm dlx prettier . --write && cd .." false } - working-directory: ./docs + - name: Check spelling + run: script/check-spelling docs/ diff --git a/script/check-spelling b/script/check-spelling index 65c674057f..76ebf74728 100755 --- a/script/check-spelling +++ b/script/check-spelling @@ -1,6 +1,9 @@ #!/bin/sh +set -eu + TYPOS_CLI_VERSION=1.24.6 +TARGET_DIR=${1:-""} if ! cargo install --list | grep "typos-cli v$TYPOS_CLI_VERSION" > /dev/null; then echo "Installing typos-cli@$TYPOS_CLI_VERSION..." @@ -8,4 +11,4 @@ if ! cargo install --list | grep "typos-cli v$TYPOS_CLI_VERSION" > /dev/null; th else echo "typos-cli@$TYPOS_CLI_VERSION is already installed." fi -typos +typos $TARGET_DIR From 59f0f4ac4260d7e7bd4e901fdd8970caf960d9a5 Mon Sep 17 00:00:00 2001 From: Agustin Gomes Date: Sun, 6 Oct 2024 20:47:48 +0200 Subject: [PATCH 15/28] Fix script/linux on RHEL/Fedora (#18788) - Add missing `/etc/os-release` from a grep call - Remove typo `grep grep` from another. Co-authored-by: Peter Tripp --- script/linux | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/script/linux b/script/linux index 540bf063ec..c69cd5a842 100755 --- a/script/linux +++ b/script/linux @@ -89,7 +89,7 @@ if [[ -n $dnf ]] || [[ -n $yum ]]; then perl-File-Copy mold ) - elif grep grep -qP '^ID="(rhel|rocky|alma|centos|ol)' /etc/os-release; then + elif grep -qP '^ID="(rhel|rocky|alma|centos|ol)' /etc/os-release; then deps+=( perl-interpreter ) fi @@ -102,7 +102,7 @@ if [[ -n $dnf ]] || [[ -n $yum ]]; then fi # libxkbcommon-x11-devel is in a non-default repo on RHEL 8.x/9.x (except on AmazonLinux) - if grep -qP '^VERSION_ID="(8|9)' && grep -qP '^ID="(rhel|rocky|centos|alma|ol)' /etc/os-release; then + if grep -qP '^VERSION_ID="(8|9)' /etc/os-release && grep -qP '^ID="(rhel|rocky|centos|alma|ol)' /etc/os-release; then $maysudo dnf install -y 'dnf-command(config-manager)' if grep -qP '^PRETTY_NAME="(AlmaLinux 8|Rocky Linux 8)' /etc/os-release; then $maysudo dnf config-manager --set-enabled powertools From 03c84466c22390bce5802c6ba4f27108d2ab5d61 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 7 Oct 2024 01:29:58 +0200 Subject: [PATCH 16/28] chore: Fix some violations of 'needless_pass_by_ref_mut' lint (#18795) While this lint is allow-by-default, it seems pretty useful to get rid of mutable borrows when they're not needed. Closes #ISSUE Release Notes: - N/A --- clippy.toml | 1 + crates/assistant/src/context.rs | 2 +- crates/client/src/client.rs | 2 +- crates/client/src/telemetry.rs | 2 +- crates/client/src/user.rs | 38 ++++----- crates/db/src/db.rs | 2 +- .../src/git_hosting_providers.rs | 2 +- crates/gpui/src/app.rs | 22 +++-- crates/gpui/src/app/async_context.rs | 4 +- crates/gpui/src/app/model_context.rs | 6 +- crates/gpui/src/elements/div.rs | 4 +- crates/gpui/src/elements/text.rs | 6 +- crates/gpui/src/elements/uniform_list.rs | 2 +- crates/gpui/src/geometry.rs | 8 +- crates/gpui/src/key_dispatch.rs | 4 +- crates/gpui/src/platform/app_menu.rs | 2 +- .../gpui/src/platform/mac/metal_renderer.rs | 18 ++--- crates/gpui/src/platform/mac/platform.rs | 2 +- crates/gpui/src/window.rs | 44 +++++----- crates/language/src/buffer.rs | 24 ++---- crates/markdown/src/markdown.rs | 18 ++--- crates/multi_buffer/src/multi_buffer.rs | 14 ++-- crates/project/src/project_settings.rs | 2 +- crates/remote/src/ssh_session.rs | 6 +- crates/session/src/session.rs | 2 +- crates/settings/src/settings_file.rs | 2 +- crates/snippet_provider/src/lib.rs | 2 +- crates/task/src/static_source.rs | 4 +- crates/terminal/src/terminal.rs | 19 ++--- crates/theme/src/registry.rs | 2 +- crates/theme/src/settings.rs | 6 +- crates/worktree/src/worktree.rs | 80 ++++++++----------- crates/zed/src/reliability.rs | 2 +- crates/zed/src/zed.rs | 4 +- .../zed/src/zed/inline_completion_registry.rs | 2 +- crates/zed/src/zed/open_listener.rs | 2 +- 36 files changed, 158 insertions(+), 204 deletions(-) diff --git a/clippy.toml b/clippy.toml index 787620d865..8c8da03a26 100644 --- a/clippy.toml +++ b/clippy.toml @@ -1 +1,2 @@ allow-private-module-inception = true +avoid-breaking-exported-api = false diff --git a/crates/assistant/src/context.rs b/crates/assistant/src/context.rs index c360c0cb69..c27d17f8c5 100644 --- a/crates/assistant/src/context.rs +++ b/crates/assistant/src/context.rs @@ -549,7 +549,7 @@ impl Context { cx: &mut ModelContext, ) -> Self { let buffer = cx.new_model(|_cx| { - let mut buffer = Buffer::remote( + let buffer = Buffer::remote( language::BufferId::new(1).unwrap(), replica_id, capability, diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 819bd7551f..03d81b117f 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -394,7 +394,7 @@ pub struct PendingEntitySubscription { } impl PendingEntitySubscription { - pub fn set_model(mut self, model: &Model, cx: &mut AsyncAppContext) -> Subscription { + pub fn set_model(mut self, model: &Model, cx: &AsyncAppContext) -> Subscription { self.consumed = true; let mut handlers = self.client.handler_set.lock(); let id = (TypeId::of::(), self.remote_id); diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index 5ec2e5b1aa..ee6da64d22 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -288,7 +288,7 @@ impl Telemetry { system_id: Option, installation_id: Option, session_id: String, - cx: &mut AppContext, + cx: &AppContext, ) { let mut state = self.state.lock(); state.system_id = system_id.map(|id| id.into()); diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 0f53b35fc1..a312dd3495 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -138,7 +138,7 @@ enum UpdateContacts { } impl UserStore { - pub fn new(client: Arc, cx: &mut ModelContext) -> Self { + pub fn new(client: Arc, cx: &ModelContext) -> Self { let (mut current_user_tx, current_user_rx) = watch::channel(); let (update_contacts_tx, mut update_contacts_rx) = mpsc::unbounded(); let rpc_subscriptions = vec![ @@ -310,7 +310,7 @@ impl UserStore { fn update_contacts( &mut self, message: UpdateContacts, - cx: &mut ModelContext, + cx: &ModelContext, ) -> Task> { match message { UpdateContacts::Wait(barrier) => { @@ -525,9 +525,9 @@ impl UserStore { } pub fn dismiss_contact_request( - &mut self, + &self, requester_id: u64, - cx: &mut ModelContext, + cx: &ModelContext, ) -> Task> { let client = self.client.upgrade(); cx.spawn(move |_, _| async move { @@ -573,7 +573,7 @@ impl UserStore { }) } - pub fn clear_contacts(&mut self) -> impl Future { + pub fn clear_contacts(&self) -> impl Future { let (tx, mut rx) = postage::barrier::channel(); self.update_contacts_tx .unbounded_send(UpdateContacts::Clear(tx)) @@ -583,7 +583,7 @@ impl UserStore { } } - pub fn contact_updates_done(&mut self) -> impl Future { + pub fn contact_updates_done(&self) -> impl Future { let (tx, mut rx) = postage::barrier::channel(); self.update_contacts_tx .unbounded_send(UpdateContacts::Wait(tx)) @@ -594,9 +594,9 @@ impl UserStore { } pub fn get_users( - &mut self, + &self, user_ids: Vec, - cx: &mut ModelContext, + cx: &ModelContext, ) -> Task>>> { let mut user_ids_to_fetch = user_ids.clone(); user_ids_to_fetch.retain(|id| !self.users.contains_key(id)); @@ -629,9 +629,9 @@ impl UserStore { } pub fn fuzzy_search_users( - &mut self, + &self, query: String, - cx: &mut ModelContext, + cx: &ModelContext, ) -> Task>>> { self.load_users(proto::FuzzySearchUsers { query }, cx) } @@ -640,11 +640,7 @@ impl UserStore { self.users.get(&user_id).cloned() } - pub fn get_user_optimistic( - &mut self, - user_id: u64, - cx: &mut ModelContext, - ) -> Option> { + pub fn get_user_optimistic(&self, user_id: u64, cx: &ModelContext) -> Option> { if let Some(user) = self.users.get(&user_id).cloned() { return Some(user); } @@ -653,11 +649,7 @@ impl UserStore { None } - pub fn get_user( - &mut self, - user_id: u64, - cx: &mut ModelContext, - ) -> Task>> { + pub fn get_user(&self, user_id: u64, cx: &ModelContext) -> Task>> { if let Some(user) = self.users.get(&user_id).cloned() { return Task::ready(Ok(user)); } @@ -697,7 +689,7 @@ impl UserStore { .map(|accepted_tos_at| accepted_tos_at.is_some()) } - pub fn accept_terms_of_service(&mut self, cx: &mut ModelContext) -> Task> { + pub fn accept_terms_of_service(&self, cx: &ModelContext) -> Task> { if self.current_user().is_none() { return Task::ready(Err(anyhow!("no current user"))); }; @@ -726,9 +718,9 @@ impl UserStore { } fn load_users( - &mut self, + &self, request: impl RequestMessage, - cx: &mut ModelContext, + cx: &ModelContext, ) -> Task>>> { let client = self.client.clone(); cx.spawn(|this, mut cx| async move { diff --git a/crates/db/src/db.rs b/crates/db/src/db.rs index 4d87222c77..98fca60d63 100644 --- a/crates/db/src/db.rs +++ b/crates/db/src/db.rs @@ -188,7 +188,7 @@ macro_rules! define_connection { }; } -pub fn write_and_log(cx: &mut AppContext, db_write: impl FnOnce() -> F + Send + 'static) +pub fn write_and_log(cx: &AppContext, db_write: impl FnOnce() -> F + Send + 'static) where F: Future> + Send, { diff --git a/crates/git_hosting_providers/src/git_hosting_providers.rs b/crates/git_hosting_providers/src/git_hosting_providers.rs index 7bb77c0307..864faa9b49 100644 --- a/crates/git_hosting_providers/src/git_hosting_providers.rs +++ b/crates/git_hosting_providers/src/git_hosting_providers.rs @@ -8,7 +8,7 @@ use gpui::AppContext; pub use crate::providers::*; /// Initializes the Git hosting providers. -pub fn init(cx: &mut AppContext) { +pub fn init(cx: &AppContext) { let provider_registry = GitHostingProviderRegistry::global(cx); // The providers are stored in a `BTreeMap`, so insertion order matters. diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 540e459ce1..bba5f857b4 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -348,7 +348,7 @@ impl AppContext { } /// Gracefully quit the application via the platform's standard routine. - pub fn quit(&mut self) { + pub fn quit(&self) { self.platform.quit(); } @@ -1004,11 +1004,7 @@ impl AppContext { self.globals_by_type.insert(global_type, lease.global); } - pub(crate) fn new_view_observer( - &mut self, - key: TypeId, - value: NewViewListener, - ) -> Subscription { + pub(crate) fn new_view_observer(&self, key: TypeId, value: NewViewListener) -> Subscription { let (subscription, activate) = self.new_view_observers.insert(key, value); activate(); subscription @@ -1016,7 +1012,7 @@ impl AppContext { /// Arrange for the given function to be invoked whenever a view of the specified type is created. /// The function will be passed a mutable reference to the view along with an appropriate context. pub fn observe_new_views( - &mut self, + &self, on_new: impl 'static + Fn(&mut V, &mut ViewContext), ) -> Subscription { self.new_view_observer( @@ -1035,7 +1031,7 @@ impl AppContext { /// Observe the release of a model or view. The callback is invoked after the model or view /// has no more strong references but before it has been dropped. pub fn observe_release( - &mut self, + &self, handle: &E, on_release: impl FnOnce(&mut T, &mut AppContext) + 'static, ) -> Subscription @@ -1062,7 +1058,7 @@ impl AppContext { mut f: impl FnMut(&KeystrokeEvent, &mut WindowContext) + 'static, ) -> Subscription { fn inner( - keystroke_observers: &mut SubscriberSet<(), KeystrokeObserver>, + keystroke_observers: &SubscriberSet<(), KeystrokeObserver>, handler: KeystrokeObserver, ) -> Subscription { let (subscription, activate) = keystroke_observers.insert((), handler); @@ -1140,7 +1136,7 @@ impl AppContext { /// Register a callback to be invoked when the application is about to quit. /// It is not possible to cancel the quit event at this point. pub fn on_app_quit( - &mut self, + &self, mut on_quit: impl FnMut(&mut AppContext) -> Fut + 'static, ) -> Subscription where @@ -1186,7 +1182,7 @@ impl AppContext { } /// Sets the menu bar for this application. This will replace any existing menu bar. - pub fn set_menus(&mut self, menus: Vec) { + pub fn set_menus(&self, menus: Vec) { self.platform.set_menus(menus, &self.keymap.borrow()); } @@ -1196,7 +1192,7 @@ impl AppContext { } /// Sets the right click menu for the app icon in the dock - pub fn set_dock_menu(&mut self, menus: Vec) { + pub fn set_dock_menu(&self, menus: Vec) { self.platform.set_dock_menu(menus, &self.keymap.borrow()); } @@ -1204,7 +1200,7 @@ impl AppContext { /// The list is usually shown on the application icon's context menu in the dock, /// and allows to open the recent files via that context menu. /// If the path is already in the list, it will be moved to the bottom of the list. - pub fn add_recent_document(&mut self, path: &Path) { + pub fn add_recent_document(&self, path: &Path) { self.platform.add_recent_document(path); } diff --git a/crates/gpui/src/app/async_context.rs b/crates/gpui/src/app/async_context.rs index 06e44e71a3..be35776595 100644 --- a/crates/gpui/src/app/async_context.rs +++ b/crates/gpui/src/app/async_context.rs @@ -107,7 +107,7 @@ impl Context for AsyncAppContext { impl AsyncAppContext { /// Schedules all windows in the application to be redrawn. - pub fn refresh(&mut self) -> Result<()> { + pub fn refresh(&self) -> Result<()> { let app = self .app .upgrade() @@ -205,7 +205,7 @@ impl AsyncAppContext { /// A convenience method for [AppContext::update_global] /// for updating the global state of the specified type. pub fn update_global( - &mut self, + &self, update: impl FnOnce(&mut G, &mut AppContext) -> R, ) -> Result { let app = self diff --git a/crates/gpui/src/app/model_context.rs b/crates/gpui/src/app/model_context.rs index 3aebf88539..d3f07b25eb 100644 --- a/crates/gpui/src/app/model_context.rs +++ b/crates/gpui/src/app/model_context.rs @@ -91,7 +91,7 @@ impl<'a, T: 'static> ModelContext<'a, T> { /// Register a callback to be invoked when GPUI releases this model. pub fn on_release( - &mut self, + &self, on_release: impl FnOnce(&mut T, &mut AppContext) + 'static, ) -> Subscription where @@ -110,7 +110,7 @@ impl<'a, T: 'static> ModelContext<'a, T> { /// Register a callback to be run on the release of another model or view pub fn observe_release( - &mut self, + &self, entity: &E, on_release: impl FnOnce(&mut T, &mut T2, &mut ModelContext<'_, T>) + 'static, ) -> Subscription @@ -154,7 +154,7 @@ impl<'a, T: 'static> ModelContext<'a, T> { /// Arrange for the given function to be invoked whenever the application is quit. /// The future returned from this callback will be polled for up to [crate::SHUTDOWN_TIMEOUT] until the app fully quits. pub fn on_app_quit( - &mut self, + &self, mut on_quit: impl FnMut(&mut T, &mut ModelContext) -> Fut + 'static, ) -> Subscription where diff --git a/crates/gpui/src/elements/div.rs b/crates/gpui/src/elements/div.rs index 284e574627..d24c5d25da 100644 --- a/crates/gpui/src/elements/div.rs +++ b/crates/gpui/src/elements/div.rs @@ -1418,7 +1418,7 @@ impl Interactivity { } fn clamp_scroll_position( - &mut self, + &self, bounds: Bounds, style: &Style, cx: &mut WindowContext, @@ -1547,7 +1547,7 @@ impl Interactivity { #[cfg(debug_assertions)] fn paint_debug_info( - &mut self, + &self, global_id: Option<&GlobalElementId>, hitbox: &Hitbox, style: &Style, diff --git a/crates/gpui/src/elements/text.rs b/crates/gpui/src/elements/text.rs index 3fa142de96..56b551737a 100644 --- a/crates/gpui/src/elements/text.rs +++ b/crates/gpui/src/elements/text.rs @@ -252,7 +252,7 @@ impl TextLayout { } fn layout( - &mut self, + &self, text: SharedString, runs: Option>, cx: &mut WindowContext, @@ -350,7 +350,7 @@ impl TextLayout { layout_id } - fn prepaint(&mut self, bounds: Bounds, text: &str) { + fn prepaint(&self, bounds: Bounds, text: &str) { let mut element_state = self.lock(); let element_state = element_state .as_mut() @@ -359,7 +359,7 @@ impl TextLayout { element_state.bounds = Some(bounds); } - fn paint(&mut self, text: &str, cx: &mut WindowContext) { + fn paint(&self, text: &str, cx: &mut WindowContext) { let element_state = self.lock(); let element_state = element_state .as_ref() diff --git a/crates/gpui/src/elements/uniform_list.rs b/crates/gpui/src/elements/uniform_list.rs index d17d078184..b6fcf91e53 100644 --- a/crates/gpui/src/elements/uniform_list.rs +++ b/crates/gpui/src/elements/uniform_list.rs @@ -115,7 +115,7 @@ impl UniformListScrollHandle { } /// Scroll the list to the given item index. - pub fn scroll_to_item(&mut self, ix: usize) { + pub fn scroll_to_item(&self, ix: usize) { self.0.borrow_mut().deferred_scroll_to_item = Some(ix); } diff --git a/crates/gpui/src/geometry.rs b/crates/gpui/src/geometry.rs index 9b9b169804..9e0b9b9014 100644 --- a/crates/gpui/src/geometry.rs +++ b/crates/gpui/src/geometry.rs @@ -706,11 +706,7 @@ pub struct Bounds { impl Bounds { /// Generate a centered bounds for the given display or primary display if none is provided - pub fn centered( - display_id: Option, - size: Size, - cx: &mut AppContext, - ) -> Self { + pub fn centered(display_id: Option, size: Size, cx: &AppContext) -> Self { let display = display_id .and_then(|id| cx.find_display(id)) .or_else(|| cx.primary_display()); @@ -730,7 +726,7 @@ impl Bounds { } /// Generate maximized bounds for the given display or primary display if none is provided - pub fn maximized(display_id: Option, cx: &mut AppContext) -> Self { + pub fn maximized(display_id: Option, cx: &AppContext) -> Self { let display = display_id .and_then(|id| cx.find_display(id)) .or_else(|| cx.primary_display()); diff --git a/crates/gpui/src/key_dispatch.rs b/crates/gpui/src/key_dispatch.rs index d11b4b2cab..cb40a56367 100644 --- a/crates/gpui/src/key_dispatch.rs +++ b/crates/gpui/src/key_dispatch.rs @@ -219,7 +219,7 @@ impl DispatchTree { self.focusable_node_ids.insert(focus_id, node_id); } - pub fn parent_view_id(&mut self) -> Option { + pub fn parent_view_id(&self) -> Option { self.view_stack.last().copied() } @@ -484,7 +484,7 @@ impl DispatchTree { /// Converts the longest prefix of input to a replay event and returns the rest. fn replay_prefix( - &mut self, + &self, mut input: SmallVec<[Keystroke; 1]>, dispatch_path: &SmallVec<[DispatchNodeId; 32]>, ) -> (SmallVec<[Keystroke; 1]>, SmallVec<[Replay; 1]>) { diff --git a/crates/gpui/src/platform/app_menu.rs b/crates/gpui/src/platform/app_menu.rs index 15686956f2..401cc1899c 100644 --- a/crates/gpui/src/platform/app_menu.rs +++ b/crates/gpui/src/platform/app_menu.rs @@ -171,7 +171,7 @@ pub enum OsAction { Redo, } -pub(crate) fn init_app_menus(platform: &dyn Platform, cx: &mut AppContext) { +pub(crate) fn init_app_menus(platform: &dyn Platform, cx: &AppContext) { platform.on_will_open_app_menu(Box::new({ let cx = cx.to_async(); move || { diff --git a/crates/gpui/src/platform/mac/metal_renderer.rs b/crates/gpui/src/platform/mac/metal_renderer.rs index 401734e253..f42a2e2df7 100644 --- a/crates/gpui/src/platform/mac/metal_renderer.rs +++ b/crates/gpui/src/platform/mac/metal_renderer.rs @@ -284,11 +284,11 @@ impl MetalRenderer { } } - pub fn update_transparency(&mut self, _transparent: bool) { + pub fn update_transparency(&self, _transparent: bool) { // todo(mac)? } - pub fn destroy(&mut self) { + pub fn destroy(&self) { // nothing to do } @@ -486,7 +486,7 @@ impl MetalRenderer { } fn rasterize_paths( - &mut self, + &self, paths: &[Path], instance_buffer: &mut InstanceBuffer, instance_offset: &mut usize, @@ -576,7 +576,7 @@ impl MetalRenderer { } fn draw_shadows( - &mut self, + &self, shadows: &[Shadow], instance_buffer: &mut InstanceBuffer, instance_offset: &mut usize, @@ -639,7 +639,7 @@ impl MetalRenderer { } fn draw_quads( - &mut self, + &self, quads: &[Quad], instance_buffer: &mut InstanceBuffer, instance_offset: &mut usize, @@ -698,7 +698,7 @@ impl MetalRenderer { } fn draw_paths( - &mut self, + &self, paths: &[Path], tiles_by_path_id: &HashMap, instance_buffer: &mut InstanceBuffer, @@ -808,7 +808,7 @@ impl MetalRenderer { } fn draw_underlines( - &mut self, + &self, underlines: &[Underline], instance_buffer: &mut InstanceBuffer, instance_offset: &mut usize, @@ -871,7 +871,7 @@ impl MetalRenderer { } fn draw_monochrome_sprites( - &mut self, + &self, texture_id: AtlasTextureId, sprites: &[MonochromeSprite], instance_buffer: &mut InstanceBuffer, @@ -945,7 +945,7 @@ impl MetalRenderer { } fn draw_polychrome_sprites( - &mut self, + &self, texture_id: AtlasTextureId, sprites: &[PolychromeSprite], instance_buffer: &mut InstanceBuffer, diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index 5873d8fe39..50ef6dcf0f 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -1432,7 +1432,7 @@ impl UTType { self.0 } - fn inner_mut(&mut self) -> *mut Object { + fn inner_mut(&self) -> *mut Object { self.0 as *mut _ } } diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index af968c5a2c..98349f8c49 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -835,10 +835,7 @@ impl Window { prompt: None, }) } - fn new_focus_listener( - &mut self, - value: AnyWindowFocusListener, - ) -> (Subscription, impl FnOnce()) { + fn new_focus_listener(&self, value: AnyWindowFocusListener) -> (Subscription, impl FnOnce()) { self.focus_listeners.insert((), value) } } @@ -929,7 +926,7 @@ impl<'a> WindowContext<'a> { /// Obtain a new [`FocusHandle`], which allows you to track and manipulate the keyboard focus /// for elements rendered within this window. - pub fn focus_handle(&mut self) -> FocusHandle { + pub fn focus_handle(&self) -> FocusHandle { FocusHandle::new(&self.window.focus_handles) } @@ -1127,7 +1124,7 @@ impl<'a> WindowContext<'a> { /// Register a callback to be invoked when the given Model or View is released. pub fn observe_release( - &mut self, + &self, entity: &E, mut on_release: impl FnOnce(&mut T, &mut WindowContext) + 'static, ) -> Subscription @@ -1155,7 +1152,7 @@ impl<'a> WindowContext<'a> { } /// Schedule the given closure to be run directly after the current frame is rendered. - pub fn on_next_frame(&mut self, callback: impl FnOnce(&mut WindowContext) + 'static) { + pub fn on_next_frame(&self, callback: impl FnOnce(&mut WindowContext) + 'static) { RefCell::borrow_mut(&self.window.next_frame_callbacks).push(Box::new(callback)); } @@ -1165,7 +1162,7 @@ impl<'a> WindowContext<'a> { /// It will cause the window to redraw on the next frame, even if no other changes have occurred. /// /// If called from within a view, it will notify that view on the next frame. Otherwise, it will refresh the entire window. - pub fn request_animation_frame(&mut self) { + pub fn request_animation_frame(&self) { let parent_id = self.parent_view_id(); self.on_next_frame(move |cx| { if let Some(parent_id) = parent_id { @@ -1179,7 +1176,7 @@ impl<'a> WindowContext<'a> { /// Spawn the future returned by the given closure on the application thread pool. /// The closure is provided a handle to the current window and an `AsyncWindowContext` for /// use within your future. - pub fn spawn(&mut self, f: impl FnOnce(AsyncWindowContext) -> Fut) -> Task + pub fn spawn(&self, f: impl FnOnce(AsyncWindowContext) -> Fut) -> Task where R: 'static, Fut: Future + 'static, @@ -2865,7 +2862,7 @@ impl<'a> WindowContext<'a> { } /// Get the last view id for the current element - pub fn parent_view_id(&mut self) -> Option { + pub fn parent_view_id(&self) -> Option { self.window.next_frame.dispatch_tree.parent_view_id() } @@ -3606,7 +3603,7 @@ impl<'a> WindowContext<'a> { } /// Updates the IME panel position suggestions for languages like japanese, chinese. - pub fn invalidate_character_coordinates(&mut self) { + pub fn invalidate_character_coordinates(&self) { self.on_next_frame(|cx| { if let Some(mut input_handler) = cx.window.platform_window.take_input_handler() { if let Some(bounds) = input_handler.selected_bounds(cx) { @@ -3752,7 +3749,7 @@ impl<'a> WindowContext<'a> { /// Register a callback that can interrupt the closing of the current window based the returned boolean. /// If the callback returns false, the window won't be closed. - pub fn on_window_should_close(&mut self, f: impl Fn(&mut WindowContext) -> bool + 'static) { + pub fn on_window_should_close(&self, f: impl Fn(&mut WindowContext) -> bool + 'static) { let mut this = self.to_async(); self.window .platform_window @@ -4070,7 +4067,7 @@ impl<'a, V: 'static> ViewContext<'a, V> { } /// Sets a given callback to be run on the next frame. - pub fn on_next_frame(&mut self, f: impl FnOnce(&mut V, &mut ViewContext) + 'static) + pub fn on_next_frame(&self, f: impl FnOnce(&mut V, &mut ViewContext) + 'static) where V: 'static, { @@ -4162,7 +4159,7 @@ impl<'a, V: 'static> ViewContext<'a, V> { /// The callback receives a handle to the view's window. This handle may be /// invalid, if the window was closed before the view was released. pub fn on_release( - &mut self, + &self, on_release: impl FnOnce(&mut V, AnyWindowHandle, &mut AppContext) + 'static, ) -> Subscription { let window_handle = self.window.handle; @@ -4179,7 +4176,7 @@ impl<'a, V: 'static> ViewContext<'a, V> { /// Register a callback to be invoked when the given Model or View is released. pub fn observe_release( - &mut self, + &self, entity: &E, mut on_release: impl FnMut(&mut V, &mut V2, &mut ViewContext<'_, V>) + 'static, ) -> Subscription @@ -4212,7 +4209,7 @@ impl<'a, V: 'static> ViewContext<'a, V> { /// Register a callback to be invoked when the window is resized. pub fn observe_window_bounds( - &mut self, + &self, mut callback: impl FnMut(&mut V, &mut ViewContext) + 'static, ) -> Subscription { let view = self.view.downgrade(); @@ -4226,7 +4223,7 @@ impl<'a, V: 'static> ViewContext<'a, V> { /// Register a callback to be invoked when the window is activated or deactivated. pub fn observe_window_activation( - &mut self, + &self, mut callback: impl FnMut(&mut V, &mut ViewContext) + 'static, ) -> Subscription { let view = self.view.downgrade(); @@ -4240,7 +4237,7 @@ impl<'a, V: 'static> ViewContext<'a, V> { /// Registers a callback to be invoked when the window appearance changes. pub fn observe_window_appearance( - &mut self, + &self, mut callback: impl FnMut(&mut V, &mut ViewContext) + 'static, ) -> Subscription { let view = self.view.downgrade(); @@ -4260,7 +4257,7 @@ impl<'a, V: 'static> ViewContext<'a, V> { mut f: impl FnMut(&mut V, &KeystrokeEvent, &mut ViewContext) + 'static, ) -> Subscription { fn inner( - keystroke_observers: &mut SubscriberSet<(), KeystrokeObserver>, + keystroke_observers: &SubscriberSet<(), KeystrokeObserver>, handler: KeystrokeObserver, ) -> Subscription { let (subscription, activate) = keystroke_observers.insert((), handler); @@ -4284,7 +4281,7 @@ impl<'a, V: 'static> ViewContext<'a, V> { /// Register a callback to be invoked when the window's pending input changes. pub fn observe_pending_input( - &mut self, + &self, mut callback: impl FnMut(&mut V, &mut ViewContext) + 'static, ) -> Subscription { let view = self.view.downgrade(); @@ -4372,7 +4369,7 @@ impl<'a, V: 'static> ViewContext<'a, V> { /// and this callback lets you chose a default place to restore the users focus. /// Returns a subscription and persists until the subscription is dropped. pub fn on_focus_lost( - &mut self, + &self, mut listener: impl FnMut(&mut V, &mut ViewContext) + 'static, ) -> Subscription { let view = self.view.downgrade(); @@ -4418,10 +4415,7 @@ impl<'a, V: 'static> ViewContext<'a, V> { /// The given callback is invoked with a [`WeakView`] to avoid leaking the view for a long-running process. /// It's also given an [`AsyncWindowContext`], which can be used to access the state of the view across await points. /// The returned future will be polled on the main thread. - pub fn spawn( - &mut self, - f: impl FnOnce(WeakView, AsyncWindowContext) -> Fut, - ) -> Task + pub fn spawn(&self, f: impl FnOnce(WeakView, AsyncWindowContext) -> Fut) -> Task where R: 'static, Fut: Future + 'static, diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 1f4c56ecc8..4990b9074f 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -588,7 +588,7 @@ impl IndentGuide { impl Buffer { /// Create a new buffer with the given base text. - pub fn local>(base_text: T, cx: &mut ModelContext) -> Self { + pub fn local>(base_text: T, cx: &ModelContext) -> Self { Self::build( TextBuffer::new(0, cx.entity_id().as_non_zero_u64().into(), base_text.into()), None, @@ -601,7 +601,7 @@ impl Buffer { pub fn local_normalized( base_text_normalized: Rope, line_ending: LineEnding, - cx: &mut ModelContext, + cx: &ModelContext, ) -> Self { Self::build( TextBuffer::new_normalized( @@ -934,7 +934,7 @@ impl Buffer { /// Assign a language registry to the buffer. This allows the buffer to retrieve /// other languages if parts of the buffer are written in different languages. - pub fn set_language_registry(&mut self, language_registry: Arc) { + pub fn set_language_registry(&self, language_registry: Arc) { self.syntax_map .lock() .set_language_registry(language_registry); @@ -967,16 +967,13 @@ impl Buffer { } /// This method is called to signal that the buffer has been discarded. - pub fn discarded(&mut self, cx: &mut ModelContext) { + pub fn discarded(&self, cx: &mut ModelContext) { cx.emit(BufferEvent::Discarded); cx.notify(); } /// Reloads the contents of the buffer from disk. - pub fn reload( - &mut self, - cx: &mut ModelContext, - ) -> oneshot::Receiver> { + pub fn reload(&mut self, cx: &ModelContext) -> oneshot::Receiver> { let (tx, rx) = futures::channel::oneshot::channel(); let prev_version = self.text.version(); self.reload_task = Some(cx.spawn(|this, mut cx| async move { @@ -1085,7 +1082,7 @@ impl Buffer { /// Sets the text that will be used to compute a Git diff /// against the buffer text. - pub fn set_diff_base(&mut self, diff_base: Option, cx: &mut ModelContext) { + pub fn set_diff_base(&mut self, diff_base: Option, cx: &ModelContext) { self.diff_base = diff_base.map(|mut raw_diff_base| { LineEnding::normalize(&mut raw_diff_base); BufferDiffBase::Git(Rope::from(raw_diff_base)) @@ -1117,7 +1114,7 @@ impl Buffer { } /// Recomputes the diff. - pub fn recalculate_diff(&mut self, cx: &mut ModelContext) -> Option> { + pub fn recalculate_diff(&self, cx: &ModelContext) -> Option> { let diff_base_rope = match self.diff_base.as_ref()? { BufferDiffBase::Git(rope) => rope.clone(), BufferDiffBase::PastBufferVersion { buffer, .. } => buffer.read(cx).as_rope().clone(), @@ -2249,12 +2246,7 @@ impl Buffer { } } - fn send_operation( - &mut self, - operation: Operation, - is_local: bool, - cx: &mut ModelContext, - ) { + fn send_operation(&self, operation: Operation, is_local: bool, cx: &mut ModelContext) { cx.emit(BufferEvent::Operation { operation, is_local, diff --git a/crates/markdown/src/markdown.rs b/crates/markdown/src/markdown.rs index 7fff489540..47a0e03c2a 100644 --- a/crates/markdown/src/markdown.rs +++ b/crates/markdown/src/markdown.rs @@ -71,7 +71,7 @@ impl Markdown { source: String, style: MarkdownStyle, language_registry: Option>, - cx: &mut ViewContext, + cx: &ViewContext, fallback_code_block_language: Option, ) -> Self { let focus_handle = cx.focus_handle(); @@ -97,7 +97,7 @@ impl Markdown { source: String, style: MarkdownStyle, language_registry: Option>, - cx: &mut ViewContext, + cx: &ViewContext, fallback_code_block_language: Option, ) -> Self { let focus_handle = cx.focus_handle(); @@ -119,12 +119,12 @@ impl Markdown { this } - pub fn append(&mut self, text: &str, cx: &mut ViewContext) { + pub fn append(&mut self, text: &str, cx: &ViewContext) { self.source.push_str(text); self.parse(cx); } - pub fn reset(&mut self, source: String, cx: &mut ViewContext) { + pub fn reset(&mut self, source: String, cx: &ViewContext) { if source == self.source() { return; } @@ -145,7 +145,7 @@ impl Markdown { &self.parsed_markdown } - fn copy(&self, text: &RenderedText, cx: &mut ViewContext) { + fn copy(&self, text: &RenderedText, cx: &ViewContext) { if self.selection.end <= self.selection.start { return; } @@ -153,7 +153,7 @@ impl Markdown { cx.write_to_clipboard(ClipboardItem::new_string(text)); } - fn parse(&mut self, cx: &mut ViewContext) { + fn parse(&mut self, cx: &ViewContext) { if self.source.is_empty() { return; } @@ -319,7 +319,7 @@ impl MarkdownElement { } fn paint_selection( - &mut self, + &self, bounds: Bounds, rendered_text: &RenderedText, cx: &mut WindowContext, @@ -382,7 +382,7 @@ impl MarkdownElement { } fn paint_mouse_listeners( - &mut self, + &self, hitbox: &Hitbox, rendered_text: &RenderedText, cx: &mut WindowContext, @@ -487,7 +487,7 @@ impl MarkdownElement { }); } - fn autoscroll(&mut self, rendered_text: &RenderedText, cx: &mut WindowContext) -> Option<()> { + fn autoscroll(&self, rendered_text: &RenderedText, cx: &mut WindowContext) -> Option<()> { let autoscroll_index = self .markdown .update(cx, |markdown, _| markdown.autoscroll_request.take())?; diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index 7aa733ba8f..18af28498f 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -515,7 +515,7 @@ impl MultiBuffer { } pub fn edit( - &mut self, + &self, edits: I, mut autoindent_mode: Option, cx: &mut ModelContext, @@ -664,7 +664,7 @@ impl MultiBuffer { drop(snapshot); // Non-generic part of edit, hoisted out to avoid blowing up LLVM IR. fn tail( - this: &mut MultiBuffer, + this: &MultiBuffer, buffer_edits: HashMap>, autoindent_mode: Option, edited_excerpt_ids: Vec, @@ -928,7 +928,7 @@ impl MultiBuffer { } } - pub fn push_transaction<'a, T>(&mut self, buffer_transactions: T, cx: &mut ModelContext) + pub fn push_transaction<'a, T>(&mut self, buffer_transactions: T, cx: &ModelContext) where T: IntoIterator, &'a language::Transaction)>, { @@ -952,7 +952,7 @@ impl MultiBuffer { } pub fn set_active_selections( - &mut self, + &self, selections: &[Selection], line_mode: bool, cursor_shape: CursorShape, @@ -1028,7 +1028,7 @@ impl MultiBuffer { } } - pub fn remove_active_selections(&mut self, cx: &mut ModelContext) { + pub fn remove_active_selections(&self, cx: &mut ModelContext) { for buffer in self.buffers.borrow().values() { buffer .buffer @@ -1180,7 +1180,7 @@ impl MultiBuffer { } pub fn push_multiple_excerpts_with_context_lines( - &mut self, + &self, buffers_with_ranges: Vec<(Model, Vec>)>, context_line_count: u32, cx: &mut ModelContext, @@ -4208,7 +4208,7 @@ impl History { &mut self, buffer_transactions: T, now: Instant, - cx: &mut ModelContext, + cx: &ModelContext, ) where T: IntoIterator, &'a language::Transaction)>, { diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index 87150587b3..e956f67260 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -321,7 +321,7 @@ impl SettingsObserver { pub async fn handle_update_user_settings( _: Model, envelope: TypedEnvelope, - mut cx: AsyncAppContext, + cx: AsyncAppContext, ) -> anyhow::Result<()> { cx.update_global(move |settings_store: &mut SettingsStore, cx| { settings_store.set_user_settings(&envelope.payload.content, cx) diff --git a/crates/remote/src/ssh_session.rs b/crates/remote/src/ssh_session.rs index 89ec5db949..32d5536b32 100644 --- a/crates/remote/src/ssh_session.rs +++ b/crates/remote/src/ssh_session.rs @@ -188,7 +188,7 @@ impl ChannelForwarder { fn new( mut incoming_tx: UnboundedSender, mut outgoing_rx: UnboundedReceiver, - cx: &mut AsyncAppContext, + cx: &AsyncAppContext, ) -> (Self, UnboundedSender, UnboundedReceiver) { let (quit_tx, mut quit_rx) = mpsc::unbounded::<()>(); @@ -298,7 +298,7 @@ impl SshRemoteClient { Ok(this) } - fn reconnect(this: Arc, cx: &mut AsyncAppContext) -> Result<()> { + fn reconnect(this: Arc, cx: &AsyncAppContext) -> Result<()> { let Some(state) = this.inner_state.lock().take() else { return Err(anyhow!("reconnect is already in progress")); }; @@ -355,7 +355,7 @@ impl SshRemoteClient { mut ssh_process: Child, incoming_tx: UnboundedSender, mut outgoing_rx: UnboundedReceiver, - cx: &mut AsyncAppContext, + cx: &AsyncAppContext, ) -> Task> { let mut child_stderr = ssh_process.stderr.take().unwrap(); let mut child_stdout = ssh_process.stdout.take().unwrap(); diff --git a/crates/session/src/session.rs b/crates/session/src/session.rs index 01646b6f7a..47b4a2e181 100644 --- a/crates/session/src/session.rs +++ b/crates/session/src/session.rs @@ -64,7 +64,7 @@ pub struct AppSession { } impl AppSession { - pub fn new(session: Session, cx: &mut ModelContext) -> Self { + pub fn new(session: Session, cx: &ModelContext) -> Self { let _subscriptions = vec![cx.on_app_quit(Self::app_will_quit)]; let _serialization_task = Some(cx.spawn(|_, cx| async move { diff --git a/crates/settings/src/settings_file.rs b/crates/settings/src/settings_file.rs index 823b75ef42..2c46732fbc 100644 --- a/crates/settings/src/settings_file.rs +++ b/crates/settings/src/settings_file.rs @@ -77,7 +77,7 @@ pub fn handle_settings_file_changes( .set_user_settings(&user_settings_content, cx) .log_err(); }); - cx.spawn(move |mut cx| async move { + cx.spawn(move |cx| async move { while let Some(user_settings_content) = user_settings_file_rx.next().await { let result = cx.update_global(|store: &mut SettingsStore, cx| { let result = store.set_user_settings(&user_settings_content, cx); diff --git a/crates/snippet_provider/src/lib.rs b/crates/snippet_provider/src/lib.rs index a18f9ff1b6..17d60d25a0 100644 --- a/crates/snippet_provider/src/lib.rs +++ b/crates/snippet_provider/src/lib.rs @@ -179,7 +179,7 @@ impl SnippetProvider { } /// Add directory to be watched for content changes - fn watch_directory(&mut self, path: &Path, cx: &mut ModelContext) { + fn watch_directory(&mut self, path: &Path, cx: &ModelContext) { let path: Arc = Arc::from(path); self.watch_tasks.push(cx.spawn(|this, mut cx| async move { diff --git a/crates/task/src/static_source.rs b/crates/task/src/static_source.rs index 3ae9ee0b70..48f8fdfa01 100644 --- a/crates/task/src/static_source.rs +++ b/crates/task/src/static_source.rs @@ -27,7 +27,7 @@ impl TrackedFile { pub fn new( mut tracker: UnboundedReceiver, notification_outlet: UnboundedSender<()>, - cx: &mut AppContext, + cx: &AppContext, ) -> Self where T: for<'a> Deserialize<'a> + Default + Send, @@ -69,7 +69,7 @@ impl TrackedFile { pub fn new_convertible Deserialize<'a> + TryInto>( mut tracker: UnboundedReceiver, notification_outlet: UnboundedSender<()>, - cx: &mut AppContext, + cx: &AppContext, ) -> Self where T: Default + Send, diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index b51308df37..9412999cbf 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -320,7 +320,7 @@ impl TerminalBuilder { max_scroll_history_lines: Option, window: AnyWindowHandle, completion_tx: Sender<()>, - cx: &mut AppContext, + cx: &AppContext, ) -> Result { // TODO: Properly set the current locale, env.entry("LC_ALL".to_string()) @@ -455,7 +455,7 @@ impl TerminalBuilder { }) } - pub fn subscribe(mut self, cx: &mut ModelContext) -> Terminal { + pub fn subscribe(mut self, cx: &ModelContext) -> Terminal { //Event loop cx.spawn(|terminal, mut cx| async move { while let Some(event) = self.events_rx.next().await { @@ -1280,7 +1280,7 @@ impl Terminal { } } - fn drag_line_delta(&mut self, e: &MouseMoveEvent, region: Bounds) -> Option { + fn drag_line_delta(&self, e: &MouseMoveEvent, region: Bounds) -> Option { //TODO: Why do these need to be doubled? Probably the same problem that the IME has let top = region.origin.y + (self.last_content.size.line_height * 2.); let bottom = region.lower_left().y - (self.last_content.size.line_height * 2.); @@ -1351,12 +1351,7 @@ impl Terminal { } } - pub fn mouse_up( - &mut self, - e: &MouseUpEvent, - origin: Point, - cx: &mut ModelContext, - ) { + pub fn mouse_up(&mut self, e: &MouseUpEvent, origin: Point, cx: &ModelContext) { let setting = TerminalSettings::get_global(cx); let position = e.position - origin; @@ -1458,9 +1453,9 @@ impl Terminal { } pub fn find_matches( - &mut self, + &self, mut searcher: RegexSearch, - cx: &mut ModelContext, + cx: &ModelContext, ) -> Task>> { let term = self.term.clone(); cx.background_executor().spawn(async move { @@ -1530,7 +1525,7 @@ impl Terminal { self.task.as_ref() } - pub fn wait_for_completed_task(&self, cx: &mut AppContext) -> Task<()> { + pub fn wait_for_completed_task(&self, cx: &AppContext) -> Task<()> { if let Some(task) = self.task() { if task.status == TaskStatus::Running { let mut completion_receiver = task.completion_rx.clone(); diff --git a/crates/theme/src/registry.rs b/crates/theme/src/registry.rs index 771511973f..9f95d19937 100644 --- a/crates/theme/src/registry.rs +++ b/crates/theme/src/registry.rs @@ -189,7 +189,7 @@ impl ThemeRegistry { } /// Removes all themes from the registry. - pub fn clear(&mut self) { + pub fn clear(&self) { self.state.write().themes.clear(); } diff --git a/crates/theme/src/settings.rs b/crates/theme/src/settings.rs index d126ec058c..e4be957a1b 100644 --- a/crates/theme/src/settings.rs +++ b/crates/theme/src/settings.rs @@ -498,7 +498,7 @@ pub fn observe_buffer_font_size_adjustment( } /// Sets the adjusted buffer font size. -pub fn adjusted_font_size(size: Pixels, cx: &mut AppContext) -> Pixels { +pub fn adjusted_font_size(size: Pixels, cx: &AppContext) -> Pixels { if let Some(AdjustedBufferFontSize(adjusted_size)) = cx.try_global::() { let buffer_font_size = ThemeSettings::get_global(cx).buffer_font_size; let delta = *adjusted_size - buffer_font_size; @@ -530,7 +530,7 @@ pub fn adjust_buffer_font_size(cx: &mut AppContext, f: fn(&mut Pixels)) { } /// Returns whether the buffer font size has been adjusted. -pub fn has_adjusted_buffer_font_size(cx: &mut AppContext) -> bool { +pub fn has_adjusted_buffer_font_size(cx: &AppContext) -> bool { cx.has_global::() } @@ -576,7 +576,7 @@ pub fn adjust_ui_font_size(cx: &mut AppContext, f: fn(&mut Pixels)) { } /// Returns whether the UI font size has been adjusted. -pub fn has_adjusted_ui_font_size(cx: &mut AppContext) -> bool { +pub fn has_adjusted_ui_font_size(cx: &AppContext) -> bool { cx.has_global::() } diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 40cd465d9b..3a5ba2c276 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -622,7 +622,7 @@ impl Worktree { } } - pub fn root_file(&self, cx: &mut ModelContext) -> Option> { + pub fn root_file(&self, cx: &ModelContext) -> Option> { let entry = self.root_entry()?; Some(File::for_entry(entry.clone(), cx.handle())) } @@ -630,7 +630,7 @@ impl Worktree { pub fn observe_updates( &mut self, project_id: u64, - cx: &mut ModelContext, + cx: &ModelContext, callback: F, ) where F: 'static + Send + Fn(proto::UpdateWorktree) -> Fut, @@ -661,11 +661,7 @@ impl Worktree { } } - pub fn load_file( - &self, - path: &Path, - cx: &mut ModelContext, - ) -> Task> { + pub fn load_file(&self, path: &Path, cx: &ModelContext) -> Task> { match self { Worktree::Local(this) => this.load_file(path, cx), Worktree::Remote(_) => { @@ -679,7 +675,7 @@ impl Worktree { path: &Path, text: Rope, line_ending: LineEnding, - cx: &mut ModelContext, + cx: &ModelContext, ) -> Task>> { match self { Worktree::Local(this) => this.write_file(path, text, line_ending, cx), @@ -693,7 +689,7 @@ impl Worktree { &mut self, path: impl Into>, is_directory: bool, - cx: &mut ModelContext, + cx: &ModelContext, ) -> Task> { let path = path.into(); let worktree_id = self.id(); @@ -773,7 +769,7 @@ impl Worktree { &mut self, entry_id: ProjectEntryId, new_path: impl Into>, - cx: &mut ModelContext, + cx: &ModelContext, ) -> Task> { let new_path = new_path.into(); match self { @@ -787,7 +783,7 @@ impl Worktree { entry_id: ProjectEntryId, relative_worktree_source_path: Option, new_path: impl Into>, - cx: &mut ModelContext, + cx: &ModelContext, ) -> Task>> { let new_path = new_path.into(); match self { @@ -830,7 +826,7 @@ impl Worktree { target_directory: PathBuf, paths: Vec>, overwrite_existing_files: bool, - cx: &mut ModelContext, + cx: &ModelContext, ) -> Task>> { match self { Worktree::Local(this) => { @@ -845,7 +841,7 @@ impl Worktree { pub fn expand_entry( &mut self, entry_id: ProjectEntryId, - cx: &mut ModelContext, + cx: &ModelContext, ) -> Option>> { match self { Worktree::Local(this) => this.expand_entry(entry_id, cx), @@ -987,7 +983,7 @@ impl LocalWorktree { !self.share_private_files && self.settings.is_path_private(path) } - fn restart_background_scanners(&mut self, cx: &mut ModelContext) { + fn restart_background_scanners(&mut self, cx: &ModelContext) { let (scan_requests_tx, scan_requests_rx) = channel::unbounded(); let (path_prefixes_to_scan_tx, path_prefixes_to_scan_rx) = channel::unbounded(); self.scan_requests_tx = scan_requests_tx; @@ -999,7 +995,7 @@ impl LocalWorktree { &mut self, scan_requests_rx: channel::Receiver, path_prefixes_to_scan_rx: channel::Receiver>, - cx: &mut ModelContext, + cx: &ModelContext, ) { let snapshot = self.snapshot(); let share_private_files = self.share_private_files; @@ -1236,7 +1232,7 @@ impl LocalWorktree { self.git_repositories.get(&repo.work_directory.0) } - fn load_file(&self, path: &Path, cx: &mut ModelContext) -> Task> { + fn load_file(&self, path: &Path, cx: &ModelContext) -> Task> { let path = Arc::from(path); let abs_path = self.absolutize(&path); let fs = self.fs.clone(); @@ -1318,7 +1314,7 @@ impl LocalWorktree { &self, path: impl Into>, is_dir: bool, - cx: &mut ModelContext, + cx: &ModelContext, ) -> Task> { let path = path.into(); let abs_path = match self.absolutize(&path) { @@ -1383,7 +1379,7 @@ impl LocalWorktree { path: impl Into>, text: Rope, line_ending: LineEnding, - cx: &mut ModelContext, + cx: &ModelContext, ) -> Task>> { let path = path.into(); let fs = self.fs.clone(); @@ -1437,7 +1433,7 @@ impl LocalWorktree { &self, entry_id: ProjectEntryId, trash: bool, - cx: &mut ModelContext, + cx: &ModelContext, ) -> Option>> { let entry = self.entry_for_id(entry_id)?.clone(); let abs_path = self.absolutize(&entry.path); @@ -1489,7 +1485,7 @@ impl LocalWorktree { &self, entry_id: ProjectEntryId, new_path: impl Into>, - cx: &mut ModelContext, + cx: &ModelContext, ) -> Task> { let old_path = match self.entry_for_id(entry_id) { Some(entry) => entry.path.clone(), @@ -1547,7 +1543,7 @@ impl LocalWorktree { entry_id: ProjectEntryId, relative_worktree_source_path: Option, new_path: impl Into>, - cx: &mut ModelContext, + cx: &ModelContext, ) -> Task>> { let old_path = match self.entry_for_id(entry_id) { Some(entry) => entry.path.clone(), @@ -1584,11 +1580,11 @@ impl LocalWorktree { } pub fn copy_external_entries( - &mut self, + &self, target_directory: PathBuf, paths: Vec>, overwrite_existing_files: bool, - cx: &mut ModelContext, + cx: &ModelContext, ) -> Task>> { let worktree_path = self.abs_path().clone(); let fs = self.fs.clone(); @@ -1665,9 +1661,9 @@ impl LocalWorktree { } fn expand_entry( - &mut self, + &self, entry_id: ProjectEntryId, - cx: &mut ModelContext, + cx: &ModelContext, ) -> Option>> { let path = self.entry_for_id(entry_id)?.path.clone(); let mut refresh = self.refresh_entries_for_paths(vec![path]); @@ -1696,7 +1692,7 @@ impl LocalWorktree { &self, path: Arc, old_path: Option>, - cx: &mut ModelContext, + cx: &ModelContext, ) -> Task>> { if self.settings.is_path_excluded(&path) { return Task::ready(Ok(None)); @@ -1720,12 +1716,8 @@ impl LocalWorktree { }) } - fn observe_updates( - &mut self, - project_id: u64, - cx: &mut ModelContext, - callback: F, - ) where + fn observe_updates(&mut self, project_id: u64, cx: &ModelContext, callback: F) + where F: 'static + Send + Fn(proto::UpdateWorktree) -> Fut, Fut: Send + Future, { @@ -1784,7 +1776,7 @@ impl LocalWorktree { }); } - pub fn share_private_files(&mut self, cx: &mut ModelContext) { + pub fn share_private_files(&mut self, cx: &ModelContext) { self.share_private_files = true; self.restart_background_scanners(cx); } @@ -1805,7 +1797,7 @@ impl RemoteWorktree { self.disconnected = true; } - pub fn update_from_remote(&mut self, update: proto::UpdateWorktree) { + pub fn update_from_remote(&self, update: proto::UpdateWorktree) { if let Some(updates_tx) = &self.updates_tx { updates_tx .unbounded_send(update) @@ -1813,12 +1805,8 @@ impl RemoteWorktree { } } - fn observe_updates( - &mut self, - project_id: u64, - cx: &mut ModelContext, - callback: F, - ) where + fn observe_updates(&mut self, project_id: u64, cx: &ModelContext, callback: F) + where F: 'static + Send + Fn(proto::UpdateWorktree) -> Fut, Fut: 'static + Send + Future, { @@ -1879,7 +1867,7 @@ impl RemoteWorktree { &mut self, entry: proto::Entry, scan_id: usize, - cx: &mut ModelContext, + cx: &ModelContext, ) -> Task> { let wait_for_snapshot = self.wait_for_snapshot(scan_id); cx.spawn(|this, mut cx| async move { @@ -1895,10 +1883,10 @@ impl RemoteWorktree { } fn delete_entry( - &mut self, + &self, entry_id: ProjectEntryId, trash: bool, - cx: &mut ModelContext, + cx: &ModelContext, ) -> Option>> { let response = self.client.request(proto::DeleteProjectEntry { project_id: self.project_id, @@ -1924,10 +1912,10 @@ impl RemoteWorktree { } fn rename_entry( - &mut self, + &self, entry_id: ProjectEntryId, new_path: impl Into>, - cx: &mut ModelContext, + cx: &ModelContext, ) -> Task> { let new_path = new_path.into(); let response = self.client.request(proto::RenameProjectEntry { @@ -3692,7 +3680,7 @@ impl BackgroundScanner { self.send_status_update(scanning, request.done) } - async fn process_events(&mut self, mut abs_paths: Vec) { + async fn process_events(&self, mut abs_paths: Vec) { let root_path = self.state.lock().snapshot.abs_path.clone(); let root_canonical_path = match self.fs.canonicalize(&root_path).await { Ok(path) => path, diff --git a/crates/zed/src/reliability.rs b/crates/zed/src/reliability.rs index b40bbc78bd..9d76a6c47f 100644 --- a/crates/zed/src/reliability.rs +++ b/crates/zed/src/reliability.rs @@ -347,7 +347,7 @@ pub fn monitor_main_thread_hangs( fn upload_panics_and_crashes( http: Arc, installation_id: Option, - cx: &mut AppContext, + cx: &AppContext, ) { let telemetry_settings = *client::TelemetrySettings::get_global(cx); cx.background_executor() diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 4dc378a755..eb60ab9fa8 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -573,7 +573,7 @@ fn feature_gate_zed_pro_actions(cx: &mut AppContext) { .detach(); } -fn initialize_pane(workspace: &mut Workspace, pane: &View, cx: &mut ViewContext) { +fn initialize_pane(workspace: &Workspace, pane: &View, cx: &mut ViewContext) { pane.update(cx, |pane, cx| { pane.toolbar().update(cx, |toolbar, cx| { let multibuffer_hint = cx.new_view(|_| MultibufferHint::new()); @@ -981,7 +981,7 @@ fn open_telemetry_log_file(workspace: &mut Workspace, cx: &mut ViewContext, title: &'static str, language: &'static str, diff --git a/crates/zed/src/zed/inline_completion_registry.rs b/crates/zed/src/zed/inline_completion_registry.rs index ff84297b58..aa0707d851 100644 --- a/crates/zed/src/zed/inline_completion_registry.rs +++ b/crates/zed/src/zed/inline_completion_registry.rs @@ -64,7 +64,7 @@ pub fn init(telemetry: Arc, cx: &mut AppContext) { .detach(); } -fn register_backward_compatible_actions(editor: &mut Editor, cx: &mut ViewContext) { +fn register_backward_compatible_actions(editor: &mut Editor, cx: &ViewContext) { // We renamed some of these actions to not be copilot-specific, but that // would have not been backwards-compatible. So here we are re-registering // the actions with the old names to not break people's keymaps. diff --git a/crates/zed/src/zed/open_listener.rs b/crates/zed/src/zed/open_listener.rs index c5fd2b2e78..7746337df1 100644 --- a/crates/zed/src/zed/open_listener.rs +++ b/crates/zed/src/zed/open_listener.rs @@ -661,7 +661,7 @@ mod tests { path: &str, open_new_workspace: Option, app_state: Arc, - cx: &mut TestAppContext, + cx: &TestAppContext, ) { let (response_tx, _) = ipc::channel::().unwrap(); From 67fbdbbed62ddb01b734f77f347d7295c462ce7a Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Mon, 7 Oct 2024 10:42:48 +0200 Subject: [PATCH 17/28] Put back code that makes the avatar rounded (#18799) Follow-up to https://github.com/zed-industries/zed/pull/18768 --- Release Notes: - N/A --- crates/ui/src/components/avatar/avatar.rs | 36 +++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/crates/ui/src/components/avatar/avatar.rs b/crates/ui/src/components/avatar/avatar.rs index 4106b17452..f0e516c7b1 100644 --- a/crates/ui/src/components/avatar/avatar.rs +++ b/crates/ui/src/components/avatar/avatar.rs @@ -2,6 +2,16 @@ use crate::prelude::*; use gpui::{img, AnyElement, Hsla, ImageSource, Img, IntoElement, Styled}; +/// The shape of an [`Avatar`]. +#[derive(Debug, Default, PartialEq, Clone)] +pub enum AvatarShape { + /// The avatar is shown in a circle. + #[default] + Circle, + /// The avatar is shown in a rectangle with rounded corners. + RoundedRectangle, +} + /// An element that renders a user avatar with customizable appearance options. /// /// # Examples @@ -10,6 +20,7 @@ use gpui::{img, AnyElement, Hsla, ImageSource, Img, IntoElement, Styled}; /// use ui::{Avatar, AvatarShape}; /// /// Avatar::new("path/to/image.png") +/// .shape(AvatarShape::Circle) /// .grayscale(true) /// .border_color(gpui::red()); /// ``` @@ -32,6 +43,27 @@ impl Avatar { } } + /// Sets the shape of the avatar image. + /// + /// This method allows the shape of the avatar to be specified using an [`AvatarShape`]. + /// It modifies the corner radius of the image to match the specified shape. + /// + /// # Examples + /// + /// ``` + /// use ui::{Avatar, AvatarShape}; + /// + /// Avatar::new("path/to/image.png").shape(AvatarShape::Circle); + /// ``` + /// + pub fn shape(mut self, shape: AvatarShape) -> Self { + self.image = match shape { + AvatarShape::Circle => self.image.rounded_full(), + AvatarShape::RoundedRectangle => self.image.rounded_md(), + }; + self + } + /// Applies a grayscale filter to the avatar image. /// /// # Examples @@ -71,6 +103,10 @@ impl Avatar { impl RenderOnce for Avatar { fn render(mut self, cx: &mut WindowContext) -> impl IntoElement { + if self.image.style().corner_radii.top_left.is_none() { + self = self.shape(AvatarShape::Circle); + } + let border_width = if self.border_color.is_some() { px(2.) } else { From c03b8d6c48b9f961ed30cb4d0ef97169fb30c1c4 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Mon, 7 Oct 2024 11:40:59 +0200 Subject: [PATCH 18/28] ssh remoting: Enable reconnecting after connection losses (#18586) Release Notes: - N/A --------- Co-authored-by: Bennet --- Cargo.lock | 2 + crates/collab/src/tests/test_server.rs | 2 +- crates/project/src/project.rs | 72 ++-- crates/project/src/terminals.rs | 2 +- crates/proto/proto/zed.proto | 6 +- crates/proto/src/proto.rs | 6 +- crates/recent_projects/src/dev_servers.rs | 22 +- crates/recent_projects/src/ssh_connections.rs | 58 +-- crates/remote/src/protocol.rs | 14 + crates/remote/src/ssh_session.rs | 191 ++++++---- crates/remote_server/Cargo.toml | 11 +- crates/remote_server/src/headless_project.rs | 19 + crates/remote_server/src/main.rs | 134 +++---- .../remote_server/src/remote_editing_tests.rs | 2 +- crates/remote_server/src/remote_server.rs | 3 + crates/remote_server/src/unix.rs | 336 ++++++++++++++++++ crates/title_bar/src/title_bar.rs | 2 +- crates/workspace/Cargo.toml | 1 + crates/workspace/src/workspace.rs | 84 +++-- 19 files changed, 727 insertions(+), 240 deletions(-) create mode 100644 crates/remote_server/src/unix.rs diff --git a/Cargo.lock b/Cargo.lock index 120f51ec93..1cac85e0c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9165,6 +9165,7 @@ version = "0.1.0" dependencies = [ "anyhow", "cargo_toml", + "clap", "client", "clock", "env_logger", @@ -14324,6 +14325,7 @@ dependencies = [ "parking_lot", "postage", "project", + "release_channel", "remote", "schemars", "serde", diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index 5e7d935c36..8d2396eef0 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/crates/collab/src/tests/test_server.rs @@ -835,7 +835,7 @@ impl TestClient { pub async fn build_ssh_project( &self, root_path: impl AsRef, - ssh: Arc, + ssh: Model, cx: &mut TestAppContext, ) -> (Model, WorktreeId) { let project = cx.update(|cx| { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 4b8c050964..f2a8d59c6f 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -141,7 +141,7 @@ pub struct Project { join_project_response_message_id: u32, user_store: Model, fs: Arc, - ssh_client: Option>, + ssh_client: Option>, client_state: ProjectClientState, collaborators: HashMap, client_subscriptions: Vec, @@ -667,7 +667,7 @@ impl Project { } pub fn ssh( - ssh: Arc, + ssh: Model, client: Arc, node: NodeRuntime, user_store: Model, @@ -684,15 +684,16 @@ impl Project { let snippets = SnippetProvider::new(fs.clone(), BTreeSet::from_iter([global_snippets_dir]), cx); + let ssh_proto = ssh.read(cx).to_proto_client(); let worktree_store = - cx.new_model(|_| WorktreeStore::remote(false, ssh.to_proto_client(), 0, None)); + cx.new_model(|_| WorktreeStore::remote(false, ssh_proto.clone(), 0, None)); cx.subscribe(&worktree_store, Self::on_worktree_store_event) .detach(); let buffer_store = cx.new_model(|cx| { BufferStore::remote( worktree_store.clone(), - ssh.to_proto_client(), + ssh.read(cx).to_proto_client(), SSH_PROJECT_ID, cx, ) @@ -701,7 +702,7 @@ impl Project { .detach(); let settings_observer = cx.new_model(|cx| { - SettingsObserver::new_ssh(ssh.to_proto_client(), worktree_store.clone(), cx) + SettingsObserver::new_ssh(ssh_proto.clone(), worktree_store.clone(), cx) }); cx.subscribe(&settings_observer, Self::on_settings_observer_event) .detach(); @@ -712,13 +713,24 @@ impl Project { buffer_store.clone(), worktree_store.clone(), languages.clone(), - ssh.to_proto_client(), + ssh_proto.clone(), SSH_PROJECT_ID, cx, ) }); cx.subscribe(&lsp_store, Self::on_lsp_store_event).detach(); + cx.on_release(|this, cx| { + if let Some(ssh_client) = this.ssh_client.as_ref() { + ssh_client + .read(cx) + .to_proto_client() + .send(proto::ShutdownRemoteServer {}) + .log_err(); + } + }) + .detach(); + let this = Self { buffer_ordered_messages_tx: tx, collaborators: Default::default(), @@ -754,20 +766,20 @@ impl Project { search_excluded_history: Self::new_search_history(), }; - let client: AnyProtoClient = ssh.to_proto_client(); - + let ssh = ssh.read(cx); ssh.subscribe_to_entity(SSH_PROJECT_ID, &cx.handle()); ssh.subscribe_to_entity(SSH_PROJECT_ID, &this.buffer_store); ssh.subscribe_to_entity(SSH_PROJECT_ID, &this.worktree_store); ssh.subscribe_to_entity(SSH_PROJECT_ID, &this.lsp_store); ssh.subscribe_to_entity(SSH_PROJECT_ID, &this.settings_observer); - client.add_model_message_handler(Self::handle_create_buffer_for_peer); - client.add_model_message_handler(Self::handle_update_worktree); - client.add_model_message_handler(Self::handle_update_project); - client.add_model_request_handler(BufferStore::handle_update_buffer); - BufferStore::init(&client); - LspStore::init(&client); - SettingsObserver::init(&client); + + ssh_proto.add_model_message_handler(Self::handle_create_buffer_for_peer); + ssh_proto.add_model_message_handler(Self::handle_update_worktree); + ssh_proto.add_model_message_handler(Self::handle_update_project); + ssh_proto.add_model_request_handler(BufferStore::handle_update_buffer); + BufferStore::init(&ssh_proto); + LspStore::init(&ssh_proto); + SettingsObserver::init(&ssh_proto); this }) @@ -1222,7 +1234,7 @@ impl Project { pub fn ssh_connection_string(&self, cx: &AppContext) -> Option { if let Some(ssh_state) = &self.ssh_client { - return Some(ssh_state.connection_string().into()); + return Some(ssh_state.read(cx).connection_string().into()); } let dev_server_id = self.dev_server_project_id()?; dev_server_projects::Store::global(cx) @@ -1232,8 +1244,8 @@ impl Project { .clone() } - pub fn ssh_is_connected(&self) -> Option { - Some(!self.ssh_client.as_ref()?.is_reconnect_underway()) + pub fn ssh_is_connected(&self, cx: &AppContext) -> Option { + Some(!self.ssh_client.as_ref()?.read(cx).is_reconnect_underway()) } pub fn replica_id(&self) -> ReplicaId { @@ -1945,6 +1957,7 @@ impl Project { BufferStoreEvent::BufferDropped(buffer_id) => { if let Some(ref ssh_client) = self.ssh_client { ssh_client + .read(cx) .to_proto_client() .send(proto::CloseBuffer { project_id: 0, @@ -2151,7 +2164,8 @@ impl Project { let operation = language::proto::serialize_operation(operation); if let Some(ssh) = &self.ssh_client { - ssh.to_proto_client() + ssh.read(cx) + .to_proto_client() .send(proto::UpdateBuffer { project_id: 0, buffer_id: buffer_id.to_proto(), @@ -2838,7 +2852,7 @@ impl Project { let (tx, rx) = smol::channel::unbounded(); let (client, remote_id): (AnyProtoClient, _) = if let Some(ssh_client) = &self.ssh_client { - (ssh_client.to_proto_client(), 0) + (ssh_client.read(cx).to_proto_client(), 0) } else if let Some(remote_id) = self.remote_id() { (self.client.clone().into(), remote_id) } else { @@ -2973,12 +2987,14 @@ impl Project { exists.then(|| ResolvedPath::AbsPath(expanded)) }) } else if let Some(ssh_client) = self.ssh_client.as_ref() { - let request = ssh_client - .to_proto_client() - .request(proto::CheckFileExists { - project_id: SSH_PROJECT_ID, - path: path.to_string(), - }); + let request = + ssh_client + .read(cx) + .to_proto_client() + .request(proto::CheckFileExists { + project_id: SSH_PROJECT_ID, + path: path.to_string(), + }); cx.background_executor().spawn(async move { let response = request.await.log_err()?; if response.exists { @@ -3054,7 +3070,7 @@ impl Project { path: query, }; - let response = session.to_proto_client().request(request); + let response = session.read(cx).to_proto_client().request(request); cx.background_executor().spawn(async move { let response = response.await?; Ok(response.entries.into_iter().map(PathBuf::from).collect()) @@ -3482,7 +3498,7 @@ impl Project { let mut payload = envelope.payload.clone(); payload.project_id = 0; cx.background_executor() - .spawn(ssh.to_proto_client().request(payload)) + .spawn(ssh.read(cx).to_proto_client().request(payload)) .detach_and_log_err(cx); } this.buffer_store.clone() diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index 7175b75e22..ecac58fb85 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -70,7 +70,7 @@ impl Project { if let Some(args) = self .ssh_client .as_ref() - .and_then(|session| session.ssh_args()) + .and_then(|session| session.read(cx).ssh_args()) { return Some(SshCommand::Direct(args)); } diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index f6e9645e9c..4e101f4305 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -282,7 +282,9 @@ message Envelope { UpdateUserSettings update_user_settings = 246; CheckFileExists check_file_exists = 255; - CheckFileExistsResponse check_file_exists_response = 256; // current max + CheckFileExistsResponse check_file_exists_response = 256; + + ShutdownRemoteServer shutdown_remote_server = 257; // current max } reserved 87 to 88; @@ -2511,3 +2513,5 @@ message CheckFileExistsResponse { bool exists = 1; string path = 2; } + +message ShutdownRemoteServer {} diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index fe1725e0d1..48733c449c 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -364,7 +364,8 @@ messages!( (CloseBuffer, Foreground), (UpdateUserSettings, Foreground), (CheckFileExists, Background), - (CheckFileExistsResponse, Background) + (CheckFileExistsResponse, Background), + (ShutdownRemoteServer, Foreground), ); request_messages!( @@ -487,7 +488,8 @@ request_messages!( (SynchronizeContexts, SynchronizeContextsResponse), (LspExtSwitchSourceHeader, LspExtSwitchSourceHeaderResponse), (AddWorktree, AddWorktreeResponse), - (CheckFileExists, CheckFileExistsResponse) + (CheckFileExists, CheckFileExistsResponse), + (ShutdownRemoteServer, Ack) ); entity_messages!( diff --git a/crates/recent_projects/src/dev_servers.rs b/crates/recent_projects/src/dev_servers.rs index 2038d069b4..722743e0ff 100644 --- a/crates/recent_projects/src/dev_servers.rs +++ b/crates/recent_projects/src/dev_servers.rs @@ -305,13 +305,19 @@ impl DevServerProjects { let connection_options = remote::SshConnectionOptions { host: host.to_string(), - username, + username: username.clone(), port, password: None, }; let ssh_prompt = cx.new_view(|cx| SshPrompt::new(&connection_options, cx)); - let connection = connect_over_ssh(connection_options.clone(), ssh_prompt.clone(), cx) - .prompt_err("Failed to connect", cx, |_, _| None); + + let connection = connect_over_ssh( + connection_options.dev_server_identifier(), + connection_options.clone(), + ssh_prompt.clone(), + cx, + ) + .prompt_err("Failed to connect", cx, |_, _| None); let creating = cx.spawn(move |this, mut cx| async move { match connection.await { @@ -363,11 +369,13 @@ impl DevServerProjects { .prompt .clone(); - let connect = connect_over_ssh(connection_options, prompt, cx).prompt_err( - "Failed to connect", + let connect = connect_over_ssh( + connection_options.dev_server_identifier(), + connection_options, + prompt, cx, - |_, _| None, - ); + ) + .prompt_err("Failed to connect", cx, |_, _| None); cx.spawn(|workspace, mut cx| async move { let Some(session) = connect.await else { workspace diff --git a/crates/recent_projects/src/ssh_connections.rs b/crates/recent_projects/src/ssh_connections.rs index d0fffc031f..9e50523773 100644 --- a/crates/recent_projects/src/ssh_connections.rs +++ b/crates/recent_projects/src/ssh_connections.rs @@ -4,12 +4,12 @@ use anyhow::Result; use auto_update::AutoUpdater; use editor::Editor; use futures::channel::oneshot; -use gpui::AppContext; use gpui::{ percentage, px, Animation, AnimationExt, AnyWindowHandle, AsyncAppContext, DismissEvent, EventEmitter, FocusableView, ParentElement as _, Render, SemanticVersion, SharedString, Task, Transformation, View, }; +use gpui::{AppContext, Model}; use release_channel::{AppVersion, ReleaseChannel}; use remote::{SshConnectionOptions, SshPlatform, SshRemoteClient}; use schemars::JsonSchema; @@ -373,25 +373,24 @@ impl SshClientDelegate { } pub fn connect_over_ssh( + unique_identifier: String, connection_options: SshConnectionOptions, ui: View, cx: &mut WindowContext, -) -> Task>> { +) -> Task>> { let window = cx.window_handle(); let known_password = connection_options.password.clone(); - cx.spawn(|mut cx| async move { - remote::SshRemoteClient::new( - connection_options, - Arc::new(SshClientDelegate { - window, - ui, - known_password, - }), - &mut cx, - ) - .await - }) + remote::SshRemoteClient::new( + unique_identifier, + connection_options, + Arc::new(SshClientDelegate { + window, + ui, + known_password, + }), + cx, + ) } pub async fn open_ssh_project( @@ -420,22 +419,25 @@ pub async fn open_ssh_project( })? }; - let session = window - .update(cx, |workspace, cx| { - cx.activate_window(); - workspace.toggle_modal(cx, |cx| SshConnectionModal::new(&connection_options, cx)); - let ui = workspace - .active_modal::(cx) - .unwrap() - .read(cx) - .prompt - .clone(); - connect_over_ssh(connection_options.clone(), ui, cx) - })? - .await?; + let delegate = window.update(cx, |workspace, cx| { + cx.activate_window(); + workspace.toggle_modal(cx, |cx| SshConnectionModal::new(&connection_options, cx)); + let ui = workspace + .active_modal::(cx) + .unwrap() + .read(cx) + .prompt + .clone(); + + Arc::new(SshClientDelegate { + window: cx.window_handle(), + ui, + known_password: connection_options.password.clone(), + }) + })?; cx.update(|cx| { - workspace::open_ssh_project(window, connection_options, session, app_state, paths, cx) + workspace::open_ssh_project(window, connection_options, delegate, app_state, paths, cx) })? .await } diff --git a/crates/remote/src/protocol.rs b/crates/remote/src/protocol.rs index bc495be4e7..311385f73b 100644 --- a/crates/remote/src/protocol.rs +++ b/crates/remote/src/protocol.rs @@ -49,3 +49,17 @@ pub async fn write_message( stream.write_all(buffer).await?; Ok(()) } + +pub async fn read_message_raw( + stream: &mut S, + buffer: &mut Vec, +) -> Result<()> { + buffer.resize(MESSAGE_LEN_SIZE, 0); + stream.read_exact(buffer).await?; + + let message_len = message_len_from_buffer(buffer); + buffer.resize(message_len as usize, 0); + stream.read_exact(buffer).await?; + + Ok(()) +} diff --git a/crates/remote/src/ssh_session.rs b/crates/remote/src/ssh_session.rs index 32d5536b32..05208dabd7 100644 --- a/crates/remote/src/ssh_session.rs +++ b/crates/remote/src/ssh_session.rs @@ -15,7 +15,9 @@ use futures::{ select_biased, AsyncReadExt as _, AsyncWriteExt as _, Future, FutureExt as _, SinkExt, StreamExt as _, }; -use gpui::{AppContext, AsyncAppContext, Model, SemanticVersion, Task}; +use gpui::{ + AppContext, AsyncAppContext, Context, Model, ModelContext, SemanticVersion, Task, WeakModel, +}; use parking_lot::Mutex; use rpc::{ proto::{self, build_typed_envelope, Envelope, EnvelopedMessage, PeerId, RequestMessage}, @@ -28,10 +30,11 @@ use smol::{ use std::{ any::TypeId, ffi::OsStr, + mem, path::{Path, PathBuf}, sync::{ atomic::{AtomicU32, Ordering::SeqCst}, - Arc, Weak, + Arc, }, time::Instant, }; @@ -92,6 +95,17 @@ impl SshConnectionOptions { host } } + + // Uniquely identifies dev server projects on a remote host. Needs to be + // stable for the same dev server project. + pub fn dev_server_identifier(&self) -> String { + let mut identifier = format!("dev-server-{:?}", self.host); + if let Some(username) = self.username.as_ref() { + identifier.push('-'); + identifier.push_str(&username); + } + identifier + } } #[derive(Copy, Clone, Debug)] @@ -250,59 +264,101 @@ struct SshRemoteClientState { pub struct SshRemoteClient { client: Arc, - inner_state: Mutex>, + unique_identifier: String, connection_options: SshConnectionOptions, + inner_state: Arc>>, +} + +impl Drop for SshRemoteClient { + fn drop(&mut self) { + self.shutdown_processes(); + } } impl SshRemoteClient { - pub async fn new( + pub fn new( + unique_identifier: String, connection_options: SshConnectionOptions, delegate: Arc, - cx: &mut AsyncAppContext, - ) -> Result> { - let (outgoing_tx, outgoing_rx) = mpsc::unbounded::(); - let (incoming_tx, incoming_rx) = mpsc::unbounded::(); + cx: &AppContext, + ) -> Task>> { + cx.spawn(|mut cx| async move { + let (outgoing_tx, outgoing_rx) = mpsc::unbounded::(); + let (incoming_tx, incoming_rx) = mpsc::unbounded::(); - let client = cx.update(|cx| ChannelClient::new(incoming_rx, outgoing_tx, cx))?; - let this = Arc::new(Self { - client, - inner_state: Mutex::new(None), - connection_options: connection_options.clone(), - }); + let this = cx.new_model(|cx| { + cx.on_app_quit(|this: &mut Self, _| { + this.shutdown_processes(); + futures::future::ready(()) + }) + .detach(); - let inner_state = { - let (proxy, proxy_incoming_tx, proxy_outgoing_rx) = - ChannelForwarder::new(incoming_tx, outgoing_rx, cx); + let client = ChannelClient::new(incoming_rx, outgoing_tx, cx); + Self { + client, + unique_identifier: unique_identifier.clone(), + connection_options: SshConnectionOptions::default(), + inner_state: Arc::new(Mutex::new(None)), + } + })?; - let (ssh_connection, ssh_process) = - Self::establish_connection(connection_options, delegate.clone(), cx).await?; + let inner_state = { + let (proxy, proxy_incoming_tx, proxy_outgoing_rx) = + ChannelForwarder::new(incoming_tx, outgoing_rx, &mut cx); - let multiplex_task = Self::multiplex( - Arc::downgrade(&this), - ssh_process, - proxy_incoming_tx, - proxy_outgoing_rx, - cx, - ); + let (ssh_connection, ssh_proxy_process) = Self::establish_connection( + unique_identifier, + connection_options, + delegate.clone(), + &mut cx, + ) + .await?; - SshRemoteClientState { - ssh_connection, - delegate, - forwarder: proxy, - multiplex_task, - } - }; + let multiplex_task = Self::multiplex( + this.downgrade(), + ssh_proxy_process, + proxy_incoming_tx, + proxy_outgoing_rx, + &mut cx, + ); - this.inner_state.lock().replace(inner_state); + SshRemoteClientState { + ssh_connection, + delegate, + forwarder: proxy, + multiplex_task, + } + }; - Ok(this) + this.update(&mut cx, |this, cx| { + this.inner_state.lock().replace(inner_state); + cx.notify(); + })?; + + Ok(this) + }) } - fn reconnect(this: Arc, cx: &AsyncAppContext) -> Result<()> { - let Some(state) = this.inner_state.lock().take() else { + fn shutdown_processes(&self) { + let Some(mut state) = self.inner_state.lock().take() else { + return; + }; + log::info!("shutting down ssh processes"); + // Drop `multiplex_task` because it owns our ssh_proxy_process, which is a + // child of master_process. + let task = mem::replace(&mut state.multiplex_task, Task::ready(Ok(()))); + drop(task); + // Now drop the rest of state, which kills master process. + drop(state); + } + + fn reconnect(&self, cx: &ModelContext) -> Result<()> { + let Some(state) = self.inner_state.lock().take() else { return Err(anyhow!("reconnect is already in progress")); }; + let workspace_identifier = self.unique_identifier.clone(); + let SshRemoteClientState { mut ssh_connection, delegate, @@ -311,7 +367,7 @@ impl SshRemoteClient { } = state; drop(multiplex_task); - cx.spawn(|mut cx| async move { + cx.spawn(|this, mut cx| async move { let (incoming_tx, outgoing_rx) = proxy.into_channels().await; ssh_connection.master_process.kill()?; @@ -323,8 +379,13 @@ impl SshRemoteClient { let connection_options = ssh_connection.socket.connection_options.clone(); - let (ssh_connection, ssh_process) = - Self::establish_connection(connection_options, delegate.clone(), &mut cx).await?; + let (ssh_connection, ssh_process) = Self::establish_connection( + workspace_identifier, + connection_options, + delegate.clone(), + &mut cx, + ) + .await?; let (proxy, proxy_incoming_tx, proxy_outgoing_rx) = ChannelForwarder::new(incoming_tx, outgoing_rx, &mut cx); @@ -334,32 +395,32 @@ impl SshRemoteClient { delegate, forwarder: proxy, multiplex_task: Self::multiplex( - Arc::downgrade(&this), + this.clone(), ssh_process, proxy_incoming_tx, proxy_outgoing_rx, &mut cx, ), }; - this.inner_state.lock().replace(inner_state); - anyhow::Ok(()) + this.update(&mut cx, |this, _| { + this.inner_state.lock().replace(inner_state); + }) }) .detach(); - - anyhow::Ok(()) + Ok(()) } fn multiplex( - this: Weak, - mut ssh_process: Child, + this: WeakModel, + mut ssh_proxy_process: Child, incoming_tx: UnboundedSender, mut outgoing_rx: UnboundedReceiver, cx: &AsyncAppContext, ) -> Task> { - let mut child_stderr = ssh_process.stderr.take().unwrap(); - let mut child_stdout = ssh_process.stdout.take().unwrap(); - let mut child_stdin = ssh_process.stdin.take().unwrap(); + let mut child_stderr = ssh_proxy_process.stderr.take().unwrap(); + let mut child_stdout = ssh_proxy_process.stdout.take().unwrap(); + let mut child_stdin = ssh_proxy_process.stdin.take().unwrap(); let io_task = cx.background_executor().spawn(async move { let mut stdin_buffer = Vec::new(); @@ -385,7 +446,7 @@ impl SshRemoteClient { Ok(0) => { child_stdin.close().await?; outgoing_rx.close(); - let status = ssh_process.status().await?; + let status = ssh_proxy_process.status().await?; if !status.success() { log::error!("ssh process exited with status: {status:?}"); return Err(anyhow!("ssh process exited with non-zero status code: {:?}", status.code())); @@ -446,9 +507,9 @@ impl SshRemoteClient { if let Err(error) = result { log::warn!("ssh io task died with error: {:?}. reconnecting...", error); - if let Some(this) = this.upgrade() { - Self::reconnect(this, &mut cx).ok(); - } + this.update(&mut cx, |this, cx| { + this.reconnect(cx).ok(); + })?; } Ok(()) @@ -456,6 +517,7 @@ impl SshRemoteClient { } async fn establish_connection( + unique_identifier: String, connection_options: SshConnectionOptions, delegate: Arc, cx: &mut AsyncAppContext, @@ -479,17 +541,22 @@ impl SshRemoteClient { let socket = ssh_connection.socket.clone(); run_cmd(socket.ssh_command(&remote_binary_path).arg("version")).await?; - let ssh_process = socket + delegate.set_status(Some("Starting proxy"), cx); + + let ssh_proxy_process = socket .ssh_command(format!( - "RUST_LOG={} RUST_BACKTRACE={} {:?} run", + "RUST_LOG={} RUST_BACKTRACE={} {:?} proxy --identifier {}", std::env::var("RUST_LOG").unwrap_or_default(), std::env::var("RUST_BACKTRACE").unwrap_or_default(), remote_binary_path, + unique_identifier, )) + // IMPORTANT: we kill this process when we drop the task that uses it. + .kill_on_drop(true) .spawn() .context("failed to spawn remote server")?; - Ok((ssh_connection, ssh_process)) + Ok((ssh_connection, ssh_proxy_process)) } pub fn subscribe_to_entity(&self, remote_id: u64, entity: &Model) { @@ -514,21 +581,25 @@ impl SshRemoteClient { pub fn is_reconnect_underway(&self) -> bool { maybe!({ Some(self.inner_state.try_lock()?.is_none()) }).unwrap_or_default() } + #[cfg(any(test, feature = "test-support"))] pub fn fake( client_cx: &mut gpui::TestAppContext, server_cx: &mut gpui::TestAppContext, - ) -> (Arc, Arc) { + ) -> (Model, Arc) { + use gpui::Context; + let (server_to_client_tx, server_to_client_rx) = mpsc::unbounded(); let (client_to_server_tx, client_to_server_rx) = mpsc::unbounded(); ( client_cx.update(|cx| { let client = ChannelClient::new(server_to_client_rx, client_to_server_tx, cx); - Arc::new(Self { + cx.new_model(|_| Self { client, - inner_state: Mutex::new(None), + unique_identifier: "fake".to_string(), connection_options: SshConnectionOptions::default(), + inner_state: Arc::new(Mutex::new(None)), }) }), server_cx.update(|cx| ChannelClient::new(client_to_server_rx, server_to_client_tx, cx)), diff --git a/crates/remote_server/Cargo.toml b/crates/remote_server/Cargo.toml index b15970042d..211b76e091 100644 --- a/crates/remote_server/Cargo.toml +++ b/crates/remote_server/Cargo.toml @@ -22,25 +22,26 @@ test-support = ["fs/test-support"] [dependencies] anyhow.workspace = true +clap.workspace = true client.workspace = true env_logger.workspace = true fs.workspace = true futures.workspace = true gpui.workspace = true -node_runtime.workspace = true +language.workspace = true +languages.workspace = true log.workspace = true +node_runtime.workspace = true project.workspace = true remote.workspace = true rpc.workspace = true -settings.workspace = true serde.workspace = true serde_json.workspace = true +settings.workspace = true shellexpand.workspace = true smol.workspace = true -worktree.workspace = true -language.workspace = true -languages.workspace = true util.workspace = true +worktree.workspace = true [dev-dependencies] client = { workspace = true, features = ["test-support"] } diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index 39540b04e0..66f9ca7ed5 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -112,6 +112,7 @@ impl HeadlessProject { client.add_request_handler(cx.weak_model(), Self::handle_list_remote_directory); client.add_request_handler(cx.weak_model(), Self::handle_check_file_exists); + client.add_request_handler(cx.weak_model(), Self::handle_shutdown_remote_server); client.add_model_request_handler(Self::handle_add_worktree); client.add_model_request_handler(Self::handle_open_buffer_by_path); @@ -335,4 +336,22 @@ impl HeadlessProject { path: expanded, }) } + + pub async fn handle_shutdown_remote_server( + _this: Model, + _envelope: TypedEnvelope, + cx: AsyncAppContext, + ) -> Result { + cx.spawn(|cx| async move { + cx.update(|cx| { + // TODO: This is a hack, because in a headless project, shutdown isn't executed + // when calling quit, but it should be. + cx.shutdown(); + cx.quit(); + }) + }) + .detach(); + + Ok(proto::Ack {}) + } } diff --git a/crates/remote_server/src/main.rs b/crates/remote_server/src/main.rs index 73b8a91da1..e5582d9b1f 100644 --- a/crates/remote_server/src/main.rs +++ b/crates/remote_server/src/main.rs @@ -1,20 +1,34 @@ #![cfg_attr(target_os = "windows", allow(unused, dead_code))] -use fs::RealFs; -use futures::channel::mpsc; -use gpui::Context as _; -use remote::{ - json_log::LogRecord, - protocol::{read_message, write_message}, -}; -use remote_server::HeadlessProject; -use smol::{io::AsyncWriteExt, stream::StreamExt as _, Async}; -use std::{ - env, - io::{self, Write}, - mem, process, - sync::Arc, -}; +use anyhow::Result; +use clap::{Parser, Subcommand}; +use std::path::PathBuf; + +#[derive(Parser)] +#[command(disable_version_flag = true)] +struct Cli { + #[command(subcommand)] + command: Option, +} + +#[derive(Subcommand)] +enum Commands { + Run { + #[arg(long)] + log_file: PathBuf, + #[arg(long)] + pid_file: PathBuf, + #[arg(long)] + stdin_socket: PathBuf, + #[arg(long)] + stdout_socket: PathBuf, + }, + Proxy { + #[arg(long)] + identifier: String, + }, + Version, +} #[cfg(windows)] fn main() { @@ -22,76 +36,32 @@ fn main() { } #[cfg(not(windows))] -fn main() { - use remote::ssh_session::ChannelClient; +fn main() -> Result<()> { + use remote_server::unix::{execute_proxy, execute_run, init_logging}; - env_logger::builder() - .format(|buf, record| { - serde_json::to_writer(&mut *buf, &LogRecord::new(record))?; - buf.write_all(b"\n")?; - Ok(()) - }) - .init(); + let cli = Cli::parse(); - let subcommand = std::env::args().nth(1); - match subcommand.as_deref() { - Some("run") => {} - Some("version") => { - println!("{}", env!("ZED_PKG_VERSION")); - return; + match cli.command { + Some(Commands::Run { + log_file, + pid_file, + stdin_socket, + stdout_socket, + }) => { + init_logging(Some(log_file))?; + execute_run(pid_file, stdin_socket, stdout_socket) } - _ => { - eprintln!("usage: remote "); - process::exit(1); + Some(Commands::Proxy { identifier }) => { + init_logging(None)?; + execute_proxy(identifier) + } + Some(Commands::Version) => { + eprintln!("{}", env!("ZED_PKG_VERSION")); + Ok(()) + } + None => { + eprintln!("usage: remote "); + std::process::exit(1); } } - - gpui::App::headless().run(move |cx| { - settings::init(cx); - HeadlessProject::init(cx); - - let (incoming_tx, incoming_rx) = mpsc::unbounded(); - let (outgoing_tx, mut outgoing_rx) = mpsc::unbounded(); - - let mut stdin = Async::new(io::stdin()).unwrap(); - let mut stdout = Async::new(io::stdout()).unwrap(); - - let session = ChannelClient::new(incoming_rx, outgoing_tx, cx); - let project = cx.new_model(|cx| { - HeadlessProject::new( - session.clone(), - Arc::new(RealFs::new(Default::default(), None)), - cx, - ) - }); - - cx.background_executor() - .spawn(async move { - let mut output_buffer = Vec::new(); - while let Some(message) = outgoing_rx.next().await { - write_message(&mut stdout, &mut output_buffer, message).await?; - stdout.flush().await?; - } - anyhow::Ok(()) - }) - .detach(); - - cx.background_executor() - .spawn(async move { - let mut input_buffer = Vec::new(); - loop { - let message = match read_message(&mut stdin, &mut input_buffer).await { - Ok(message) => message, - Err(error) => { - log::warn!("error reading message: {:?}", error); - process::exit(0); - } - }; - incoming_tx.unbounded_send(message).ok(); - } - }) - .detach(); - - mem::forget(project); - }); } diff --git a/crates/remote_server/src/remote_editing_tests.rs b/crates/remote_server/src/remote_editing_tests.rs index 960b7c248c..6e962a134a 100644 --- a/crates/remote_server/src/remote_editing_tests.rs +++ b/crates/remote_server/src/remote_editing_tests.rs @@ -655,7 +655,7 @@ async fn init_test( (project, headless, fs) } -fn build_project(ssh: Arc, cx: &mut TestAppContext) -> Model { +fn build_project(ssh: Model, cx: &mut TestAppContext) -> Model { cx.update(|cx| { let settings_store = SettingsStore::test(cx); cx.set_global(settings_store); diff --git a/crates/remote_server/src/remote_server.rs b/crates/remote_server/src/remote_server.rs index 0aa36b0cd1..2321ee1c6e 100644 --- a/crates/remote_server/src/remote_server.rs +++ b/crates/remote_server/src/remote_server.rs @@ -1,5 +1,8 @@ mod headless_project; +#[cfg(not(windows))] +pub mod unix; + #[cfg(test)] mod remote_editing_tests; diff --git a/crates/remote_server/src/unix.rs b/crates/remote_server/src/unix.rs new file mode 100644 index 0000000000..74b71a2277 --- /dev/null +++ b/crates/remote_server/src/unix.rs @@ -0,0 +1,336 @@ +use crate::HeadlessProject; +use anyhow::{anyhow, Context, Result}; +use fs::RealFs; +use futures::channel::mpsc; +use futures::{select, select_biased, AsyncRead, AsyncWrite, FutureExt, SinkExt}; +use gpui::{AppContext, Context as _}; +use remote::ssh_session::ChannelClient; +use remote::{ + json_log::LogRecord, + protocol::{read_message, write_message}, +}; +use rpc::proto::Envelope; +use smol::Async; +use smol::{io::AsyncWriteExt, net::unix::UnixListener, stream::StreamExt as _}; +use std::{ + env, + io::Write, + mem, + path::{Path, PathBuf}, + sync::Arc, +}; + +pub fn init_logging(log_file: Option) -> Result<()> { + if let Some(log_file) = log_file { + let target = Box::new(if log_file.exists() { + std::fs::OpenOptions::new() + .append(true) + .open(&log_file) + .context("Failed to open log file in append mode")? + } else { + std::fs::File::create(&log_file).context("Failed to create log file")? + }); + + env_logger::Builder::from_default_env() + .target(env_logger::Target::Pipe(target)) + .init(); + } else { + env_logger::builder() + .format(|buf, record| { + serde_json::to_writer(&mut *buf, &LogRecord::new(record))?; + buf.write_all(b"\n")?; + Ok(()) + }) + .init(); + } + Ok(()) +} + +fn start_server( + stdin_listener: UnixListener, + stdout_listener: UnixListener, + cx: &mut AppContext, +) -> Arc { + // This is the server idle timeout. If no connection comes in in this timeout, the server will shut down. + const IDLE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10 * 60); + + let (incoming_tx, incoming_rx) = mpsc::unbounded::(); + let (outgoing_tx, mut outgoing_rx) = mpsc::unbounded::(); + let (app_quit_tx, mut app_quit_rx) = mpsc::unbounded::<()>(); + + cx.on_app_quit(move |_| { + let mut app_quit_tx = app_quit_tx.clone(); + async move { + app_quit_tx.send(()).await.ok(); + } + }) + .detach(); + + cx.spawn(|cx| async move { + let mut stdin_incoming = stdin_listener.incoming(); + let mut stdout_incoming = stdout_listener.incoming(); + + loop { + let streams = futures::future::join(stdin_incoming.next(), stdout_incoming.next()); + + log::info!("server: accepting new connections"); + let result = select! { + streams = streams.fuse() => { + let (Some(Ok(stdin_stream)), Some(Ok(stdout_stream))) = streams else { + break; + }; + anyhow::Ok((stdin_stream, stdout_stream)) + } + _ = futures::FutureExt::fuse(smol::Timer::after(IDLE_TIMEOUT)) => { + log::warn!("server: timed out waiting for new connections after {:?}. exiting.", IDLE_TIMEOUT); + cx.update(|cx| { + // TODO: This is a hack, because in a headless project, shutdown isn't executed + // when calling quit, but it should be. + cx.shutdown(); + cx.quit(); + })?; + break; + } + _ = app_quit_rx.next().fuse() => { + break; + } + }; + + let Ok((mut stdin_stream, mut stdout_stream)) = result else { + break; + }; + + let mut input_buffer = Vec::new(); + let mut output_buffer = Vec::new(); + loop { + select_biased! { + _ = app_quit_rx.next().fuse() => { + return anyhow::Ok(()); + } + + stdin_message = read_message(&mut stdin_stream, &mut input_buffer).fuse() => { + let message = match stdin_message { + Ok(message) => message, + Err(error) => { + log::warn!("server: error reading message on stdin: {}. exiting.", error); + break; + } + }; + if let Err(error) = incoming_tx.unbounded_send(message) { + log::error!("server: failed to send message to application: {:?}. exiting.", error); + return Err(anyhow!(error)); + } + } + + outgoing_message = outgoing_rx.next().fuse() => { + let Some(message) = outgoing_message else { + log::error!("server: stdout handler, no message"); + break; + }; + + if let Err(error) = + write_message(&mut stdout_stream, &mut output_buffer, message).await + { + log::error!("server: failed to write stdout message: {:?}", error); + break; + } + if let Err(error) = stdout_stream.flush().await { + log::error!("server: failed to flush stdout message: {:?}", error); + break; + } + } + } + } + } + anyhow::Ok(()) + }) + .detach(); + + ChannelClient::new(incoming_rx, outgoing_tx, cx) +} + +pub fn execute_run(pid_file: PathBuf, stdin_socket: PathBuf, stdout_socket: PathBuf) -> Result<()> { + write_pid_file(&pid_file) + .with_context(|| format!("failed to write pid file: {:?}", &pid_file))?; + + let stdin_listener = UnixListener::bind(stdin_socket).context("failed to bind stdin socket")?; + let stdout_listener = + UnixListener::bind(stdout_socket).context("failed to bind stdout socket")?; + + gpui::App::headless().run(move |cx| { + settings::init(cx); + HeadlessProject::init(cx); + + let session = start_server(stdin_listener, stdout_listener, cx); + let project = cx.new_model(|cx| { + HeadlessProject::new(session, Arc::new(RealFs::new(Default::default(), None)), cx) + }); + + mem::forget(project); + }); + log::info!("server: gpui app is shut down. quitting."); + Ok(()) +} + +pub fn execute_proxy(identifier: String) -> Result<()> { + log::debug!("proxy: starting up. PID: {}", std::process::id()); + + let project_dir = ensure_project_dir(&identifier)?; + + let pid_file = project_dir.join("server.pid"); + let stdin_socket = project_dir.join("stdin.sock"); + let stdout_socket = project_dir.join("stdout.sock"); + let log_file = project_dir.join("server.log"); + + let server_running = check_pid_file(&pid_file)?; + if !server_running { + spawn_server(&log_file, &pid_file, &stdin_socket, &stdout_socket)?; + }; + + let stdin_task = smol::spawn(async move { + let stdin = Async::new(std::io::stdin())?; + let stream = smol::net::unix::UnixStream::connect(stdin_socket).await?; + handle_io(stdin, stream, "stdin").await + }); + + let stdout_task: smol::Task> = smol::spawn(async move { + let stdout = Async::new(std::io::stdout())?; + let stream = smol::net::unix::UnixStream::connect(stdout_socket).await?; + handle_io(stream, stdout, "stdout").await + }); + + if let Err(forwarding_result) = + smol::block_on(async move { smol::future::race(stdin_task, stdout_task).await }) + { + log::error!( + "proxy: failed to forward messages: {:?}, terminating...", + forwarding_result + ); + return Err(forwarding_result); + } + + Ok(()) +} + +fn ensure_project_dir(identifier: &str) -> Result { + let project_dir = env::var("HOME").unwrap_or_else(|_| ".".to_string()); + let project_dir = PathBuf::from(project_dir) + .join(".local") + .join("state") + .join("zed-remote-server") + .join(identifier); + + std::fs::create_dir_all(&project_dir)?; + + Ok(project_dir) +} + +fn spawn_server( + log_file: &Path, + pid_file: &Path, + stdin_socket: &Path, + stdout_socket: &Path, +) -> Result<()> { + if stdin_socket.exists() { + std::fs::remove_file(&stdin_socket)?; + } + if stdout_socket.exists() { + std::fs::remove_file(&stdout_socket)?; + } + + let binary_name = std::env::current_exe()?; + let server_process = std::process::Command::new(binary_name) + .arg("run") + .arg("--log-file") + .arg(log_file) + .arg("--pid-file") + .arg(pid_file) + .arg("--stdin-socket") + .arg(stdin_socket) + .arg("--stdout-socket") + .arg(stdout_socket) + .spawn()?; + + log::debug!("proxy: server started. PID: {:?}", server_process.id()); + + let mut total_time_waited = std::time::Duration::from_secs(0); + let wait_duration = std::time::Duration::from_millis(20); + while !stdout_socket.exists() || !stdin_socket.exists() { + log::debug!("proxy: waiting for server to be ready to accept connections..."); + std::thread::sleep(wait_duration); + total_time_waited += wait_duration; + } + + log::info!( + "proxy: server ready to accept connections. total time waited: {:?}", + total_time_waited + ); + Ok(()) +} + +fn check_pid_file(path: &Path) -> Result { + let Some(pid) = std::fs::read_to_string(&path) + .ok() + .and_then(|contents| contents.parse::().ok()) + else { + return Ok(false); + }; + + log::debug!("proxy: Checking if process with PID {} exists...", pid); + match std::process::Command::new("kill") + .arg("-0") + .arg(pid.to_string()) + .output() + { + Ok(output) if output.status.success() => { + log::debug!("proxy: Process with PID {} exists. NOT spawning new server, but attaching to existing one.", pid); + Ok(true) + } + _ => { + log::debug!("proxy: Found PID file, but process with that PID does not exist. Removing PID file."); + std::fs::remove_file(&path).context("proxy: Failed to remove PID file")?; + Ok(false) + } + } +} + +fn write_pid_file(path: &Path) -> Result<()> { + if path.exists() { + std::fs::remove_file(path)?; + } + + std::fs::write(path, std::process::id().to_string()).context("Failed to write PID file") +} + +async fn handle_io(mut reader: R, mut writer: W, socket_name: &str) -> Result<()> +where + R: AsyncRead + Unpin, + W: AsyncWrite + Unpin, +{ + use remote::protocol::read_message_raw; + + let mut buffer = Vec::new(); + loop { + read_message_raw(&mut reader, &mut buffer) + .await + .with_context(|| format!("proxy: failed to read message from {}", socket_name))?; + + write_size_prefixed_buffer(&mut writer, &mut buffer) + .await + .with_context(|| format!("proxy: failed to write message to {}", socket_name))?; + + writer.flush().await?; + + buffer.clear(); + } +} + +async fn write_size_prefixed_buffer( + stream: &mut S, + buffer: &mut Vec, +) -> Result<()> { + let len = buffer.len() as u32; + stream.write_all(len.to_le_bytes().as_slice()).await?; + stream.write_all(buffer).await?; + Ok(()) +} diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index 81f908ce79..52dab68a2a 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -265,7 +265,7 @@ impl TitleBar { fn render_ssh_project_host(&self, cx: &mut ViewContext) -> Option { let host = self.project.read(cx).ssh_connection_string(cx)?; let meta = SharedString::from(format!("Connected to: {host}")); - let indicator_color = if self.project.read(cx).ssh_is_connected()? { + let indicator_color = if self.project.read(cx).ssh_is_connected(cx)? { Color::Success } else { Color::Warning diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index 1b998eeabe..47f6c138c8 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -51,6 +51,7 @@ postage.workspace = true project.workspace = true dev_server_projects.workspace = true task.workspace = true +release_channel.workspace = true remote.workspace = true schemars.workspace = true serde.workspace = true diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index b668a5802c..d2ccd9cd4a 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -61,7 +61,8 @@ use postage::stream::Stream; use project::{ DirectoryLister, Project, ProjectEntryId, ProjectPath, ResolvedPath, Worktree, WorktreeId, }; -use remote::{SshConnectionOptions, SshRemoteClient}; +use release_channel::ReleaseChannel; +use remote::{SshClientDelegate, SshConnectionOptions}; use serde::Deserialize; use session::AppSession; use settings::{InvalidSettingsError, Settings}; @@ -5514,22 +5515,31 @@ pub fn join_hosted_project( pub fn open_ssh_project( window: WindowHandle, connection_options: SshConnectionOptions, - session: Arc, + delegate: Arc, app_state: Arc, paths: Vec, cx: &mut AppContext, ) -> Task> { + let release_channel = ReleaseChannel::global(cx); + cx.spawn(|mut cx| async move { - let serialized_ssh_project = persistence::DB - .get_or_create_ssh_project( - connection_options.host.clone(), - connection_options.port, - paths - .iter() - .map(|path| path.to_string_lossy().to_string()) - .collect::>(), - connection_options.username.clone(), - ) + let (serialized_ssh_project, workspace_id, serialized_workspace) = + serialize_ssh_project(connection_options.clone(), paths.clone(), &cx).await?; + + let identifier_prefix = match release_channel { + ReleaseChannel::Stable => None, + _ => Some(format!("{}-", release_channel.dev_name())), + }; + let unique_identifier = format!( + "{}workspace-{}", + identifier_prefix.unwrap_or_default(), + workspace_id.0 + ); + + let session = cx + .update(|cx| { + remote::SshRemoteClient::new(unique_identifier, connection_options, delegate, cx) + })? .await?; let project = cx.update(|cx| { @@ -5561,17 +5571,6 @@ pub fn open_ssh_project( }; } - let serialized_workspace = - persistence::DB.workspace_for_ssh_project(&serialized_ssh_project); - - let workspace_id = if let Some(workspace_id) = - serialized_workspace.as_ref().map(|workspace| workspace.id) - { - workspace_id - } else { - persistence::DB.next_id().await? - }; - cx.update_window(window.into(), |_, cx| { cx.replace_root_view(|cx| { let mut workspace = @@ -5603,6 +5602,45 @@ pub fn open_ssh_project( }) } +fn serialize_ssh_project( + connection_options: SshConnectionOptions, + paths: Vec, + cx: &AsyncAppContext, +) -> Task< + Result<( + SerializedSshProject, + WorkspaceId, + Option, + )>, +> { + cx.background_executor().spawn(async move { + let serialized_ssh_project = persistence::DB + .get_or_create_ssh_project( + connection_options.host.clone(), + connection_options.port, + paths + .iter() + .map(|path| path.to_string_lossy().to_string()) + .collect::>(), + connection_options.username.clone(), + ) + .await?; + + let serialized_workspace = + persistence::DB.workspace_for_ssh_project(&serialized_ssh_project); + + let workspace_id = if let Some(workspace_id) = + serialized_workspace.as_ref().map(|workspace| workspace.id) + { + workspace_id + } else { + persistence::DB.next_id().await? + }; + + Ok((serialized_ssh_project, workspace_id, serialized_workspace)) + }) +} + pub fn join_dev_server_project( dev_server_project_id: DevServerProjectId, project_id: ProjectId, From 9c5bec5efba4927bd872bc4cb5909733062ecbbe Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Mon, 7 Oct 2024 12:24:12 +0200 Subject: [PATCH 19/28] formatting: Use project environment to find external formatters (#18611) Closes #18261 This makes sure that we find external formatters in the project environment. TODO: - [x] Use a different type for the triplet of `(buffer_handle, buffer_path, buffer_env)`. Something like `FormattableBuffer`. - [x] Test this!! Release Notes: - Fixed external formatters not being found, even when they were available in the `$PATH` of a project. --------- Co-authored-by: Bennet --- crates/project/src/lsp_store.rs | 135 ++++++++++++++++++++------------ 1 file changed, 87 insertions(+), 48 deletions(-) diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index a3763810e1..b33416228e 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -158,7 +158,7 @@ impl LocalLspStore { async fn format_locally( lsp_store: WeakModel, - mut buffers_with_paths: Vec<(Model, Option)>, + mut buffers: Vec, push_to_history: bool, trigger: FormatTrigger, mut cx: AsyncAppContext, @@ -167,22 +167,22 @@ impl LocalLspStore { // same buffer. lsp_store.update(&mut cx, |this, cx| { let this = this.as_local_mut().unwrap(); - buffers_with_paths.retain(|(buffer, _)| { + buffers.retain(|buffer| { this.buffers_being_formatted - .insert(buffer.read(cx).remote_id()) + .insert(buffer.handle.read(cx).remote_id()) }); })?; let _cleanup = defer({ let this = lsp_store.clone(); let mut cx = cx.clone(); - let buffers = &buffers_with_paths; + let buffers = &buffers; move || { this.update(&mut cx, |this, cx| { let this = this.as_local_mut().unwrap(); - for (buffer, _) in buffers { + for buffer in buffers { this.buffers_being_formatted - .remove(&buffer.read(cx).remote_id()); + .remove(&buffer.handle.read(cx).remote_id()); } }) .ok(); @@ -190,10 +190,10 @@ impl LocalLspStore { }); let mut project_transaction = ProjectTransaction::default(); - for (buffer, buffer_abs_path) in &buffers_with_paths { + for buffer in &buffers { let (primary_adapter_and_server, adapters_and_servers) = lsp_store.update(&mut cx, |lsp_store, cx| { - let buffer = buffer.read(cx); + let buffer = buffer.handle.read(cx); let adapters_and_servers = lsp_store .language_servers_for_buffer(buffer, cx) @@ -207,7 +207,7 @@ impl LocalLspStore { (primary_adapter, adapters_and_servers) })?; - let settings = buffer.update(&mut cx, |buffer, cx| { + let settings = buffer.handle.update(&mut cx, |buffer, cx| { language_settings(buffer.language(), buffer.file(), cx).clone() })?; @@ -218,13 +218,14 @@ impl LocalLspStore { let trailing_whitespace_diff = if remove_trailing_whitespace { Some( buffer + .handle .update(&mut cx, |b, cx| b.remove_trailing_whitespace(cx))? .await, ) } else { None }; - let whitespace_transaction_id = buffer.update(&mut cx, |buffer, cx| { + let whitespace_transaction_id = buffer.handle.update(&mut cx, |buffer, cx| { buffer.finalize_last_transaction(); buffer.start_transaction(); if let Some(diff) = trailing_whitespace_diff { @@ -246,7 +247,7 @@ impl LocalLspStore { &lsp_store, &adapters_and_servers, code_actions, - buffer, + &buffer.handle, push_to_history, &mut project_transaction, &mut cx, @@ -261,9 +262,9 @@ impl LocalLspStore { primary_adapter_and_server.map(|(_adapter, server)| server.clone()); let server_and_buffer = primary_language_server .as_ref() - .zip(buffer_abs_path.as_ref()); + .zip(buffer.abs_path.as_ref()); - let prettier_settings = buffer.read_with(&cx, |buffer, cx| { + let prettier_settings = buffer.handle.read_with(&cx, |buffer, cx| { language_settings(buffer.language(), buffer.file(), cx) .prettier .clone() @@ -288,7 +289,6 @@ impl LocalLspStore { server_and_buffer, lsp_store.clone(), buffer, - buffer_abs_path, &settings, &adapters_and_servers, push_to_history, @@ -302,7 +302,6 @@ impl LocalLspStore { server_and_buffer, lsp_store.clone(), buffer, - buffer_abs_path, &settings, &adapters_and_servers, push_to_history, @@ -325,7 +324,6 @@ impl LocalLspStore { server_and_buffer, lsp_store.clone(), buffer, - buffer_abs_path, &settings, &adapters_and_servers, push_to_history, @@ -351,7 +349,6 @@ impl LocalLspStore { server_and_buffer, lsp_store.clone(), buffer, - buffer_abs_path, &settings, &adapters_and_servers, push_to_history, @@ -379,7 +376,6 @@ impl LocalLspStore { server_and_buffer, lsp_store.clone(), buffer, - buffer_abs_path, &settings, &adapters_and_servers, push_to_history, @@ -393,7 +389,6 @@ impl LocalLspStore { server_and_buffer, lsp_store.clone(), buffer, - buffer_abs_path, &settings, &adapters_and_servers, push_to_history, @@ -418,7 +413,6 @@ impl LocalLspStore { server_and_buffer, lsp_store.clone(), buffer, - buffer_abs_path, &settings, &adapters_and_servers, push_to_history, @@ -438,7 +432,7 @@ impl LocalLspStore { } } - buffer.update(&mut cx, |b, cx| { + buffer.handle.update(&mut cx, |b, cx| { // If the buffer had its whitespace formatted and was edited while the language-specific // formatting was being computed, avoid applying the language-specific formatting, because // it can't be grouped with the whitespace formatting in the undo history. @@ -467,7 +461,7 @@ impl LocalLspStore { if let Some(transaction_id) = whitespace_transaction_id { b.group_until_transaction(transaction_id); - } else if let Some(transaction) = project_transaction.0.get(buffer) { + } else if let Some(transaction) = project_transaction.0.get(&buffer.handle) { b.group_until_transaction(transaction.id) } } @@ -476,7 +470,9 @@ impl LocalLspStore { if !push_to_history { b.forget_transaction(transaction.id); } - project_transaction.0.insert(buffer.clone(), transaction); + project_transaction + .0 + .insert(buffer.handle.clone(), transaction); } })?; } @@ -489,8 +485,7 @@ impl LocalLspStore { formatter: &Formatter, primary_server_and_buffer: Option<(&Arc, &PathBuf)>, lsp_store: WeakModel, - buffer: &Model, - buffer_abs_path: &Option, + buffer: &FormattableBuffer, settings: &LanguageSettings, adapters_and_servers: &[(Arc, Arc)], push_to_history: bool, @@ -514,7 +509,7 @@ impl LocalLspStore { Some(FormatOperation::Lsp( LspStore::format_via_lsp( &lsp_store, - buffer, + &buffer.handle, buffer_abs_path, language_server, settings, @@ -531,27 +526,20 @@ impl LocalLspStore { let prettier = lsp_store.update(cx, |lsp_store, _cx| { lsp_store.prettier_store().unwrap().downgrade() })?; - prettier_store::format_with_prettier(&prettier, buffer, cx) + prettier_store::format_with_prettier(&prettier, &buffer.handle, cx) .await .transpose() .ok() .flatten() } Formatter::External { command, arguments } => { - let buffer_abs_path = buffer_abs_path.as_ref().map(|path| path.as_path()); - Self::format_via_external_command( - buffer, - buffer_abs_path, - command, - arguments.as_deref(), - cx, - ) - .await - .context(format!( - "failed to format via external command {:?}", - command - ))? - .map(FormatOperation::External) + Self::format_via_external_command(buffer, command, arguments.as_deref(), cx) + .await + .context(format!( + "failed to format via external command {:?}", + command + ))? + .map(FormatOperation::External) } Formatter::CodeActions(code_actions) => { let code_actions = deserialize_code_actions(code_actions); @@ -560,7 +548,7 @@ impl LocalLspStore { &lsp_store, adapters_and_servers, code_actions, - buffer, + &buffer.handle, push_to_history, transaction, cx, @@ -574,13 +562,12 @@ impl LocalLspStore { } async fn format_via_external_command( - buffer: &Model, - buffer_abs_path: Option<&Path>, + buffer: &FormattableBuffer, command: &str, arguments: Option<&[String]>, cx: &mut AsyncAppContext, ) -> Result> { - let working_dir_path = buffer.update(cx, |buffer, cx| { + let working_dir_path = buffer.handle.update(cx, |buffer, cx| { let file = File::from_dyn(buffer.file())?; let worktree = file.worktree.read(cx); let mut worktree_path = worktree.abs_path().to_path_buf(); @@ -597,13 +584,17 @@ impl LocalLspStore { child.creation_flags(windows::Win32::System::Threading::CREATE_NO_WINDOW.0); } + if let Some(buffer_env) = buffer.env.as_ref() { + child.envs(buffer_env); + } + if let Some(working_dir_path) = working_dir_path { child.current_dir(working_dir_path); } if let Some(arguments) = arguments { child.args(arguments.iter().map(|arg| { - if let Some(buffer_abs_path) = buffer_abs_path { + if let Some(buffer_abs_path) = buffer.abs_path.as_ref() { arg.replace("{buffer_path}", &buffer_abs_path.to_string_lossy()) } else { arg.replace("{buffer_path}", "Untitled") @@ -621,7 +612,9 @@ impl LocalLspStore { .stdin .as_mut() .ok_or_else(|| anyhow!("failed to acquire stdin"))?; - let text = buffer.update(cx, |buffer, _| buffer.as_rope().clone())?; + let text = buffer + .handle + .update(cx, |buffer, _| buffer.as_rope().clone())?; for chunk in text.chunks() { stdin.write_all(chunk.as_bytes()).await?; } @@ -640,12 +633,19 @@ impl LocalLspStore { let stdout = String::from_utf8(output.stdout)?; Ok(Some( buffer + .handle .update(cx, |buffer, cx| buffer.diff(stdout, cx))? .await, )) } } +pub struct FormattableBuffer { + handle: Model, + abs_path: Option, + env: Option>, +} + pub struct RemoteLspStore { upstream_client: AnyProtoClient, upstream_project_id: u64, @@ -5028,6 +5028,28 @@ impl LspStore { .and_then(|local| local.last_formatting_failure.as_deref()) } + pub fn environment_for_buffer( + &self, + buffer: &Model, + cx: &mut ModelContext, + ) -> Shared>>> { + let worktree_id = buffer.read(cx).file().map(|file| file.worktree_id(cx)); + let worktree_abs_path = worktree_id.and_then(|worktree_id| { + self.worktree_store + .read(cx) + .worktree_for_id(worktree_id, cx) + .map(|entry| entry.read(cx).abs_path().clone()) + }); + + if let Some(environment) = &self.as_local().map(|local| local.environment.clone()) { + environment.update(cx, |env, cx| { + env.get_environment(worktree_id, worktree_abs_path, cx) + }) + } else { + Task::ready(None).shared() + } + } + pub fn format( &mut self, buffers: HashSet>, @@ -5042,14 +5064,31 @@ impl LspStore { let buffer = buffer_handle.read(cx); let buffer_abs_path = File::from_dyn(buffer.file()) .and_then(|file| file.as_local().map(|f| f.abs_path(cx))); + (buffer_handle, buffer_abs_path) }) .collect::>(); cx.spawn(move |lsp_store, mut cx| async move { + let mut formattable_buffers = Vec::with_capacity(buffers_with_paths.len()); + + for (handle, abs_path) in buffers_with_paths { + let env = lsp_store + .update(&mut cx, |lsp_store, cx| { + lsp_store.environment_for_buffer(&handle, cx) + })? + .await; + + formattable_buffers.push(FormattableBuffer { + handle, + abs_path, + env, + }); + } + let result = LocalLspStore::format_locally( lsp_store.clone(), - buffers_with_paths, + formattable_buffers, push_to_history, trigger, cx.clone(), From 5aa165c530d74afc22ca54c07f5bdfaab01847e7 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 7 Oct 2024 15:01:50 +0200 Subject: [PATCH 20/28] ssh: Overhaul remoting UI (#18727) Release Notes: - N/A --------- Co-authored-by: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> --- Cargo.lock | 1 - assets/icons/trash_alt.svg | 1 + crates/recent_projects/Cargo.toml | 1 - crates/recent_projects/src/dev_servers.rs | 601 +++++++----------- crates/recent_projects/src/ssh_connections.rs | 127 ++-- crates/ui/src/components/icon.rs | 1 + crates/ui/src/components/modal.rs | 36 +- crates/ui/src/traits/styled_ext.rs | 20 +- 8 files changed, 361 insertions(+), 427 deletions(-) create mode 100644 assets/icons/trash_alt.svg diff --git a/Cargo.lock b/Cargo.lock index 1cac85e0c7..f682d76ad5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9002,7 +9002,6 @@ dependencies = [ "gpui", "language", "log", - "markdown", "menu", "ordered-float 2.10.1", "picker", diff --git a/assets/icons/trash_alt.svg b/assets/icons/trash_alt.svg new file mode 100644 index 0000000000..6867b42147 --- /dev/null +++ b/assets/icons/trash_alt.svg @@ -0,0 +1 @@ + diff --git a/crates/recent_projects/Cargo.toml b/crates/recent_projects/Cargo.toml index da4ee210e1..2eea6321a0 100644 --- a/crates/recent_projects/Cargo.toml +++ b/crates/recent_projects/Cargo.toml @@ -22,7 +22,6 @@ futures.workspace = true fuzzy.workspace = true gpui.workspace = true log.workspace = true -markdown.workspace = true menu.workspace = true ordered-float.workspace = true picker.workspace = true diff --git a/crates/recent_projects/src/dev_servers.rs b/crates/recent_projects/src/dev_servers.rs index 722743e0ff..7761461ab5 100644 --- a/crates/recent_projects/src/dev_servers.rs +++ b/crates/recent_projects/src/dev_servers.rs @@ -8,17 +8,18 @@ use anyhow::Result; use client::Client; use dev_server_projects::{DevServer, DevServerId, DevServerProject, DevServerProjectId}; use editor::Editor; +use gpui::pulsating_between; use gpui::AsyncWindowContext; +use gpui::ClipboardItem; use gpui::PathPromptOptions; use gpui::Subscription; use gpui::Task; use gpui::WeakView; use gpui::{ - percentage, Animation, AnimationExt, AnyElement, AppContext, DismissEvent, EventEmitter, - FocusHandle, FocusableView, Model, ScrollHandle, Transformation, View, ViewContext, + percentage, Action, Animation, AnimationExt, AnyElement, AppContext, DismissEvent, + EventEmitter, FocusHandle, FocusableView, Model, ScrollHandle, Transformation, View, + ViewContext, }; -use markdown::Markdown; -use markdown::MarkdownStyle; use project::terminals::wrap_for_ssh; use project::terminals::SshCommand; use rpc::proto::RegenerateDevServerTokenResponse; @@ -35,8 +36,8 @@ use terminal_view::terminal_panel::TerminalPanel; use ui::ElevationIndex; use ui::Section; use ui::{ - prelude::*, Indicator, List, ListHeader, ListItem, Modal, ModalFooter, ModalHeader, - RadioWithLabel, Tooltip, + prelude::*, IconButtonShape, Indicator, List, ListItem, Modal, ModalFooter, ModalHeader, + Tooltip, }; use ui_input::{FieldLabelLayout, TextField}; use util::ResultExt; @@ -62,7 +63,6 @@ pub struct DevServerProjects { workspace: WeakView, project_path_input: View, dev_server_name_input: View, - markdown: View, _dev_server_subscription: Subscription, } @@ -132,26 +132,6 @@ impl DevServerProjects { ..Default::default() }); - let markdown_style = MarkdownStyle { - base_text_style: base_style, - code_block: gpui::StyleRefinement { - text: Some(gpui::TextStyleRefinement { - font_family: Some("Zed Plex Mono".into()), - ..Default::default() - }), - ..Default::default() - }, - link: gpui::TextStyleRefinement { - color: Some(Color::Accent.color(cx)), - ..Default::default() - }, - syntax: cx.theme().syntax().clone(), - selection_background_color: cx.theme().players().local().selection, - ..Default::default() - }; - let markdown = - cx.new_view(|cx| Markdown::new("".to_string(), markdown_style, None, cx, None)); - Self { mode: Mode::Default(None), focus_handle, @@ -159,7 +139,6 @@ impl DevServerProjects { dev_server_store, project_path_input, dev_server_name_input, - markdown, workspace, _dev_server_subscription: subscription, } @@ -845,7 +824,7 @@ impl DevServerProjects { }) .child({ let dev_server_id = dev_server.id; - IconButton::new("remove-dev-server", IconName::Trash) + IconButton::new("remove-dev-server", IconName::TrashAlt) .on_click(cx.listener(move |this, _, cx| { this.delete_dev_server(dev_server_id, cx) })) @@ -913,40 +892,73 @@ impl DevServerProjects { ) -> impl IntoElement { v_flex() .w_full() + .px(Spacing::Small.rems(cx) + Spacing::Small.rems(cx)) .child( - h_flex().group("ssh-server").justify_between().child( - h_flex() - .gap_2() - .child( - div() - .id(("status", ix)) - .relative() - .child(Icon::new(IconName::Server).size(IconSize::Small)), - ) - .child( - div() - .max_w(rems(26.)) - .overflow_hidden() - .whitespace_nowrap() - .child(Label::new(ssh_connection.host.clone())), - ) - .child(h_flex().visible_on_hover("ssh-server").gap_1().child({ - IconButton::new("remove-dev-server", IconName::Trash) - .on_click( - cx.listener(move |this, _, cx| this.delete_ssh_server(ix, cx)), - ) - .tooltip(|cx| Tooltip::text("Remove Dev Server", cx)) - })), - ), + h_flex() + .w_full() + .group("ssh-server") + .justify_between() + .child( + h_flex() + .gap_2() + .w_full() + .child( + div() + .id(("status", ix)) + .relative() + .child(Icon::new(IconName::Server).size(IconSize::Small)), + ) + .child( + h_flex() + .max_w(rems(26.)) + .overflow_hidden() + .whitespace_nowrap() + .child(Label::new(ssh_connection.host.clone())), + ), + ) + .child( + h_flex() + .visible_on_hover("ssh-server") + .gap_1() + .child({ + IconButton::new("copy-dev-server-address", IconName::Copy) + .icon_size(IconSize::Small) + .on_click(cx.listener(move |this, _, cx| { + this.update_settings_file(cx, move |servers, cx| { + if let Some(content) = servers + .ssh_connections + .as_ref() + .and_then(|connections| { + connections + .get(ix) + .map(|connection| connection.host.clone()) + }) + { + cx.write_to_clipboard(ClipboardItem::new_string( + content, + )); + } + }); + })) + .tooltip(|cx| Tooltip::text("Copy Server Address", cx)) + }) + .child({ + IconButton::new("remove-dev-server", IconName::TrashAlt) + .icon_size(IconSize::Small) + .on_click(cx.listener(move |this, _, cx| { + this.delete_ssh_server(ix, cx) + })) + .tooltip(|cx| Tooltip::text("Remove Dev Server", cx)) + }), + ), ) .child( v_flex() .w_full() - .bg(cx.theme().colors().background) - .border_1() + .border_l_1() .border_color(cx.theme().colors().border_variant) - .rounded_md() .my_1() + .mx_1p5() .py_0p5() .px_3() .child( @@ -956,12 +968,17 @@ impl DevServerProjects { self.render_ssh_project(ix, &ssh_connection, pix, p, cx) })) .child( - ListItem::new("new-remote_project") - .start_slot(Icon::new(IconName::Plus)) - .child(Label::new("Open folder…")) - .on_click(cx.listener(move |this, _, cx| { - this.create_ssh_project(ix, ssh_connection.clone(), cx); - })), + h_flex().child( + Button::new("new-remote_project", "Open Folder…") + .icon(IconName::Plus) + .size(ButtonSize::Default) + .style(ButtonStyle::Filled) + .layer(ElevationIndex::ModalSurface) + .icon_position(IconPosition::Start) + .on_click(cx.listener(move |this, _, cx| { + this.create_ssh_project(ix, ssh_connection.clone(), cx); + })), + ), ), ), ) @@ -978,7 +995,8 @@ impl DevServerProjects { let project = project.clone(); let server = server.clone(); ListItem::new(("remote-project", ix)) - .start_slot(Icon::new(IconName::FileTree)) + .spacing(ui::ListItemSpacing::Sparse) + .start_slot(Icon::new(IconName::Folder).color(Color::Muted)) .child(Label::new(project.paths.join(", "))) .on_click(cx.listener(move |this, _, cx| { let Some(app_state) = this @@ -1014,7 +1032,7 @@ impl DevServerProjects { .detach(); })) .end_hover_slot::(Some( - IconButton::new("remove-remote-project", IconName::Trash) + IconButton::new("remove-remote-project", IconName::TrashAlt) .on_click( cx.listener(move |this, _, cx| this.delete_ssh_project(server_ix, ix, cx)), ) @@ -1026,7 +1044,7 @@ impl DevServerProjects { fn update_settings_file( &mut self, cx: &mut ViewContext, - f: impl FnOnce(&mut RemoteSettingsContent) + Send + Sync + 'static, + f: impl FnOnce(&mut RemoteSettingsContent, &AppContext) + Send + Sync + 'static, ) { let Some(fs) = self .workspace @@ -1035,11 +1053,11 @@ impl DevServerProjects { else { return; }; - update_settings_file::(fs, cx, move |setting, _| f(setting)); + update_settings_file::(fs, cx, move |setting, cx| f(setting, cx)); } fn delete_ssh_server(&mut self, server: usize, cx: &mut ViewContext) { - self.update_settings_file(cx, move |setting| { + self.update_settings_file(cx, move |setting, _| { if let Some(connections) = setting.ssh_connections.as_mut() { connections.remove(server); } @@ -1047,7 +1065,7 @@ impl DevServerProjects { } fn delete_ssh_project(&mut self, server: usize, project: usize, cx: &mut ViewContext) { - self.update_settings_file(cx, move |setting| { + self.update_settings_file(cx, move |setting, _| { if let Some(server) = setting .ssh_connections .as_mut() @@ -1063,7 +1081,7 @@ impl DevServerProjects { connection_options: remote::SshConnectionOptions, cx: &mut ViewContext, ) { - self.update_settings_file(cx, move |setting| { + self.update_settings_file(cx, move |setting, _| { setting .ssh_connections .get_or_insert(Default::default()) @@ -1124,7 +1142,7 @@ impl DevServerProjects { }).detach(); } })) - .end_hover_slot::(Some(IconButton::new("remove-remote-project", IconName::Trash) + .end_hover_slot::(Some(IconButton::new("remove-remote-project", IconName::TrashAlt) .on_click(cx.listener(move |this, _, cx| { this.delete_dev_server_project(dev_server_project_id, cx) })) @@ -1148,250 +1166,109 @@ impl DevServerProjects { kind = NewServerKind::DirectSSH; } - let status = dev_server_id - .map(|id| self.dev_server_store.read(cx).dev_server_status(id)) - .unwrap_or_default(); - - let name = self.dev_server_name_input.update(cx, |input, cx| { + self.dev_server_name_input.update(cx, |input, cx| { input.editor().update(cx, |editor, cx| { if editor.text(cx).is_empty() { - match kind { - NewServerKind::DirectSSH => editor.set_placeholder_text("ssh host", cx), - NewServerKind::LegacySSH => editor.set_placeholder_text("ssh host", cx), - NewServerKind::Manual => editor.set_placeholder_text("example-host", cx), - } + editor.set_placeholder_text("ssh me@my.server / ssh@secret-box:2222", cx); } - editor.text(cx) }) }); - - const MANUAL_SETUP_MESSAGE: &str = - "Generate a token for this server and follow the steps to set Zed up on that machine."; - const SSH_SETUP_MESSAGE: &str = - "Enter the command you use to SSH into this server.\nFor example: `ssh me@my.server` or `ssh me@secret-box:2222`."; - - Modal::new("create-dev-server", Some(self.scroll_handle.clone())) - .header( - ModalHeader::new() - .headline("Create Dev Server") - .show_back_button(true), - ) - .section( - Section::new() - .header(if kind == NewServerKind::Manual { - "Server Name".into() - } else { - "SSH arguments".into() - }) - .child( - div() - .max_w(rems(16.)) - .child(self.dev_server_name_input.clone()), - ), - ) - .section( - Section::new_contained() - .header("Connection Method".into()) - .child( - v_flex() - .w_full() - .px_2() - .gap_y(Spacing::Large.rems(cx)) - .when(ssh_prompt.is_none(), |el| { - el.child( - v_flex() - .when(use_direct_ssh, |el| { - el.child(RadioWithLabel::new( - "use-server-name-in-ssh", - Label::new("Connect via SSH (default)"), - NewServerKind::DirectSSH == kind, - cx.listener({ - move |this, _, cx| { - if let Mode::CreateDevServer( - CreateDevServer { kind, .. }, - ) = &mut this.mode - { - *kind = NewServerKind::DirectSSH; - } - cx.notify() - } - }), - )) - }) - .when(!use_direct_ssh, |el| { - el.child(RadioWithLabel::new( - "use-server-name-in-ssh", - Label::new("Configure over SSH (default)"), - kind == NewServerKind::LegacySSH, - cx.listener({ - move |this, _, cx| { - if let Mode::CreateDevServer( - CreateDevServer { kind, .. }, - ) = &mut this.mode - { - *kind = NewServerKind::LegacySSH; - } - cx.notify() - } - }), - )) - }) - .child(RadioWithLabel::new( - "use-server-name-in-ssh", - Label::new("Configure manually"), - kind == NewServerKind::Manual, - cx.listener({ - move |this, _, cx| { - if let Mode::CreateDevServer( - CreateDevServer { kind, .. }, - ) = &mut this.mode - { - *kind = NewServerKind::Manual; - } - cx.notify() - } - }), - )), - ) - }) - .when(dev_server_id.is_none() && ssh_prompt.is_none(), |el| { - el.child( - if kind == NewServerKind::Manual { - Label::new(MANUAL_SETUP_MESSAGE) - } else { - Label::new(SSH_SETUP_MESSAGE) - } - .size(LabelSize::Small) - .color(Color::Muted), - ) - }) - .when_some(ssh_prompt, |el, ssh_prompt| el.child(ssh_prompt)) - .when(dev_server_id.is_some() && access_token.is_none(), |el| { - el.child( - if kind == NewServerKind::Manual { - Label::new( - "Note: updating the dev server generate a new token", - ) - } else { - Label::new(SSH_SETUP_MESSAGE) - } - .size(LabelSize::Small) - .color(Color::Muted), - ) - }) - .when_some(access_token.clone(), { - |el, access_token| { - el.child(self.render_dev_server_token_creating( - access_token, - name, - kind, - status, - creating, - cx, - )) - } - }), - ), - ) - .footer( - ModalFooter::new().end_slot(if status == DevServerStatus::Online { - Button::new("create-dev-server", "Done") - .style(ButtonStyle::Filled) - .layer(ElevationIndex::ModalSurface) - .on_click(cx.listener(move |this, _, cx| { - cx.focus(&this.focus_handle); - this.mode = Mode::Default(None); - cx.notify(); - })) - } else { - Button::new( - "create-dev-server", - if kind == NewServerKind::Manual { - if dev_server_id.is_some() { - "Update" - } else { - "Create" - } - } else if dev_server_id.is_some() { - "Reconnect" - } else { - "Connect" - }, - ) - .style(ButtonStyle::Filled) - .layer(ElevationIndex::ModalSurface) - .disabled(creating && dev_server_id.is_none()) - .on_click(cx.listener({ - let access_token = access_token.clone(); - move |this, _, cx| { - if kind == NewServerKind::DirectSSH { - this.create_ssh_server(cx); - return; - } - this.create_or_update_dev_server( - kind, - dev_server_id, - access_token.clone(), - cx, - ); - } - })) - }), - ) - } - - fn render_dev_server_token_creating( - &self, - access_token: String, - dev_server_name: String, - kind: NewServerKind, - status: DevServerStatus, - creating: bool, - cx: &mut ViewContext, - ) -> Div { - self.markdown.update(cx, |markdown, cx| { - if kind == NewServerKind::Manual { - markdown.reset(format!("Please log into '{}'. If you don't yet have Zed installed, run:\n```\ncurl https://zed.dev/install.sh | bash\n```\nThen, to start Zed in headless mode:\n```\nzed --dev-server-token {}\n```", dev_server_name, access_token), cx); - } else { - markdown.reset("Please wait while we connect over SSH.\n\nIf you run into problems, please [file a bug](https://github.com/zed-industries/zed), and in the meantime try using the manual setup.".to_string(), cx); - } - }); - + let theme = cx.theme(); v_flex() - .pl_2() - .pt_2() - .gap_2() - .child(v_flex().w_full().text_sm().child(self.markdown.clone())) - .map(|el| { - if status == DevServerStatus::Offline && kind != NewServerKind::Manual && !creating - { - el.child( - h_flex() - .gap_2() - .child(Icon::new(IconName::Disconnected).size(IconSize::Medium)) - .child(Label::new("Not connected")), - ) - } else if status == DevServerStatus::Offline { - el.child(Self::render_loading_spinner("Waiting for connection…")) - } else { - el.child(Label::new("🎊 Connection established!")) - } - }) - } - - fn render_loading_spinner(label: impl Into) -> Div { - h_flex() - .gap_2() + .id("create-dev-server") + .overflow_hidden() + .size_full() + .flex_1() .child( - Icon::new(IconName::ArrowCircle) - .size(IconSize::Medium) - .with_animation( - "arrow-circle", - Animation::new(Duration::from_secs(2)).repeat(), - |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), + h_flex() + .p_2() + .gap_2() + .items_center() + .border_b_1() + .border_color(theme.colors().border_variant) + .child( + IconButton::new("cancel-dev-server-creation", IconName::ArrowLeft) + .shape(IconButtonShape::Square) + .on_click(|_, cx| { + cx.dispatch_action(menu::Cancel.boxed_clone()); + }), + ) + .child(Label::new("Connect New Dev Server")), + ) + .child( + v_flex() + .p_3() + .border_b_1() + .border_color(theme.colors().border_variant) + .child(Label::new("SSH Arguments")) + .child( + Label::new("Enter the command you use to SSH into this server.") + .size(LabelSize::Small) + .color(Color::Muted), + ) + .child( + h_flex() + .mt_2() + .w_full() + .gap_2() + .child(self.dev_server_name_input.clone()) + .child( + Button::new("create-dev-server", "Connect Server") + .style(ButtonStyle::Filled) + .layer(ElevationIndex::ModalSurface) + .disabled(creating && dev_server_id.is_none()) + .on_click(cx.listener({ + let access_token = access_token.clone(); + move |this, _, cx| { + if kind == NewServerKind::DirectSSH { + this.create_ssh_server(cx); + return; + } + this.create_or_update_dev_server( + kind, + dev_server_id, + access_token.clone(), + cx, + ); + } + })), + ), ), ) - .child(Label::new(label)) + .child( + h_flex() + .bg(theme.colors().editor_background) + .w_full() + .map(|this| { + if let Some(ssh_prompt) = ssh_prompt { + this.child(h_flex().w_full().child(ssh_prompt)) + } else { + let color = Color::Muted.color(cx); + this.child( + h_flex() + .p_2() + .w_full() + .content_center() + .gap_2() + .child(h_flex().w_full()) + .child( + div().p_1().rounded_lg().bg(color).with_animation( + "pulse-ssh-waiting-for-connection", + Animation::new(Duration::from_secs(2)) + .repeat() + .with_easing(pulsating_between(0.2, 0.5)), + move |this, progress| this.bg(color.opacity(progress)), + ), + ) + .child( + Label::new("Waiting for connection…") + .size(LabelSize::Small), + ) + .child(h_flex().w_full()), + ) + } + }), + ) } fn render_default(&mut self, cx: &mut ViewContext) -> impl IntoElement { @@ -1416,64 +1293,73 @@ impl DevServerProjects { creating_dev_server = Some(*dev_server_id); }; + let footer = format!("Connections: {}", ssh_connections.len() + dev_servers.len()); Modal::new("remote-projects", Some(self.scroll_handle.clone())) .header( - ModalHeader::new() - .show_dismiss_button(true) - .child(Headline::new("Remote Projects (alpha)").size(HeadlineSize::Small)), + ModalHeader::new().child( + h_flex() + .justify_between() + .child(Headline::new("Remote Projects (alpha)").size(HeadlineSize::XSmall)) + .child( + Button::new("register-dev-server-button", "Connect New Server") + .style(ButtonStyle::Filled) + .layer(ElevationIndex::ModalSurface) + .icon(IconName::Plus) + .icon_position(IconPosition::Start) + .icon_color(Color::Muted) + .on_click(cx.listener(|this, _, cx| { + this.mode = Mode::CreateDevServer(CreateDevServer { + kind: if SshSettings::get_global(cx).use_direct_ssh() { + NewServerKind::DirectSSH + } else { + NewServerKind::LegacySSH + }, + ..Default::default() + }); + this.dev_server_name_input.update(cx, |text_field, cx| { + text_field.editor().update(cx, |editor, cx| { + editor.set_text("", cx); + }); + }); + cx.notify(); + })), + ), + ), ) .section( - Section::new().child( - div().child( - List::new() - .empty_message("No dev servers registered yet.") - .header(Some( - ListHeader::new("Connections").end_slot( - Button::new("register-dev-server-button", "Connect New Server") - .icon(IconName::Plus) - .icon_position(IconPosition::Start) - .icon_color(Color::Muted) - .on_click(cx.listener(|this, _, cx| { - this.mode = Mode::CreateDevServer(CreateDevServer { - kind: if SshSettings::get_global(cx) - .use_direct_ssh() - { - NewServerKind::DirectSSH - } else { - NewServerKind::LegacySSH - }, - ..Default::default() - }); - this.dev_server_name_input.update( - cx, - |text_field, cx| { - text_field.editor().update(cx, |editor, cx| { - editor.set_text("", cx); - }); - }, - ); - cx.notify(); - })), - ), - )) - .children(ssh_connections.iter().cloned().enumerate().map( - |(ix, connection)| { - self.render_ssh_connection(ix, connection, cx) - .into_any_element() - }, - )) - .children(dev_servers.iter().map(|dev_server| { - let creating = if creating_dev_server == Some(dev_server.id) { - is_creating - } else { - None - }; - self.render_dev_server(dev_server, creating, cx) - .into_any_element() - })), - ), + Section::new().padded(false).child( + div() + .border_y_1() + .border_color(cx.theme().colors().border_variant) + .w_full() + .child( + div().p_2().child( + List::new() + .empty_message("No dev servers registered yet.") + .children(ssh_connections.iter().cloned().enumerate().map( + |(ix, connection)| { + self.render_ssh_connection(ix, connection, cx) + .into_any_element() + }, + )) + .children(dev_servers.iter().map(|dev_server| { + let creating = if creating_dev_server == Some(dev_server.id) + { + is_creating + } else { + None + }; + self.render_dev_server(dev_server, creating, cx) + .into_any_element() + })), + ), + ), ), ) + .footer( + ModalFooter::new() + .start_slot(div().child(Label::new(footer).size(LabelSize::Small))), + ) } } @@ -1501,7 +1387,6 @@ impl Render for DevServerProjects { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { div() .track_focus(&self.focus_handle) - .p_2() .elevation_3(cx) .key_context("DevServerModal") .on_action(cx.listener(Self::cancel)) diff --git a/crates/recent_projects/src/ssh_connections.rs b/crates/recent_projects/src/ssh_connections.rs index 9e50523773..554146eab2 100644 --- a/crates/recent_projects/src/ssh_connections.rs +++ b/crates/recent_projects/src/ssh_connections.rs @@ -5,9 +5,9 @@ use auto_update::AutoUpdater; use editor::Editor; use futures::channel::oneshot; use gpui::{ - percentage, px, Animation, AnimationExt, AnyWindowHandle, AsyncAppContext, DismissEvent, - EventEmitter, FocusableView, ParentElement as _, Render, SemanticVersion, SharedString, Task, - Transformation, View, + percentage, px, Action, Animation, AnimationExt, AnyWindowHandle, AsyncAppContext, + DismissEvent, EventEmitter, FocusableView, ParentElement as _, Render, SemanticVersion, + SharedString, Task, Transformation, View, }; use gpui::{AppContext, Model}; use release_channel::{AppVersion, ReleaseChannel}; @@ -16,9 +16,9 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsSources}; use ui::{ - h_flex, v_flex, Color, FluentBuilder as _, Icon, IconName, IconSize, InteractiveElement, - IntoElement, Label, LabelCommon, Styled, StyledExt as _, ViewContext, VisualContext, - WindowContext, + div, h_flex, v_flex, ActiveTheme, ButtonCommon, Clickable, Color, FluentBuilder as _, Icon, + IconButton, IconName, IconSize, InteractiveElement, IntoElement, Label, LabelCommon, Styled, + StyledExt as _, Tooltip, ViewContext, VisualContext, WindowContext, }; use workspace::{AppState, ModalView, Workspace}; @@ -140,47 +140,57 @@ impl SshPrompt { } impl Render for SshPrompt { - fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { + fn render(&mut self, _: &mut ViewContext) -> impl IntoElement { v_flex() + .w_full() .key_context("PasswordPrompt") - .p_4() - .size_full() + .justify_start() .child( - h_flex() - .gap_2() - .child(if self.error_message.is_some() { - Icon::new(IconName::XCircle) - .size(IconSize::Medium) - .color(Color::Error) - .into_any_element() - } else { - Icon::new(IconName::ArrowCircle) - .size(IconSize::Medium) - .with_animation( - "arrow-circle", - Animation::new(Duration::from_secs(2)).repeat(), - |icon, delta| { - icon.transform(Transformation::rotate(percentage(delta))) - }, - ) - .into_any_element() - }) + v_flex() + .p_4() + .size_full() .child( - Label::new(format!("ssh {}…", self.connection_string)) - .size(ui::LabelSize::Large), - ), + h_flex() + .gap_2() + .justify_between() + .child(h_flex().w_full()) + .child(if self.error_message.is_some() { + Icon::new(IconName::XCircle) + .size(IconSize::Medium) + .color(Color::Error) + .into_any_element() + } else { + Icon::new(IconName::ArrowCircle) + .size(IconSize::Medium) + .with_animation( + "arrow-circle", + Animation::new(Duration::from_secs(2)).repeat(), + |icon, delta| { + icon.transform(Transformation::rotate(percentage( + delta, + ))) + }, + ) + .into_any_element() + }) + .child(Label::new(format!( + "Connecting to {}…", + self.connection_string + ))) + .child(h_flex().w_full()), + ) + .when_some(self.error_message.as_ref(), |el, error| { + el.child(Label::new(error.clone())) + }) + .when( + self.error_message.is_none() && self.status_message.is_some(), + |el| el.child(Label::new(self.status_message.clone().unwrap())), + ) + .when_some(self.prompt.as_ref(), |el, prompt| { + el.child(Label::new(prompt.0.clone())) + .child(self.editor.clone()) + }), ) - .when_some(self.error_message.as_ref(), |el, error| { - el.child(Label::new(error.clone())) - }) - .when( - self.error_message.is_none() && self.status_message.is_some(), - |el| el.child(Label::new(self.status_message.clone().unwrap())), - ) - .when_some(self.prompt.as_ref(), |el, prompt| { - el.child(Label::new(prompt.0.clone())) - .child(self.editor.clone()) - }) } } @@ -202,14 +212,41 @@ impl SshConnectionModal { impl Render for SshConnectionModal { fn render(&mut self, cx: &mut ui::ViewContext) -> impl ui::IntoElement { + let connection_string = self.prompt.read(cx).connection_string.clone(); + let theme = cx.theme(); + let header_color = theme.colors().element_background; + let body_color = theme.colors().background; v_flex() .elevation_3(cx) - .p_4() - .gap_2() .on_action(cx.listener(Self::dismiss)) .on_action(cx.listener(Self::confirm)) .w(px(400.)) - .child(self.prompt.clone()) + .child( + h_flex() + .p_1() + .border_b_1() + .border_color(theme.colors().border) + .bg(header_color) + .justify_between() + .child( + IconButton::new("ssh-connection-cancel", IconName::ArrowLeft) + .icon_size(IconSize::XSmall) + .on_click(|_, cx| cx.dispatch_action(menu::Cancel.boxed_clone())) + .tooltip(|cx| Tooltip::for_action("Back", &menu::Cancel, cx)), + ) + .child( + h_flex() + .gap_2() + .child(Icon::new(IconName::Server).size(IconSize::XSmall)) + .child( + Label::new(connection_string) + .size(ui::LabelSize::Small) + .single_line(), + ), + ) + .child(div()), + ) + .child(h_flex().bg(body_color).w_full().child(self.prompt.clone())) } } diff --git a/crates/ui/src/components/icon.rs b/crates/ui/src/components/icon.rs index 693caaaafd..8d374ef67c 100644 --- a/crates/ui/src/components/icon.rs +++ b/crates/ui/src/components/icon.rs @@ -275,6 +275,7 @@ pub enum IconName { Tab, Terminal, Trash, + TrashAlt, TriangleRight, Undo, Unpin, diff --git a/crates/ui/src/components/modal.rs b/crates/ui/src/components/modal.rs index 11611f9c0f..512f9601a8 100644 --- a/crates/ui/src/components/modal.rs +++ b/crates/ui/src/components/modal.rs @@ -262,6 +262,7 @@ impl RenderOnce for ModalFooter { #[derive(IntoElement)] pub struct Section { contained: bool, + padded: bool, header: Option, meta: Option, children: SmallVec<[AnyElement; 2]>, @@ -277,6 +278,7 @@ impl Section { pub fn new() -> Self { Self { contained: false, + padded: true, header: None, meta: None, children: SmallVec::new(), @@ -286,6 +288,7 @@ impl Section { pub fn new_contained() -> Self { Self { contained: true, + padded: true, header: None, meta: None, children: SmallVec::new(), @@ -306,6 +309,10 @@ impl Section { self.meta = Some(meta.into()); self } + pub fn padded(mut self, padded: bool) -> Self { + self.padded = padded; + self + } } impl ParentElement for Section { @@ -320,22 +327,27 @@ impl RenderOnce for Section { section_bg.fade_out(0.96); let children = if self.contained { - v_flex().flex_1().px(Spacing::XLarge.rems(cx)).child( - v_flex() - .w_full() - .rounded_md() - .border_1() - .border_color(cx.theme().colors().border) - .bg(section_bg) - .py(Spacing::Medium.rems(cx)) - .gap_y(Spacing::Small.rems(cx)) - .child(div().flex().flex_1().size_full().children(self.children)), - ) + v_flex() + .flex_1() + .when(self.padded, |this| this.px(Spacing::XLarge.rems(cx))) + .child( + v_flex() + .w_full() + .rounded_md() + .border_1() + .border_color(cx.theme().colors().border) + .bg(section_bg) + .py(Spacing::Medium.rems(cx)) + .gap_y(Spacing::Small.rems(cx)) + .child(div().flex().flex_1().size_full().children(self.children)), + ) } else { v_flex() .w_full() .gap_y(Spacing::Small.rems(cx)) - .px(Spacing::Medium.rems(cx) + Spacing::Medium.rems(cx)) + .when(self.padded, |this| { + this.px(Spacing::Medium.rems(cx) + Spacing::Medium.rems(cx)) + }) .children(self.children) }; diff --git a/crates/ui/src/traits/styled_ext.rs b/crates/ui/src/traits/styled_ext.rs index 997e80ca86..09d8a4f74f 100644 --- a/crates/ui/src/traits/styled_ext.rs +++ b/crates/ui/src/traits/styled_ext.rs @@ -3,7 +3,7 @@ use gpui::{hsla, Styled, WindowContext}; use crate::prelude::*; use crate::ElevationIndex; -fn elevated(this: E, cx: &mut WindowContext, index: ElevationIndex) -> E { +fn elevated(this: E, cx: &WindowContext, index: ElevationIndex) -> E { this.bg(cx.theme().colors().elevated_surface_background) .rounded_lg() .border_1() @@ -11,7 +11,7 @@ fn elevated(this: E, cx: &mut WindowContext, index: ElevationIndex) - .shadow(index.shadow()) } -fn elevated_borderless(this: E, cx: &mut WindowContext, index: ElevationIndex) -> E { +fn elevated_borderless(this: E, cx: &WindowContext, index: ElevationIndex) -> E { this.bg(cx.theme().colors().elevated_surface_background) .rounded_lg() .shadow(index.shadow()) @@ -38,14 +38,14 @@ pub trait StyledExt: Styled + Sized { /// Sets `bg()`, `rounded_lg()`, `border()`, `border_color()`, `shadow()` /// /// Example Elements: Title Bar, Panel, Tab Bar, Editor - fn elevation_1(self, cx: &mut WindowContext) -> Self { + fn elevation_1(self, cx: &WindowContext) -> Self { elevated(self, cx, ElevationIndex::Surface) } /// See [`elevation_1`]. /// /// Renders a borderless version [`elevation_1`]. - fn elevation_1_borderless(self, cx: &mut WindowContext) -> Self { + fn elevation_1_borderless(self, cx: &WindowContext) -> Self { elevated_borderless(self, cx, ElevationIndex::Surface) } @@ -54,14 +54,14 @@ pub trait StyledExt: Styled + Sized { /// Sets `bg()`, `rounded_lg()`, `border()`, `border_color()`, `shadow()` /// /// Examples: Notifications, Palettes, Detached/Floating Windows, Detached/Floating Panels - fn elevation_2(self, cx: &mut WindowContext) -> Self { + fn elevation_2(self, cx: &WindowContext) -> Self { elevated(self, cx, ElevationIndex::ElevatedSurface) } /// See [`elevation_2`]. /// /// Renders a borderless version [`elevation_2`]. - fn elevation_2_borderless(self, cx: &mut WindowContext) -> Self { + fn elevation_2_borderless(self, cx: &WindowContext) -> Self { elevated_borderless(self, cx, ElevationIndex::ElevatedSurface) } @@ -74,24 +74,24 @@ pub trait StyledExt: Styled + Sized { /// Sets `bg()`, `rounded_lg()`, `border()`, `border_color()`, `shadow()` /// /// Examples: Settings Modal, Channel Management, Wizards/Setup UI, Dialogs - fn elevation_3(self, cx: &mut WindowContext) -> Self { + fn elevation_3(self, cx: &WindowContext) -> Self { elevated(self, cx, ElevationIndex::ModalSurface) } /// See [`elevation_3`]. /// /// Renders a borderless version [`elevation_3`]. - fn elevation_3_borderless(self, cx: &mut WindowContext) -> Self { + fn elevation_3_borderless(self, cx: &WindowContext) -> Self { elevated_borderless(self, cx, ElevationIndex::ModalSurface) } /// The theme's primary border color. - fn border_primary(self, cx: &mut WindowContext) -> Self { + fn border_primary(self, cx: &WindowContext) -> Self { self.border_color(cx.theme().colors().border) } /// The theme's secondary or muted border color. - fn border_muted(self, cx: &mut WindowContext) -> Self { + fn border_muted(self, cx: &WindowContext) -> Self { self.border_color(cx.theme().colors().border_variant) } From 25a97a6a2be277c2c0897a651658254de34d0bcd Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Mon, 7 Oct 2024 15:08:16 +0200 Subject: [PATCH 21/28] ssh: Detect timeouts when server is unresponsive (#18808) To detect connection timeouts we ping the remote server every X seconds and attempt to reconnect if the server failed to respond. Next up is showing some feedback in the UI to make this visible to the user, and stop reconnecting after X amount of retries. Release Notes: - N/A --------- Co-authored-by: Thorsten --- crates/remote/src/ssh_session.rs | 75 +++++++++++++++++++- crates/remote_server/src/headless_project.rs | 10 +++ 2 files changed, 82 insertions(+), 3 deletions(-) diff --git a/crates/remote/src/ssh_session.rs b/crates/remote/src/ssh_session.rs index 05208dabd7..0a1cd00992 100644 --- a/crates/remote/src/ssh_session.rs +++ b/crates/remote/src/ssh_session.rs @@ -26,6 +26,7 @@ use rpc::{ use smol::{ fs, process::{self, Child, Stdio}, + Timer, }; use std::{ any::TypeId, @@ -36,7 +37,7 @@ use std::{ atomic::{AtomicU32, Ordering::SeqCst}, Arc, }, - time::Instant, + time::{Duration, Instant}, }; use tempfile::TempDir; use util::maybe; @@ -173,7 +174,7 @@ async fn run_cmd(command: &mut process::Command) -> Result { #[cfg(unix)] async fn read_with_timeout( stdout: &mut process::ChildStdout, - timeout: std::time::Duration, + timeout: Duration, output: &mut Vec, ) -> Result<(), std::io::Error> { smol::future::or( @@ -260,6 +261,7 @@ struct SshRemoteClientState { delegate: Arc, forwarder: ChannelForwarder, multiplex_task: Task>, + heartbeat_task: Task>, } pub struct SshRemoteClient { @@ -327,6 +329,7 @@ impl SshRemoteClient { delegate, forwarder: proxy, multiplex_task, + heartbeat_task: Self::heartbeat(this.downgrade(), &mut cx), } }; @@ -353,6 +356,7 @@ impl SshRemoteClient { } fn reconnect(&self, cx: &ModelContext) -> Result<()> { + log::info!("Trying to reconnect to ssh server..."); let Some(state) = self.inner_state.lock().take() else { return Err(anyhow!("reconnect is already in progress")); }; @@ -364,8 +368,10 @@ impl SshRemoteClient { delegate, forwarder: proxy, multiplex_task, + heartbeat_task, } = state; drop(multiplex_task); + drop(heartbeat_task); cx.spawn(|this, mut cx| async move { let (incoming_tx, outgoing_rx) = proxy.into_channels().await; @@ -401,6 +407,7 @@ impl SshRemoteClient { proxy_outgoing_rx, &mut cx, ), + heartbeat_task: Self::heartbeat(this.clone(), &mut cx), }; this.update(&mut cx, |this, _| { @@ -411,6 +418,68 @@ impl SshRemoteClient { Ok(()) } + fn heartbeat(this: WeakModel, cx: &mut AsyncAppContext) -> Task> { + let Ok(client) = this.update(cx, |this, _| this.client.clone()) else { + return Task::ready(Err(anyhow!("SshRemoteClient lost"))); + }; + cx.spawn(|mut cx| { + let this = this.clone(); + async move { + const MAX_MISSED_HEARTBEATS: usize = 5; + const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5); + const HEARTBEAT_TIMEOUT: Duration = Duration::from_secs(5); + + let mut missed_heartbeats = 0; + + let mut timer = Timer::interval(HEARTBEAT_INTERVAL); + loop { + timer.next().await; + + log::info!("Sending heartbeat to server..."); + + let result = smol::future::or( + async { + client.request(proto::Ping {}).await?; + Ok(()) + }, + async { + smol::Timer::after(HEARTBEAT_TIMEOUT).await; + + Err(anyhow!("Timeout detected")) + }, + ) + .await; + + if result.is_err() { + missed_heartbeats += 1; + log::warn!( + "No heartbeat from server after {:?}. Missed heartbeat {} out of {}.", + HEARTBEAT_TIMEOUT, + missed_heartbeats, + MAX_MISSED_HEARTBEATS + ); + } else { + missed_heartbeats = 0; + } + + if missed_heartbeats >= MAX_MISSED_HEARTBEATS { + log::error!( + "Missed last {} hearbeats. Reconnecting...", + missed_heartbeats + ); + + this.update(&mut cx, |this, cx| { + this.reconnect(cx) + .context("failed to reconnect after missing heartbeats") + }) + .context("failed to update weak reference, SshRemoteClient lost?")??; + return Ok(()); + } + } + } + }) + } + fn multiplex( this: WeakModel, mut ssh_proxy_process: Child, @@ -712,7 +781,7 @@ impl SshRemoteConnection { // has completed. let stdout = master_process.stdout.as_mut().unwrap(); let mut output = Vec::new(); - let connection_timeout = std::time::Duration::from_secs(10); + let connection_timeout = Duration::from_secs(10); let result = read_with_timeout(stdout, connection_timeout, &mut output).await; if let Err(e) = result { let error_message = if e.kind() == std::io::ErrorKind::TimedOut { diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index 66f9ca7ed5..0ad16caacc 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -113,6 +113,7 @@ impl HeadlessProject { client.add_request_handler(cx.weak_model(), Self::handle_list_remote_directory); client.add_request_handler(cx.weak_model(), Self::handle_check_file_exists); client.add_request_handler(cx.weak_model(), Self::handle_shutdown_remote_server); + client.add_request_handler(cx.weak_model(), Self::handle_ping); client.add_model_request_handler(Self::handle_add_worktree); client.add_model_request_handler(Self::handle_open_buffer_by_path); @@ -354,4 +355,13 @@ impl HeadlessProject { Ok(proto::Ack {}) } + + pub async fn handle_ping( + _this: Model, + _envelope: TypedEnvelope, + _cx: AsyncAppContext, + ) -> Result { + log::debug!("Received ping from client"); + Ok(proto::Ack {}) + } } From 65c9b157969a8db7e722c3be9f80f4dc8ad43f97 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 7 Oct 2024 09:23:40 -0400 Subject: [PATCH 22/28] Remove avatar shape (#18810) This PR re-removes `AvatarShape` as it is unused. The previous time it was removed incorrectly, resulting in square avatars! Release Notes: - N/A --- crates/ui/src/components/avatar/avatar.rs | 44 +++-------------------- 1 file changed, 4 insertions(+), 40 deletions(-) diff --git a/crates/ui/src/components/avatar/avatar.rs b/crates/ui/src/components/avatar/avatar.rs index f0e516c7b1..ebacd334fc 100644 --- a/crates/ui/src/components/avatar/avatar.rs +++ b/crates/ui/src/components/avatar/avatar.rs @@ -2,16 +2,6 @@ use crate::prelude::*; use gpui::{img, AnyElement, Hsla, ImageSource, Img, IntoElement, Styled}; -/// The shape of an [`Avatar`]. -#[derive(Debug, Default, PartialEq, Clone)] -pub enum AvatarShape { - /// The avatar is shown in a circle. - #[default] - Circle, - /// The avatar is shown in a rectangle with rounded corners. - RoundedRectangle, -} - /// An element that renders a user avatar with customizable appearance options. /// /// # Examples @@ -43,27 +33,6 @@ impl Avatar { } } - /// Sets the shape of the avatar image. - /// - /// This method allows the shape of the avatar to be specified using an [`AvatarShape`]. - /// It modifies the corner radius of the image to match the specified shape. - /// - /// # Examples - /// - /// ``` - /// use ui::{Avatar, AvatarShape}; - /// - /// Avatar::new("path/to/image.png").shape(AvatarShape::Circle); - /// ``` - /// - pub fn shape(mut self, shape: AvatarShape) -> Self { - self.image = match shape { - AvatarShape::Circle => self.image.rounded_full(), - AvatarShape::RoundedRectangle => self.image.rounded_md(), - }; - self - } - /// Applies a grayscale filter to the avatar image. /// /// # Examples @@ -102,11 +71,7 @@ impl Avatar { } impl RenderOnce for Avatar { - fn render(mut self, cx: &mut WindowContext) -> impl IntoElement { - if self.image.style().corner_radii.top_left.is_none() { - self = self.shape(AvatarShape::Circle); - } - + fn render(self, cx: &mut WindowContext) -> impl IntoElement { let border_width = if self.border_color.is_some() { px(2.) } else { @@ -118,16 +83,15 @@ impl RenderOnce for Avatar { div() .size(container_size) - .map(|mut div| { - div.style().corner_radii = self.image.style().corner_radii.clone(); - div - }) + .rounded_full() + .overflow_hidden() .when_some(self.border_color, |this, color| { this.border(border_width).border_color(color) }) .child( self.image .size(image_size) + .rounded_full() .bg(cx.theme().colors().ghost_element_background), ) .children(self.indicator.map(|indicator| div().child(indicator))) From a3b63448df2ff3e934b7600196572ac44ecf2c56 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Mon, 7 Oct 2024 15:53:32 +0200 Subject: [PATCH 23/28] ssh: Do not cancel connection process if user is typing password (#18812) Previously, the connection process would be cancelled after 10 seconds, even if the connection was established successfully but the user was still typing in a password. We know recognize when the user is prompted for a password, and cancel the timeout task. Co-Authored-by: Thorsten Release Notes: - N/A --------- Co-authored-by: Thorsten --- crates/remote/src/ssh_session.rs | 67 ++++++++++++++------------------ 1 file changed, 30 insertions(+), 37 deletions(-) diff --git a/crates/remote/src/ssh_session.rs b/crates/remote/src/ssh_session.rs index 0a1cd00992..26ef8626ec 100644 --- a/crates/remote/src/ssh_session.rs +++ b/crates/remote/src/ssh_session.rs @@ -171,28 +171,6 @@ async fn run_cmd(command: &mut process::Command) -> Result { )) } } -#[cfg(unix)] -async fn read_with_timeout( - stdout: &mut process::ChildStdout, - timeout: Duration, - output: &mut Vec, -) -> Result<(), std::io::Error> { - smol::future::or( - async { - stdout.read_to_end(output).await?; - Ok::<_, std::io::Error>(()) - }, - async { - smol::Timer::after(timeout).await; - - Err(std::io::Error::new( - std::io::ErrorKind::TimedOut, - "Read operation timed out", - )) - }, - ) - .await -} struct ChannelForwarder { quit_tx: UnboundedSender<()>, @@ -725,13 +703,19 @@ impl SshRemoteConnection { // Create a domain socket listener to handle requests from the askpass program. let askpass_socket = temp_dir.path().join("askpass.sock"); + let (askpass_opened_tx, askpass_opened_rx) = oneshot::channel::<()>(); let listener = UnixListener::bind(&askpass_socket).context("failed to create askpass socket")?; let askpass_task = cx.spawn({ let delegate = delegate.clone(); |mut cx| async move { + let mut askpass_opened_tx = Some(askpass_opened_tx); + while let Ok((mut stream, _)) = listener.accept().await { + if let Some(askpass_opened_tx) = askpass_opened_tx.take() { + askpass_opened_tx.send(()).ok(); + } let mut buffer = Vec::new(); let mut reader = BufReader::new(&mut stream); if reader.read_until(b'\0', &mut buffer).await.is_err() { @@ -782,19 +766,28 @@ impl SshRemoteConnection { let stdout = master_process.stdout.as_mut().unwrap(); let mut output = Vec::new(); let connection_timeout = Duration::from_secs(10); - let result = read_with_timeout(stdout, connection_timeout, &mut output).await; - if let Err(e) = result { - let error_message = if e.kind() == std::io::ErrorKind::TimedOut { - format!( - "Failed to connect to host. Timed out after {:?}.", - connection_timeout - ) - } else { - format!("Failed to connect to host: {}.", e) - }; + let result = select_biased! { + _ = askpass_opened_rx.fuse() => { + // If the askpass script has opened, that means the user is typing + // their password, in which case we don't want to timeout anymore, + // since we know a connection has been established. + stdout.read_to_end(&mut output).await?; + Ok(()) + } + result = stdout.read_to_end(&mut output).fuse() => { + result?; + Ok(()) + } + _ = futures::FutureExt::fuse(smol::Timer::after(connection_timeout)) => { + Err(anyhow!("Exceeded {:?} timeout trying to connect to host", connection_timeout)) + } + }; + + if let Err(e) = result { + let error_message = format!("Failed to connect to host: {}.", e); delegate.set_error(error_message, cx); - return Err(e.into()); + return Err(e); } drop(askpass_task); @@ -803,10 +796,10 @@ impl SshRemoteConnection { output.clear(); let mut stderr = master_process.stderr.take().unwrap(); stderr.read_to_end(&mut output).await?; - Err(anyhow!( - "failed to connect: {}", - String::from_utf8_lossy(&output) - ))?; + + let error_message = format!("failed to connect: {}", String::from_utf8_lossy(&output)); + delegate.set_error(error_message.clone(), cx); + Err(anyhow!(error_message))?; } Ok(Self { From 7c7151551a69bd6c925d2b896f84ec8fd2411476 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 7 Oct 2024 10:11:12 -0400 Subject: [PATCH 24/28] proto: Bump to v0.2.0 (#18814) This PR bumps the Protobuf extension to v0.2.0. Changes: - https://github.com/zed-industries/zed/pull/18763 Release Notes: - N/A --- Cargo.lock | 2 +- extensions/proto/Cargo.toml | 2 +- extensions/proto/extension.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f682d76ad5..d3e76eefd2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14866,7 +14866,7 @@ dependencies = [ [[package]] name = "zed_proto" -version = "0.1.0" +version = "0.2.0" dependencies = [ "zed_extension_api 0.1.0", ] diff --git a/extensions/proto/Cargo.toml b/extensions/proto/Cargo.toml index 496099c526..215a09f896 100644 --- a/extensions/proto/Cargo.toml +++ b/extensions/proto/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zed_proto" -version = "0.1.0" +version = "0.2.0" edition = "2021" publish = false license = "Apache-2.0" diff --git a/extensions/proto/extension.toml b/extensions/proto/extension.toml index 6ecf9bf33f..f26aee7dde 100644 --- a/extensions/proto/extension.toml +++ b/extensions/proto/extension.toml @@ -1,7 +1,7 @@ id = "proto" name = "Proto" description = "Protocol Buffers support." -version = "0.1.0" +version = "0.2.0" schema_version = 1 authors = ["Zed Industries "] repository = "https://github.com/zed-industries/zed" From d1a758708dcb7b93afa02c06b7159a0776c66111 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 7 Oct 2024 10:23:16 -0400 Subject: [PATCH 25/28] php: Bump to v0.2.1 (#18815) This PR bumps the PHP extension to v0.2.1. Changes: - https://github.com/zed-industries/zed/pull/18368 - https://github.com/zed-industries/zed/pull/18774 Release Notes: - N/A --- Cargo.lock | 2 +- extensions/php/Cargo.toml | 2 +- extensions/php/extension.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d3e76eefd2..0715c64984 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14852,7 +14852,7 @@ dependencies = [ [[package]] name = "zed_php" -version = "0.2.0" +version = "0.2.1" dependencies = [ "zed_extension_api 0.1.0", ] diff --git a/extensions/php/Cargo.toml b/extensions/php/Cargo.toml index d4a8f36cbd..9726c6f0d5 100644 --- a/extensions/php/Cargo.toml +++ b/extensions/php/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zed_php" -version = "0.2.0" +version = "0.2.1" edition = "2021" publish = false license = "Apache-2.0" diff --git a/extensions/php/extension.toml b/extensions/php/extension.toml index 92bd7ed85b..a59c5bab53 100644 --- a/extensions/php/extension.toml +++ b/extensions/php/extension.toml @@ -1,7 +1,7 @@ id = "php" name = "PHP" description = "PHP support." -version = "0.2.0" +version = "0.2.1" schema_version = 1 authors = ["Piotr Osiewicz "] repository = "https://github.com/zed-industries/zed" From c83690ff140da4a42ba9cb9f4691799e81d0664a Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 7 Oct 2024 12:29:10 -0400 Subject: [PATCH 26/28] storybook: Wire up HTTP client (#18818) This PR wires up the HTTP client in the Storybook. Release Notes: - N/A --- Cargo.lock | 1 + crates/storybook/Cargo.toml | 1 + crates/storybook/src/storybook.rs | 10 ++++++++++ 3 files changed, 12 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 0715c64984..9deb937370 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10998,6 +10998,7 @@ dependencies = [ "theme", "title_bar", "ui", + "ureq_client", ] [[package]] diff --git a/crates/storybook/Cargo.toml b/crates/storybook/Cargo.toml index 3b10d6331b..b05c7692f9 100644 --- a/crates/storybook/Cargo.toml +++ b/crates/storybook/Cargo.toml @@ -35,6 +35,7 @@ strum = { workspace = true, features = ["derive"] } theme.workspace = true title_bar = { workspace = true, features = ["stories"] } ui = { workspace = true, features = ["stories"] } +ureq_client.workspace = true [dev-dependencies] gpui = { workspace = true, features = ["test-support"] } diff --git a/crates/storybook/src/storybook.rs b/crates/storybook/src/storybook.rs index a8997498a5..a77602efcc 100644 --- a/crates/storybook/src/storybook.rs +++ b/crates/storybook/src/storybook.rs @@ -4,6 +4,8 @@ mod assets; mod stories; mod story_selector; +use std::sync::Arc; + use clap::Parser; use dialoguer::FuzzySelect; use gpui::{ @@ -17,6 +19,7 @@ use simplelog::SimpleLogger; use strum::IntoEnumIterator; use theme::{ThemeRegistry, ThemeSettings}; use ui::prelude::*; +use ureq_client::UreqClient; use crate::app_menus::app_menus; use crate::assets::Assets; @@ -65,6 +68,13 @@ fn main() { gpui::App::new().with_assets(Assets).run(move |cx| { load_embedded_fonts(cx).unwrap(); + let http_client = UreqClient::new( + None, + "zed_storybook".to_string(), + cx.background_executor().clone(), + ); + cx.set_http_client(Arc::new(http_client)); + settings::init(cx); theme::init(theme::LoadThemes::All(Box::new(Assets)), cx); From 11206a84449d29c77cd2f4d1e7b7982538bbe77d Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 7 Oct 2024 12:53:11 -0400 Subject: [PATCH 27/28] ui: Fix avatar indicators getting cut off (#18821) This PR fixes an issue introduced in #18810 that was causing the avatar indicators to get cut off. Release Notes: - N/A --- crates/ui/src/components/avatar/avatar.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/ui/src/components/avatar/avatar.rs b/crates/ui/src/components/avatar/avatar.rs index ebacd334fc..302658b55c 100644 --- a/crates/ui/src/components/avatar/avatar.rs +++ b/crates/ui/src/components/avatar/avatar.rs @@ -84,7 +84,6 @@ impl RenderOnce for Avatar { div() .size(container_size) .rounded_full() - .overflow_hidden() .when_some(self.border_color, |this, color| { this.border(border_width).border_color(color) }) From 60c12a8d0679b98309e0b9f54e8b98572b59e478 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 7 Oct 2024 19:18:44 +0200 Subject: [PATCH 28/28] ssh: Remove old dev servers code paths (#18823) Closes #ISSUE Release Notes: - N/A --- crates/recent_projects/src/dev_servers.rs | 547 +----------------- crates/recent_projects/src/ssh_connections.rs | 4 - 2 files changed, 13 insertions(+), 538 deletions(-) diff --git a/crates/recent_projects/src/dev_servers.rs b/crates/recent_projects/src/dev_servers.rs index 7761461ab5..0a05b6a1e9 100644 --- a/crates/recent_projects/src/dev_servers.rs +++ b/crates/recent_projects/src/dev_servers.rs @@ -5,8 +5,7 @@ use std::time::Duration; use anyhow::anyhow; use anyhow::Context; use anyhow::Result; -use client::Client; -use dev_server_projects::{DevServer, DevServerId, DevServerProject, DevServerProjectId}; +use dev_server_projects::{DevServer, DevServerId, DevServerProjectId}; use editor::Editor; use gpui::pulsating_between; use gpui::AsyncWindowContext; @@ -16,17 +15,12 @@ use gpui::Subscription; use gpui::Task; use gpui::WeakView; use gpui::{ - percentage, Action, Animation, AnimationExt, AnyElement, AppContext, DismissEvent, - EventEmitter, FocusHandle, FocusableView, Model, ScrollHandle, Transformation, View, - ViewContext, + Action, Animation, AnimationExt, AnyElement, AppContext, DismissEvent, EventEmitter, + FocusHandle, FocusableView, Model, ScrollHandle, View, ViewContext, }; use project::terminals::wrap_for_ssh; use project::terminals::SshCommand; -use rpc::proto::RegenerateDevServerTokenResponse; -use rpc::{ - proto::{CreateDevServerResponse, DevServerStatus}, - ErrorCode, ErrorExt, -}; +use rpc::{proto::DevServerStatus, ErrorCode, ErrorExt}; use settings::update_settings_file; use settings::Settings; use task::HideStrategy; @@ -35,14 +29,11 @@ use task::SpawnInTerminal; use terminal_view::terminal_panel::TerminalPanel; use ui::ElevationIndex; use ui::Section; -use ui::{ - prelude::*, IconButtonShape, Indicator, List, ListItem, Modal, ModalFooter, ModalHeader, - Tooltip, -}; +use ui::{prelude::*, IconButtonShape, List, ListItem, Modal, ModalFooter, ModalHeader, Tooltip}; use ui_input::{FieldLabelLayout, TextField}; use util::ResultExt; use workspace::OpenOptions; -use workspace::{notifications::DetachAndPromptErr, AppState, ModalView, Workspace, WORKSPACE_DB}; +use workspace::{notifications::DetachAndPromptErr, AppState, ModalView, Workspace}; use crate::open_dev_server_project; use crate::ssh_connections::connect_over_ssh; @@ -69,15 +60,11 @@ pub struct DevServerProjects { #[derive(Default)] struct CreateDevServer { creating: Option>>, - dev_server_id: Option, - access_token: Option, ssh_prompt: Option>, - kind: NewServerKind, } struct CreateDevServerProject { dev_server_id: DevServerId, - creating: bool, _opening: Option, } @@ -86,14 +73,6 @@ enum Mode { CreateDevServer(CreateDevServer), } -#[derive(Default, PartialEq, Eq, Clone, Copy)] -enum NewServerKind { - DirectSSH, - #[default] - LegacySSH, - Manual, -} - impl DevServerProjects { pub fn register(workspace: &mut Workspace, _: &mut ViewContext) { workspace.register_action(|workspace, _: &OpenRemote, cx| { @@ -223,14 +202,12 @@ impl DevServerProjects { this.mode = Mode::Default(Some(CreateDevServerProject { dev_server_id, - creating: true, _opening: Some(subscription), })); } } else { this.mode = Mode::Default(Some(CreateDevServerProject { dev_server_id, - creating: false, _opening: None, })); } @@ -253,7 +230,7 @@ impl DevServerProjects { self.mode = Mode::Default(Some(CreateDevServerProject { dev_server_id, - creating: true, + _opening: None, })); } @@ -309,10 +286,7 @@ impl DevServerProjects { .log_err(), None => this .update(&mut cx, |this, cx| { - this.mode = Mode::CreateDevServer(CreateDevServer { - kind: NewServerKind::DirectSSH, - ..Default::default() - }); + this.mode = Mode::CreateDevServer(CreateDevServer::default()); cx.notify() }) .log_err(), @@ -320,10 +294,8 @@ impl DevServerProjects { None }); self.mode = Mode::CreateDevServer(CreateDevServer { - kind: NewServerKind::DirectSSH, ssh_prompt: Some(ssh_prompt.clone()), creating: Some(creating), - ..Default::default() }); } @@ -460,228 +432,6 @@ impl DevServerProjects { }) } - fn create_or_update_dev_server( - &mut self, - kind: NewServerKind, - existing_id: Option, - access_token: Option, - cx: &mut ViewContext, - ) { - let name = get_text(&self.dev_server_name_input, cx); - if name.is_empty() { - return; - } - - let manual_setup = match kind { - NewServerKind::DirectSSH => unreachable!(), - NewServerKind::LegacySSH => false, - NewServerKind::Manual => true, - }; - - let ssh_connection_string = if manual_setup { - None - } else if name.contains(' ') { - Some(name.clone()) - } else { - Some(format!("ssh {}", name)) - }; - - let dev_server = self.dev_server_store.update(cx, { - let access_token = access_token.clone(); - |store, cx| { - let ssh_connection_string = ssh_connection_string.clone(); - if let Some(dev_server_id) = existing_id { - let rename = store.rename_dev_server( - dev_server_id, - name.clone(), - ssh_connection_string, - cx, - ); - let token = if let Some(access_token) = access_token { - Task::ready(Ok(RegenerateDevServerTokenResponse { - dev_server_id: dev_server_id.0, - access_token, - })) - } else { - store.regenerate_dev_server_token(dev_server_id, cx) - }; - cx.spawn(|_, _| async move { - rename.await?; - let response = token.await?; - Ok(CreateDevServerResponse { - dev_server_id: dev_server_id.0, - name, - access_token: response.access_token, - }) - }) - } else { - store.create_dev_server(name, ssh_connection_string.clone(), cx) - } - } - }); - - let workspace = self.workspace.clone(); - let store = dev_server_projects::Store::global(cx); - - let task = cx - .spawn({ - |this, mut cx| async move { - let result = dev_server.await; - - match result { - Ok(dev_server) => { - if let Some(ssh_connection_string) = ssh_connection_string { - this.update(&mut cx, |this, cx| { - if let Mode::CreateDevServer(CreateDevServer { - access_token, - dev_server_id, - .. - }) = &mut this.mode - { - access_token.replace(dev_server.access_token.clone()); - dev_server_id - .replace(DevServerId(dev_server.dev_server_id)); - } - cx.notify(); - })?; - - spawn_ssh_task( - workspace - .upgrade() - .ok_or_else(|| anyhow!("workspace dropped"))?, - store, - DevServerId(dev_server.dev_server_id), - ssh_connection_string, - dev_server.access_token.clone(), - &mut cx, - ) - .await - .log_err(); - } - - this.update(&mut cx, |this, cx| { - this.focus_handle.focus(cx); - this.mode = Mode::CreateDevServer(CreateDevServer { - dev_server_id: Some(DevServerId(dev_server.dev_server_id)), - access_token: Some(dev_server.access_token), - kind, - ..Default::default() - }); - cx.notify(); - })?; - Ok(()) - } - Err(e) => { - this.update(&mut cx, |this, cx| { - this.mode = Mode::CreateDevServer(CreateDevServer { - dev_server_id: existing_id, - access_token: None, - kind, - ..Default::default() - }); - cx.notify() - }) - .log_err(); - - Err(e) - } - } - } - }) - .prompt_err("Failed to create server", cx, |_, _| None); - - self.mode = Mode::CreateDevServer(CreateDevServer { - creating: Some(task), - dev_server_id: existing_id, - access_token, - kind, - ..Default::default() - }); - cx.notify() - } - - fn delete_dev_server(&mut self, id: DevServerId, cx: &mut ViewContext) { - let store = self.dev_server_store.read(cx); - let prompt = if store.projects_for_server(id).is_empty() - && store - .dev_server(id) - .is_some_and(|server| server.status == DevServerStatus::Offline) - { - None - } else { - Some(cx.prompt( - gpui::PromptLevel::Warning, - "Are you sure?", - Some("This will delete the dev server and all of its remote projects."), - &["Delete", "Cancel"], - )) - }; - - cx.spawn(|this, mut cx| async move { - if let Some(prompt) = prompt { - if prompt.await? != 0 { - return Ok(()); - } - } - - let project_ids: Vec = this.update(&mut cx, |this, cx| { - this.dev_server_store.update(cx, |store, _| { - store - .projects_for_server(id) - .into_iter() - .map(|project| project.id) - .collect() - }) - })?; - - this.update(&mut cx, |this, cx| { - this.dev_server_store - .update(cx, |store, cx| store.delete_dev_server(id, cx)) - })? - .await?; - - for id in project_ids { - WORKSPACE_DB - .delete_workspace_by_dev_server_project_id(id) - .await - .log_err(); - } - Ok(()) - }) - .detach_and_prompt_err("Failed to delete dev server", cx, |_, _| None); - } - - fn delete_dev_server_project(&mut self, id: DevServerProjectId, cx: &mut ViewContext) { - let answer = cx.prompt( - gpui::PromptLevel::Warning, - "Delete this project?", - Some("This will delete the remote project. You can always re-add it later."), - &["Delete", "Cancel"], - ); - - cx.spawn(|this, mut cx| async move { - let answer = answer.await?; - - if answer != 0 { - return Ok(()); - } - - this.update(&mut cx, |this, cx| { - this.dev_server_store - .update(cx, |store, cx| store.delete_dev_server_project(id, cx)) - })? - .await?; - - WORKSPACE_DB - .delete_workspace_by_dev_server_project_id(id) - .await - .log_err(); - - Ok(()) - }) - .detach_and_prompt_err("Failed to delete dev server project", cx, |_, _| None); - } - fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { match &self.mode { Mode::Default(None) => {} @@ -695,18 +445,8 @@ impl DevServerProjects { }); return; } - if state.kind == NewServerKind::DirectSSH { - self.create_ssh_server(cx); - return; - } - if state.creating.is_none() || state.dev_server_id.is_some() { - self.create_or_update_dev_server( - state.kind, - state.dev_server_id, - state.access_token.clone(), - cx, - ); - } + + self.create_ssh_server(cx); } } } @@ -716,7 +456,6 @@ impl DevServerProjects { Mode::Default(None) => cx.emit(DismissEvent), Mode::CreateDevServer(state) if state.ssh_prompt.is_some() => { self.mode = Mode::CreateDevServer(CreateDevServer { - kind: NewServerKind::DirectSSH, ..Default::default() }); cx.notify(); @@ -729,161 +468,6 @@ impl DevServerProjects { } } - fn render_dev_server( - &mut self, - dev_server: &DevServer, - create_project: Option, - cx: &mut ViewContext, - ) -> impl IntoElement { - let dev_server_id = dev_server.id; - let status = dev_server.status; - let dev_server_name = dev_server.name.clone(); - let kind = if dev_server.ssh_connection_string.is_some() { - NewServerKind::LegacySSH - } else { - NewServerKind::Manual - }; - - v_flex() - .w_full() - .child( - h_flex().group("dev-server").justify_between().child( - h_flex() - .gap_2() - .child( - div() - .id(("status", dev_server.id.0)) - .relative() - .child(Icon::new(IconName::Server).size(IconSize::Small)) - .child(div().absolute().bottom_0().left(rems_from_px(8.0)).child( - Indicator::dot().color(match status { - DevServerStatus::Online => Color::Created, - DevServerStatus::Offline => Color::Hidden, - }), - )) - .tooltip(move |cx| { - Tooltip::text( - match status { - DevServerStatus::Online => "Online", - DevServerStatus::Offline => "Offline", - }, - cx, - ) - }), - ) - .child( - div() - .max_w(rems(26.)) - .overflow_hidden() - .whitespace_nowrap() - .child(Label::new(dev_server_name.clone())), - ) - .child( - h_flex() - .visible_on_hover("dev-server") - .gap_1() - .child(if dev_server.ssh_connection_string.is_some() { - let dev_server = dev_server.clone(); - IconButton::new("reconnect-dev-server", IconName::ArrowCircle) - .on_click(cx.listener(move |this, _, cx| { - let Some(workspace) = this.workspace.upgrade() else { - return; - }; - - reconnect_to_dev_server( - workspace, - dev_server.clone(), - cx, - ) - .detach_and_prompt_err( - "Failed to reconnect", - cx, - |_, _| None, - ); - })) - .tooltip(|cx| Tooltip::text("Reconnect", cx)) - } else { - IconButton::new("edit-dev-server", IconName::Pencil) - .on_click(cx.listener(move |this, _, cx| { - this.mode = Mode::CreateDevServer(CreateDevServer { - dev_server_id: Some(dev_server_id), - kind, - ..Default::default() - }); - let dev_server_name = dev_server_name.clone(); - this.dev_server_name_input.update( - cx, - move |input, cx| { - input.editor().update(cx, move |editor, cx| { - editor.set_text(dev_server_name, cx) - }) - }, - ) - })) - .tooltip(|cx| Tooltip::text("Edit dev server", cx)) - }) - .child({ - let dev_server_id = dev_server.id; - IconButton::new("remove-dev-server", IconName::TrashAlt) - .on_click(cx.listener(move |this, _, cx| { - this.delete_dev_server(dev_server_id, cx) - })) - .tooltip(|cx| Tooltip::text("Remove dev server", cx)) - }), - ), - ), - ) - .child( - v_flex() - .w_full() - .bg(cx.theme().colors().background) - .border_1() - .border_color(cx.theme().colors().border_variant) - .rounded_md() - .my_1() - .py_0p5() - .px_3() - .child( - List::new() - .empty_message("No projects.") - .children( - self.dev_server_store - .read(cx) - .projects_for_server(dev_server.id) - .iter() - .map(|p| self.render_dev_server_project(p, cx)), - ) - .when( - create_project.is_none() - && dev_server.status == DevServerStatus::Online, - |el| { - el.child( - ListItem::new("new-remote_project") - .start_slot(Icon::new(IconName::Plus)) - .child(Label::new("Open folder…")) - .on_click(cx.listener(move |this, _, cx| { - this.mode = - Mode::Default(Some(CreateDevServerProject { - dev_server_id, - creating: false, - _opening: None, - })); - this.project_path_input - .read(cx) - .focus_handle(cx) - .focus(cx); - cx.notify(); - })), - ) - }, - ) - .when_some(create_project, |el, creating| { - el.child(self.render_create_new_project(creating, cx)) - }), - ), - ) - } - fn render_ssh_connection( &mut self, ix: usize, @@ -1094,77 +678,13 @@ impl DevServerProjects { }); } - fn render_create_new_project( - &mut self, - creating: bool, - _: &mut ViewContext, - ) -> impl IntoElement { - ListItem::new("create-remote-project") - .disabled(true) - .start_slot(Icon::new(IconName::FileTree).color(Color::Muted)) - .child(self.project_path_input.clone()) - .child(div().w(IconSize::Medium.rems()).when(creating, |el| { - el.child( - Icon::new(IconName::ArrowCircle) - .size(IconSize::Medium) - .with_animation( - "arrow-circle", - Animation::new(Duration::from_secs(2)).repeat(), - |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), - ), - ) - })) - } - - fn render_dev_server_project( - &mut self, - project: &DevServerProject, - cx: &mut ViewContext, - ) -> impl IntoElement { - let dev_server_project_id = project.id; - let project_id = project.project_id; - let is_online = project_id.is_some(); - - ListItem::new(("remote-project", dev_server_project_id.0)) - .start_slot(Icon::new(IconName::FileTree).when(!is_online, |icon| icon.color(Color::Muted))) - .child( - Label::new(project.paths.join(", ")) - ) - .on_click(cx.listener(move |_, _, cx| { - if let Some(project_id) = project_id { - if let Some(app_state) = AppState::global(cx).upgrade() { - workspace::join_dev_server_project(dev_server_project_id, project_id, app_state, None, cx) - .detach_and_prompt_err("Could not join project", cx, |_, _| None) - } - } else { - cx.spawn(|_, mut cx| async move { - cx.prompt(gpui::PromptLevel::Critical, "This project is offline", Some("The `zed` instance running on this dev server is not connected. You will have to restart it."), &["Ok"]).await.log_err(); - }).detach(); - } - })) - .end_hover_slot::(Some(IconButton::new("remove-remote-project", IconName::TrashAlt) - .on_click(cx.listener(move |this, _, cx| { - this.delete_dev_server_project(dev_server_project_id, cx) - })) - .tooltip(|cx| Tooltip::text("Delete remote project", cx)).into_any_element())) - } - fn render_create_dev_server( &self, state: &CreateDevServer, cx: &mut ViewContext, ) -> impl IntoElement { let creating = state.creating.is_some(); - let dev_server_id = state.dev_server_id; - let access_token = state.access_token.clone(); let ssh_prompt = state.ssh_prompt.clone(); - let use_direct_ssh = SshSettings::get_global(cx).use_direct_ssh() - || Client::global(cx).status().borrow().is_signed_out(); - - let mut kind = state.kind; - if use_direct_ssh && kind == NewServerKind::LegacySSH { - kind = NewServerKind::DirectSSH; - } self.dev_server_name_input.update(cx, |input, cx| { input.editor().update(cx, |editor, cx| { @@ -1216,20 +736,10 @@ impl DevServerProjects { Button::new("create-dev-server", "Connect Server") .style(ButtonStyle::Filled) .layer(ElevationIndex::ModalSurface) - .disabled(creating && dev_server_id.is_none()) + .disabled(creating) .on_click(cx.listener({ - let access_token = access_token.clone(); move |this, _, cx| { - if kind == NewServerKind::DirectSSH { - this.create_ssh_server(cx); - return; - } - this.create_or_update_dev_server( - kind, - dev_server_id, - access_token.clone(), - cx, - ); + this.create_ssh_server(cx); } })), ), @@ -1277,22 +787,6 @@ impl DevServerProjects { .ssh_connections() .collect::>(); - let Mode::Default(create_dev_server_project) = &self.mode else { - unreachable!() - }; - - let mut is_creating = None; - let mut creating_dev_server = None; - if let Some(CreateDevServerProject { - creating, - dev_server_id, - .. - }) = create_dev_server_project - { - is_creating = Some(*creating); - creating_dev_server = Some(*dev_server_id); - }; - let footer = format!("Connections: {}", ssh_connections.len() + dev_servers.len()); Modal::new("remote-projects", Some(self.scroll_handle.clone())) .header( @@ -1309,11 +803,6 @@ impl DevServerProjects { .icon_color(Color::Muted) .on_click(cx.listener(|this, _, cx| { this.mode = Mode::CreateDevServer(CreateDevServer { - kind: if SshSettings::get_global(cx).use_direct_ssh() { - NewServerKind::DirectSSH - } else { - NewServerKind::LegacySSH - }, ..Default::default() }); this.dev_server_name_input.update(cx, |text_field, cx| { @@ -1341,17 +830,7 @@ impl DevServerProjects { self.render_ssh_connection(ix, connection, cx) .into_any_element() }, - )) - .children(dev_servers.iter().map(|dev_server| { - let creating = if creating_dev_server == Some(dev_server.id) - { - is_creating - } else { - None - }; - self.render_dev_server(dev_server, creating, cx) - .into_any_element() - })), + )), ), ), ), diff --git a/crates/recent_projects/src/ssh_connections.rs b/crates/recent_projects/src/ssh_connections.rs index 554146eab2..5862d48e81 100644 --- a/crates/recent_projects/src/ssh_connections.rs +++ b/crates/recent_projects/src/ssh_connections.rs @@ -28,10 +28,6 @@ pub struct SshSettings { } impl SshSettings { - pub fn use_direct_ssh(&self) -> bool { - self.ssh_connections.is_some() - } - pub fn ssh_connections(&self) -> impl Iterator { self.ssh_connections.clone().into_iter().flatten() }