From 3d956ca68bb5c90b7f1dccca18ccc4b1358345f6 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 30 Oct 2024 15:50:41 -0600 Subject: [PATCH 01/45] Fail download if download fails (#19990) Co-Authored-By: Mikayla Release Notes: - Remoting: Fixes a bug where we could cache an HTML error page as a binary Co-authored-by: Mikayla --- crates/auto_update/src/auto_update.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs index fbbd23907a..be0c1b40a3 100644 --- a/crates/auto_update/src/auto_update.rs +++ b/crates/auto_update/src/auto_update.rs @@ -686,6 +686,12 @@ async fn download_remote_server_binary( let request_body = AsyncBody::from(serde_json::to_string(&update_request_body)?); let mut response = client.get(&release.url, request_body, true).await?; + if !response.status().is_success() { + return Err(anyhow!( + "failed to download remote server release: {:?}", + response.status() + )); + } smol::io::copy(response.body_mut(), &mut temp_file).await?; smol::fs::rename(&temp, &target_path).await?; From f80eb264fb9598ef929bbc38558f0adaac52e603 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 30 Oct 2024 16:17:50 -0600 Subject: [PATCH 02/45] Robustify download on remote (#19983) Closes #19976 Closes #19972 We now prefer curl to wget (as it supports socks5:// proxies) and pass -f to curl so it fails; and use sh instead of bash, which should have more consistent behaviour across systems Release Notes: - SSH Remoting: make downloading binary on remote more reliable. --------- Co-authored-by: Will --- crates/remote/src/ssh_session.rs | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/crates/remote/src/ssh_session.rs b/crates/remote/src/ssh_session.rs index a69f0330ff..8cdc5d8478 100644 --- a/crates/remote/src/ssh_session.rs +++ b/crates/remote/src/ssh_session.rs @@ -1853,26 +1853,25 @@ impl SshRemoteConnection { delegate.set_status(Some("Downloading remote development server on host"), cx); + let body = shlex::try_quote(body).unwrap(); + let url = shlex::try_quote(url).unwrap(); + let dst_str = dst_path_gz.to_string_lossy(); + let dst_escaped = shlex::try_quote(&dst_str).unwrap(); + let script = format!( r#" - if command -v wget >/dev/null 2>&1; then - wget --max-redirect=5 --method=GET --header="Content-Type: application/json" --body-data='{}' '{}' -O '{}' && echo "wget" - elif command -v curl >/dev/null 2>&1; then - curl -L -X GET -H "Content-Type: application/json" -d '{}' '{}' -o '{}' && echo "curl" + if command -v curl >/dev/null 2>&1; then + curl -f -L -X GET -H "Content-Type: application/json" -d {body} {url} -o {dst_escaped} && echo "curl" + elif command -v wget >/dev/null 2>&1; then + wget --max-redirect=5 --method=GET --header="Content-Type: application/json" --body-data={body} {url} -O {dst_escaped} && echo "wget" else echo "Neither curl nor wget is available" >&2 exit 1 fi - "#, - body.replace("'", r#"\'"#), - url, - dst_path_gz.display(), - body.replace("'", r#"\'"#), - url, - dst_path_gz.display(), + "# ); - let output = run_cmd(self.socket.ssh_command("bash").arg("-c").arg(script)) + let output = run_cmd(self.socket.ssh_command("sh").arg("-c").arg(script)) .await .context("Failed to download server binary")?; From 6d5784daa6ca7edb03c903af367525cd48e2bc6f Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Wed, 30 Oct 2024 19:42:42 -0300 Subject: [PATCH 03/45] Adjust design of the slash command picker (#19973) This PR removes the quote selection icon button from the footer and adds it in the picker, and adds an icon field to each command entry. Final result looks like: https://github.com/user-attachments/assets/d177f1c1-b6f6-4652-9434-f6291b279e34 Release Notes: - N/A --- assets/icons/wand.svg | 1 + crates/assistant/src/assistant_panel.rs | 39 +--- .../src/slash_command/auto_command.rs | 6 +- .../src/slash_command/delta_command.rs | 5 + .../src/slash_command/diagnostics_command.rs | 4 + .../src/slash_command/file_command.rs | 6 +- .../src/slash_command/project_command.rs | 7 +- .../src/slash_command/prompt_command.rs | 4 + .../src/slash_command/search_command.rs | 4 + .../src/slash_command/symbols_command.rs | 4 + .../src/slash_command/tab_command.rs | 6 +- .../src/slash_command/terminal_command.rs | 4 + crates/assistant/src/slash_command_picker.rs | 201 +++++++++++------- .../src/assistant_slash_command.rs | 3 + crates/ui/src/components/icon.rs | 1 + 15 files changed, 185 insertions(+), 110 deletions(-) create mode 100644 assets/icons/wand.svg diff --git a/assets/icons/wand.svg b/assets/icons/wand.svg new file mode 100644 index 0000000000..a6704b1c42 --- /dev/null +++ b/assets/icons/wand.svg @@ -0,0 +1 @@ + diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 3d498d94eb..4f0462b66a 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -73,12 +73,11 @@ use std::{ }; use terminal_view::{terminal_panel::TerminalPanel, TerminalView}; use text::SelectionGoal; -use ui::TintColor; use ui::{ prelude::*, utils::{format_distance_from_now, DateTimeType}, Avatar, ButtonLike, ContextMenu, Disclosure, ElevationIndex, KeyBinding, ListItem, - ListItemSpacing, PopoverMenu, PopoverMenuHandle, Tooltip, + ListItemSpacing, PopoverMenu, PopoverMenuHandle, TintColor, Tooltip, }; use util::{maybe, ResultExt}; use workspace::{ @@ -4006,13 +4005,7 @@ impl Render for ContextEditor { } else { None }; - let focus_handle = self - .workspace - .update(cx, |workspace, cx| { - Some(workspace.active_item_as::(cx)?.focus_handle(cx)) - }) - .ok() - .flatten(); + v_flex() .key_context("ContextEditor") .capture_action(cx.listener(ContextEditor::cancel)) @@ -4060,28 +4053,7 @@ impl Render for ContextEditor { .child( h_flex() .gap_1() - .child(render_inject_context_menu(cx.view().downgrade(), cx)) - .child( - IconButton::new("quote-button", IconName::Quote) - .icon_size(IconSize::Small) - .on_click(|_, cx| { - cx.dispatch_action(QuoteSelection.boxed_clone()); - }) - .tooltip(move |cx| { - cx.new_view(|cx| { - Tooltip::new("Insert Selection").key_binding( - focus_handle.as_ref().and_then(|handle| { - KeyBinding::for_action_in( - &QuoteSelection, - &handle, - cx, - ) - }), - ) - }) - .into() - }), - ), + .child(render_inject_context_menu(cx.view().downgrade(), cx)), ) .child( h_flex() @@ -4376,6 +4348,7 @@ fn render_inject_context_menu( Button::new("trigger", "Add Context") .icon(IconName::Plus) .icon_size(IconSize::Small) + .icon_color(Color::Muted) .icon_position(IconPosition::Start) .tooltip(|cx| Tooltip::text("Type / to insert via keyboard", cx)), ) @@ -4550,7 +4523,7 @@ impl Render for ContextEditorToolbarItem { .w_full() .justify_between() .gap_2() - .child(Label::new("Insert Context")) + .child(Label::new("Add Context")) .child(Label::new("/ command").color(Color::Muted)) .into_any() }, @@ -4574,7 +4547,7 @@ impl Render for ContextEditorToolbarItem { } }, ) - .action("Insert Selection", QuoteSelection.boxed_clone()) + .action("Add Selection", QuoteSelection.boxed_clone()) })) } }), diff --git a/crates/assistant/src/slash_command/auto_command.rs b/crates/assistant/src/slash_command/auto_command.rs index cc73f36ebf..61f720be6d 100644 --- a/crates/assistant/src/slash_command/auto_command.rs +++ b/crates/assistant/src/slash_command/auto_command.rs @@ -14,7 +14,7 @@ use language_model::{ use semantic_index::{FileSummary, SemanticDb}; use smol::channel; use std::sync::{atomic::AtomicBool, Arc}; -use ui::{BorrowAppContext, WindowContext}; +use ui::{prelude::*, BorrowAppContext, WindowContext}; use util::ResultExt; use workspace::Workspace; @@ -37,6 +37,10 @@ impl SlashCommand for AutoCommand { "Automatically infer what context to add".into() } + fn icon(&self) -> IconName { + IconName::Wand + } + fn menu_text(&self) -> String { self.description() } diff --git a/crates/assistant/src/slash_command/delta_command.rs b/crates/assistant/src/slash_command/delta_command.rs index c9985d9f00..5c8bc2b023 100644 --- a/crates/assistant/src/slash_command/delta_command.rs +++ b/crates/assistant/src/slash_command/delta_command.rs @@ -10,6 +10,7 @@ use gpui::{Task, WeakView, WindowContext}; use language::{BufferSnapshot, LspAdapterDelegate}; use std::sync::{atomic::AtomicBool, Arc}; use text::OffsetRangeExt; +use ui::prelude::*; use workspace::Workspace; pub(crate) struct DeltaSlashCommand; @@ -27,6 +28,10 @@ impl SlashCommand for DeltaSlashCommand { self.description() } + fn icon(&self) -> IconName { + IconName::Diff + } + fn requires_argument(&self) -> bool { false } diff --git a/crates/assistant/src/slash_command/diagnostics_command.rs b/crates/assistant/src/slash_command/diagnostics_command.rs index c7475445ce..3f1e3e5e71 100644 --- a/crates/assistant/src/slash_command/diagnostics_command.rs +++ b/crates/assistant/src/slash_command/diagnostics_command.rs @@ -98,6 +98,10 @@ impl SlashCommand for DiagnosticsSlashCommand { "Insert diagnostics".into() } + fn icon(&self) -> IconName { + IconName::XCircle + } + fn menu_text(&self) -> String { self.description() } diff --git a/crates/assistant/src/slash_command/file_command.rs b/crates/assistant/src/slash_command/file_command.rs index 1d0fa2bf3e..3964754029 100644 --- a/crates/assistant/src/slash_command/file_command.rs +++ b/crates/assistant/src/slash_command/file_command.rs @@ -117,7 +117,7 @@ impl SlashCommand for FileSlashCommand { } fn description(&self) -> String { - "Insert file".into() + "Insert file and/or directory".into() } fn menu_text(&self) -> String { @@ -128,6 +128,10 @@ impl SlashCommand for FileSlashCommand { true } + fn icon(&self) -> IconName { + IconName::File + } + fn complete_argument( self: Arc, arguments: &[String], diff --git a/crates/assistant/src/slash_command/project_command.rs b/crates/assistant/src/slash_command/project_command.rs index d14cb310ad..ee6434ec03 100644 --- a/crates/assistant/src/slash_command/project_command.rs +++ b/crates/assistant/src/slash_command/project_command.rs @@ -24,7 +24,8 @@ use std::{ ops::DerefMut, sync::{atomic::AtomicBool, Arc}, }; -use ui::{BorrowAppContext as _, IconName}; + +use ui::prelude::*; use workspace::Workspace; pub struct ProjectSlashCommand { @@ -50,6 +51,10 @@ impl SlashCommand for ProjectSlashCommand { "Generate a semantic search based on context".into() } + fn icon(&self) -> IconName { + IconName::Folder + } + fn menu_text(&self) -> String { self.description() } diff --git a/crates/assistant/src/slash_command/prompt_command.rs b/crates/assistant/src/slash_command/prompt_command.rs index 079d1425af..9eb44d3418 100644 --- a/crates/assistant/src/slash_command/prompt_command.rs +++ b/crates/assistant/src/slash_command/prompt_command.rs @@ -21,6 +21,10 @@ impl SlashCommand for PromptSlashCommand { "Insert prompt from library".into() } + fn icon(&self) -> IconName { + IconName::Library + } + fn menu_text(&self) -> String { self.description() } diff --git a/crates/assistant/src/slash_command/search_command.rs b/crates/assistant/src/slash_command/search_command.rs index 9c4938ce93..f4bc3e36b6 100644 --- a/crates/assistant/src/slash_command/search_command.rs +++ b/crates/assistant/src/slash_command/search_command.rs @@ -38,6 +38,10 @@ impl SlashCommand for SearchSlashCommand { "Search your project semantically".into() } + fn icon(&self) -> IconName { + IconName::SearchCode + } + fn menu_text(&self) -> String { self.description() } diff --git a/crates/assistant/src/slash_command/symbols_command.rs b/crates/assistant/src/slash_command/symbols_command.rs index 468c8d7126..2b261bc368 100644 --- a/crates/assistant/src/slash_command/symbols_command.rs +++ b/crates/assistant/src/slash_command/symbols_command.rs @@ -22,6 +22,10 @@ impl SlashCommand for OutlineSlashCommand { "Insert symbols for active tab".into() } + fn icon(&self) -> IconName { + IconName::ListTree + } + fn menu_text(&self) -> String { self.description() } diff --git a/crates/assistant/src/slash_command/tab_command.rs b/crates/assistant/src/slash_command/tab_command.rs index 771c0765ee..1579938c92 100644 --- a/crates/assistant/src/slash_command/tab_command.rs +++ b/crates/assistant/src/slash_command/tab_command.rs @@ -12,7 +12,7 @@ use std::{ path::PathBuf, sync::{atomic::AtomicBool, Arc}, }; -use ui::{ActiveTheme, WindowContext}; +use ui::{prelude::*, ActiveTheme, WindowContext}; use util::ResultExt; use workspace::Workspace; @@ -31,6 +31,10 @@ impl SlashCommand for TabSlashCommand { "Insert open tabs (active tab by default)".to_owned() } + fn icon(&self) -> IconName { + IconName::FileTree + } + fn menu_text(&self) -> String { self.description() } diff --git a/crates/assistant/src/slash_command/terminal_command.rs b/crates/assistant/src/slash_command/terminal_command.rs index 2ca1d4041b..84dbb7146f 100644 --- a/crates/assistant/src/slash_command/terminal_command.rs +++ b/crates/assistant/src/slash_command/terminal_command.rs @@ -33,6 +33,10 @@ impl SlashCommand for TerminalSlashCommand { "Insert terminal output".into() } + fn icon(&self) -> IconName { + IconName::Terminal + } + fn menu_text(&self) -> String { self.description() } diff --git a/crates/assistant/src/slash_command_picker.rs b/crates/assistant/src/slash_command_picker.rs index 35ae90d412..3d667d7f82 100644 --- a/crates/assistant/src/slash_command_picker.rs +++ b/crates/assistant/src/slash_command_picker.rs @@ -1,19 +1,13 @@ use std::sync::Arc; use assistant_slash_command::SlashCommandRegistry; -use gpui::AnyElement; -use gpui::DismissEvent; -use gpui::WeakView; -use picker::PickerEditorPosition; -use ui::ListItemSpacing; - -use gpui::SharedString; -use gpui::Task; -use picker::{Picker, PickerDelegate}; -use ui::{prelude::*, ListItem, PopoverMenu, PopoverTrigger}; +use gpui::{AnyElement, DismissEvent, SharedString, Task, WeakView}; +use picker::{Picker, PickerDelegate, PickerEditorPosition}; +use ui::{prelude::*, KeyBinding, ListItem, ListItemSpacing, PopoverMenu, PopoverTrigger}; use crate::assistant_panel::ContextEditor; +use crate::QuoteSelection; #[derive(IntoElement)] pub(super) struct SlashCommandSelector { @@ -27,6 +21,7 @@ struct SlashCommandInfo { name: SharedString, description: SharedString, args: Option, + icon: IconName, } #[derive(Clone)] @@ -37,6 +32,7 @@ enum SlashCommandEntry { renderer: fn(&mut WindowContext<'_>) -> AnyElement, on_confirm: fn(&mut WindowContext<'_>), }, + QuoteButton, } impl AsRef for SlashCommandEntry { @@ -44,6 +40,7 @@ impl AsRef for SlashCommandEntry { match self { SlashCommandEntry::Info(SlashCommandInfo { name, .. }) | SlashCommandEntry::Advert { name, .. } => name, + SlashCommandEntry::QuoteButton => "Quote Selection", } } } @@ -145,16 +142,23 @@ impl PickerDelegate for SlashCommandDelegate { } ret } + fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext>) { if let Some(command) = self.filtered_commands.get(self.selected_index) { - if let SlashCommandEntry::Info(info) = command { - self.active_context_editor - .update(cx, |context_editor, cx| { - context_editor.insert_command(&info.name, cx) - }) - .ok(); - } else if let SlashCommandEntry::Advert { on_confirm, .. } = command { - on_confirm(cx); + match command { + SlashCommandEntry::Info(info) => { + self.active_context_editor + .update(cx, |context_editor, cx| { + context_editor.insert_command(&info.name, cx) + }) + .ok(); + } + SlashCommandEntry::QuoteButton => { + cx.dispatch_action(Box::new(QuoteSelection)); + } + SlashCommandEntry::Advert { on_confirm, .. } => { + on_confirm(cx); + } } cx.emit(DismissEvent); } @@ -181,46 +185,78 @@ impl PickerDelegate for SlashCommandDelegate { .spacing(ListItemSpacing::Dense) .selected(selected) .child( - h_flex() + v_flex() .group(format!("command-entry-label-{ix}")) .w_full() .min_w(px(250.)) .child( - v_flex() - .child( - h_flex() - .child(div().font_buffer(cx).child({ - let mut label = format!("/{}", info.name); - if let Some(args) = - info.args.as_ref().filter(|_| selected) - { - label.push_str(&args); - } - Label::new(label).size(LabelSize::Small) - })) - .children(info.args.clone().filter(|_| !selected).map( - |args| { - div() - .font_buffer(cx) - .child( - Label::new(args) - .size(LabelSize::Small) - .color(Color::Muted), - ) - .visible_on_hover(format!( - "command-entry-label-{ix}" - )) - }, - )), - ) - .child( - Label::new(info.description.clone()) - .size(LabelSize::Small) - .color(Color::Muted), - ), + h_flex() + .gap_1p5() + .child(Icon::new(info.icon).size(IconSize::XSmall)) + .child(div().font_buffer(cx).child({ + let mut label = format!("{}", info.name); + if let Some(args) = info.args.as_ref().filter(|_| selected) + { + label.push_str(&args); + } + Label::new(label).size(LabelSize::Small) + })) + .children(info.args.clone().filter(|_| !selected).map( + |args| { + div() + .font_buffer(cx) + .child( + Label::new(args) + .size(LabelSize::Small) + .color(Color::Muted), + ) + .visible_on_hover(format!( + "command-entry-label-{ix}" + )) + }, + )), + ) + .child( + Label::new(info.description.clone()) + .size(LabelSize::Small) + .color(Color::Muted), ), ), ), + SlashCommandEntry::QuoteButton => { + let focus = cx.focus_handle(); + let key_binding = KeyBinding::for_action_in(&QuoteSelection, &focus, cx); + + Some( + ListItem::new(ix) + .inset(true) + .spacing(ListItemSpacing::Dense) + .selected(selected) + .child( + v_flex() + .child( + h_flex() + .gap_1p5() + .child(Icon::new(IconName::Quote).size(IconSize::XSmall)) + .child( + div().font_buffer(cx).child( + Label::new("selection").size(LabelSize::Small), + ), + ), + ) + .child( + h_flex() + .gap_1p5() + .child( + Label::new("Insert editor selection") + .color(Color::Muted) + .size(LabelSize::Small), + ) + .children(key_binding.map(|kb| kb.render(cx))), + ), + ), + ) + } SlashCommandEntry::Advert { renderer, .. } => Some( ListItem::new(ix) .inset(true) @@ -251,31 +287,50 @@ impl RenderOnce for SlashCommandSelector { name: command_name.into(), description: menu_text, args, + icon: command.icon(), })) }) - .chain([SlashCommandEntry::Advert { - name: "create-your-command".into(), - renderer: |cx| { - v_flex() - .child( - h_flex() - .font_buffer(cx) - .items_center() - .gap_1() - .child(div().font_buffer(cx).child( - Label::new("create-your-command").size(LabelSize::Small), - )) - .child(Icon::new(IconName::ArrowUpRight).size(IconSize::XSmall)), - ) - .child( - Label::new("Learn how to create a custom command") - .size(LabelSize::Small) - .color(Color::Muted), - ) - .into_any_element() + .chain([ + SlashCommandEntry::Advert { + name: "create-your-command".into(), + renderer: |cx| { + v_flex() + .w_full() + .child( + h_flex() + .w_full() + .font_buffer(cx) + .items_center() + .justify_between() + .child( + h_flex() + .items_center() + .gap_1p5() + .child(Icon::new(IconName::Plus).size(IconSize::XSmall)) + .child( + div().font_buffer(cx).child( + Label::new("create-your-command") + .size(LabelSize::Small), + ), + ), + ) + .child( + Icon::new(IconName::ArrowUpRight) + .size(IconSize::XSmall) + .color(Color::Muted), + ), + ) + .child( + Label::new("Create your custom command") + .size(LabelSize::Small) + .color(Color::Muted), + ) + .into_any_element() + }, + on_confirm: |cx| cx.open_url("https://zed.dev/docs/extensions/slash-commands"), }, - on_confirm: |cx| cx.open_url("https://zed.dev/docs/extensions/slash-commands"), - }]) + SlashCommandEntry::QuoteButton, + ]) .collect::>(); let delegate = SlashCommandDelegate { diff --git a/crates/assistant_slash_command/src/assistant_slash_command.rs b/crates/assistant_slash_command/src/assistant_slash_command.rs index de247602d8..58f4fcb9b4 100644 --- a/crates/assistant_slash_command/src/assistant_slash_command.rs +++ b/crates/assistant_slash_command/src/assistant_slash_command.rs @@ -62,6 +62,9 @@ pub type SlashCommandResult = Result String; + fn icon(&self) -> IconName { + IconName::Slash + } fn label(&self, _cx: &AppContext) -> CodeLabel { CodeLabel::plain(self.name(), None) } diff --git a/crates/ui/src/components/icon.rs b/crates/ui/src/components/icon.rs index 890476f5fe..e11b6edf32 100644 --- a/crates/ui/src/components/icon.rs +++ b/crates/ui/src/components/icon.rs @@ -284,6 +284,7 @@ pub enum IconName { Update, UserGroup, Visible, + Wand, Warning, WholeWord, XCircle, From 40802d91d4faf849ad35fb53d6b00320c1d04cc1 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 30 Oct 2024 17:20:11 -0600 Subject: [PATCH 04/45] SSH installation refactor (#19991) This also cleans up logic for deciding how to do things. Release Notes: - Remoting: If downloading the binary on the remote fails, fall back to uploading it. --------- Co-authored-by: Mikayala --- Cargo.lock | 1 + crates/recent_projects/src/ssh_connections.rs | 270 +++----------- crates/remote/Cargo.toml | 1 + crates/remote/src/ssh_session.rs | 340 +++++++++++++----- 4 files changed, 315 insertions(+), 297 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9a30984724..10e4622534 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9538,6 +9538,7 @@ dependencies = [ "log", "parking_lot", "prost", + "release_channel", "rpc", "serde", "serde_json", diff --git a/crates/recent_projects/src/ssh_connections.rs b/crates/recent_projects/src/ssh_connections.rs index 84618a2f49..0d40da375b 100644 --- a/crates/recent_projects/src/ssh_connections.rs +++ b/crates/recent_projects/src/ssh_connections.rs @@ -13,8 +13,7 @@ use gpui::{AppContext, Model}; use language::CursorShape; use markdown::{Markdown, MarkdownStyle}; -use release_channel::{AppVersion, ReleaseChannel}; -use remote::ssh_session::{ServerBinary, ServerVersion}; +use release_channel::ReleaseChannel; use remote::{SshConnectionOptions, SshPlatform, SshRemoteClient}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -441,23 +440,66 @@ impl remote::SshClientDelegate for SshClientDelegate { self.update_status(status, cx) } - fn get_server_binary( + fn download_server_binary_locally( &self, platform: SshPlatform, - upload_binary_over_ssh: bool, + release_channel: ReleaseChannel, + version: Option, cx: &mut AsyncAppContext, - ) -> oneshot::Receiver> { - let (tx, rx) = oneshot::channel(); - let this = self.clone(); + ) -> Task> { cx.spawn(|mut cx| async move { - tx.send( - this.get_server_binary_impl(platform, upload_binary_over_ssh, &mut cx) - .await, + let binary_path = AutoUpdater::download_remote_server_release( + platform.os, + platform.arch, + release_channel, + version, + &mut cx, ) - .ok(); + .await + .map_err(|e| { + anyhow!( + "Failed to download remote server binary (version: {}, os: {}, arch: {}): {}", + version + .map(|v| format!("{}", v)) + .unwrap_or("unknown".to_string()), + platform.os, + platform.arch, + e + ) + })?; + Ok(binary_path) }) - .detach(); - rx + } + + fn get_download_params( + &self, + platform: SshPlatform, + release_channel: ReleaseChannel, + version: Option, + cx: &mut AsyncAppContext, + ) -> Task> { + cx.spawn(|mut cx| async move { + let (release, request_body) = AutoUpdater::get_remote_server_release_url( + platform.os, + platform.arch, + release_channel, + version, + &mut cx, + ) + .await + .map_err(|e| { + anyhow!( + "Failed to get remote server binary download url (version: {}, os: {}, arch: {}): {}", + version.map(|v| format!("{}", v)).unwrap_or("unknown".to_string()), + platform.os, + platform.arch, + e + ) + })?; + + Ok((release.url, request_body)) + } + ) } fn remote_server_binary_path( @@ -485,208 +527,6 @@ impl SshClientDelegate { }) .ok(); } - - async fn get_server_binary_impl( - &self, - platform: SshPlatform, - upload_binary_via_ssh: bool, - cx: &mut AsyncAppContext, - ) -> Result<(ServerBinary, ServerVersion)> { - let (version, release_channel) = cx.update(|cx| { - let version = AppVersion::global(cx); - let channel = ReleaseChannel::global(cx); - - (version, channel) - })?; - - // In dev mode, build the remote server binary from source - #[cfg(debug_assertions)] - if release_channel == ReleaseChannel::Dev { - let result = self.build_local(cx, platform, version).await?; - // Fall through to a remote binary if we're not able to compile a local binary - if let Some((path, version)) = result { - return Ok(( - ServerBinary::LocalBinary(path), - ServerVersion::Semantic(version), - )); - } - } - - // For nightly channel, always get latest - let current_version = if release_channel == ReleaseChannel::Nightly { - None - } else { - Some(version) - }; - - self.update_status( - Some(&format!("Checking remote server release {}", version)), - cx, - ); - - if upload_binary_via_ssh { - let binary_path = AutoUpdater::download_remote_server_release( - platform.os, - platform.arch, - release_channel, - current_version, - cx, - ) - .await - .map_err(|e| { - anyhow!( - "Failed to download remote server binary (version: {}, os: {}, arch: {}): {}", - version, - platform.os, - platform.arch, - e - ) - })?; - - Ok(( - ServerBinary::LocalBinary(binary_path), - ServerVersion::Semantic(version), - )) - } else { - let (release, request_body) = AutoUpdater::get_remote_server_release_url( - platform.os, - platform.arch, - release_channel, - current_version, - cx, - ) - .await - .map_err(|e| { - anyhow!( - "Failed to get remote server binary download url (version: {}, os: {}, arch: {}): {}", - version, - platform.os, - platform.arch, - e - ) - })?; - - let version = release - .version - .parse::() - .map(ServerVersion::Semantic) - .unwrap_or_else(|_| ServerVersion::Commit(release.version)); - Ok(( - ServerBinary::ReleaseUrl { - url: release.url, - body: request_body, - }, - version, - )) - } - } - - #[cfg(debug_assertions)] - async fn build_local( - &self, - cx: &mut AsyncAppContext, - platform: SshPlatform, - version: gpui::SemanticVersion, - ) -> Result> { - use smol::process::{Command, Stdio}; - - async fn run_cmd(command: &mut Command) -> Result<()> { - let output = command - .kill_on_drop(true) - .stderr(Stdio::inherit()) - .output() - .await?; - if !output.status.success() { - Err(anyhow!("Failed to run command: {:?}", command))?; - } - Ok(()) - } - - if platform.arch == std::env::consts::ARCH && platform.os == std::env::consts::OS { - self.update_status(Some("Building remote server binary from source"), cx); - log::info!("building remote server binary from source"); - run_cmd(Command::new("cargo").args([ - "build", - "--package", - "remote_server", - "--features", - "debug-embed", - "--target-dir", - "target/remote_server", - ])) - .await?; - - self.update_status(Some("Compressing binary"), cx); - - run_cmd(Command::new("gzip").args([ - "-9", - "-f", - "target/remote_server/debug/remote_server", - ])) - .await?; - - let path = std::env::current_dir()?.join("target/remote_server/debug/remote_server.gz"); - return Ok(Some((path, version))); - } else if let Some(triple) = platform.triple() { - smol::fs::create_dir_all("target/remote_server").await?; - - self.update_status(Some("Installing cross.rs for cross-compilation"), cx); - log::info!("installing cross"); - run_cmd(Command::new("cargo").args([ - "install", - "cross", - "--git", - "https://github.com/cross-rs/cross", - ])) - .await?; - - self.update_status( - Some(&format!( - "Building remote server binary from source for {} with Docker", - &triple - )), - cx, - ); - log::info!("building remote server binary from source for {}", &triple); - run_cmd( - Command::new("cross") - .args([ - "build", - "--package", - "remote_server", - "--features", - "debug-embed", - "--target-dir", - "target/remote_server", - "--target", - &triple, - ]) - .env( - "CROSS_CONTAINER_OPTS", - "--mount type=bind,src=./target,dst=/app/target", - ), - ) - .await?; - - self.update_status(Some("Compressing binary"), cx); - - run_cmd(Command::new("gzip").args([ - "-9", - "-f", - &format!("target/remote_server/{}/debug/remote_server", triple), - ])) - .await?; - - let path = std::env::current_dir()?.join(format!( - "target/remote_server/{}/debug/remote_server.gz", - triple - )); - - return Ok(Some((path, version))); - } else { - return Ok(None); - } - } } pub fn is_connecting_over_ssh(workspace: &Workspace, cx: &AppContext) -> bool { diff --git a/crates/remote/Cargo.toml b/crates/remote/Cargo.toml index 937a69ee59..086e718c35 100644 --- a/crates/remote/Cargo.toml +++ b/crates/remote/Cargo.toml @@ -35,6 +35,7 @@ smol.workspace = true tempfile.workspace = true thiserror.workspace = true util.workspace = true +release_channel.workspace = true [dev-dependencies] gpui = { workspace = true, features = ["test-support"] } diff --git a/crates/remote/src/ssh_session.rs b/crates/remote/src/ssh_session.rs index 8cdc5d8478..1e708e4b0a 100644 --- a/crates/remote/src/ssh_session.rs +++ b/crates/remote/src/ssh_session.rs @@ -21,6 +21,7 @@ use gpui::{ ModelContext, SemanticVersion, Task, WeakModel, }; use parking_lot::Mutex; +use release_channel::{AppCommitSha, AppVersion, ReleaseChannel}; use rpc::{ proto::{self, build_typed_envelope, Envelope, EnvelopedMessage, PeerId, RequestMessage}, AnyProtoClient, EntityMessageSubscriber, ErrorExt, ProtoClient, ProtoMessageHandlerSet, @@ -227,10 +228,19 @@ pub enum ServerBinary { ReleaseUrl { url: String, body: String }, } +#[derive(Clone, Debug, PartialEq, Eq)] pub enum ServerVersion { Semantic(SemanticVersion), Commit(String), } +impl ServerVersion { + pub fn semantic_version(&self) -> Option { + match self { + Self::Semantic(version) => Some(*version), + _ => None, + } + } +} impl std::fmt::Display for ServerVersion { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -252,12 +262,21 @@ pub trait SshClientDelegate: Send + Sync { platform: SshPlatform, cx: &mut AsyncAppContext, ) -> Result; - fn get_server_binary( + fn get_download_params( &self, platform: SshPlatform, - upload_binary_over_ssh: bool, + release_channel: ReleaseChannel, + version: Option, cx: &mut AsyncAppContext, - ) -> oneshot::Receiver>; + ) -> Task>; + + fn download_server_binary_locally( + &self, + platform: SshPlatform, + release_channel: ReleaseChannel, + version: Option, + cx: &mut AsyncAppContext, + ) -> Task>; fn set_status(&self, status: Option<&str>, cx: &mut AsyncAppContext); } @@ -1727,86 +1746,123 @@ impl SshRemoteConnection { platform: SshPlatform, cx: &mut AsyncAppContext, ) -> Result<()> { - if std::env::var("ZED_USE_CACHED_REMOTE_SERVER").is_ok() { - if let Ok(installed_version) = - run_cmd(self.socket.ssh_command(dst_path).arg("version")).await - { - log::info!("using cached server binary version {}", installed_version); - return Ok(()); - } - } - - if cfg!(not(debug_assertions)) { - // When we're not in dev mode, we don't want to switch out the binary if it's - // still open. - // In dev mode, that's fine, since we often kill Zed processes with Ctrl-C and want - // to still replace the binary. - if self.is_binary_in_use(dst_path).await? { - log::info!("server binary is opened by another process. not updating"); - delegate.set_status( - Some("Skipping update of remote development server, since it's still in use"), - cx, - ); - return Ok(()); - } - } - - let upload_binary_over_ssh = self.socket.connection_options.upload_binary_over_ssh; - let (binary, new_server_version) = delegate - .get_server_binary(platform, upload_binary_over_ssh, cx) - .await??; - - if cfg!(not(debug_assertions)) { - let installed_version = if let Ok(version_output) = - run_cmd(self.socket.ssh_command(dst_path).arg("version")).await - { + let current_version = match run_cmd(self.socket.ssh_command(dst_path).arg("version")).await + { + Ok(version_output) => { if let Ok(version) = version_output.trim().parse::() { Some(ServerVersion::Semantic(version)) } else { Some(ServerVersion::Commit(version_output.trim().to_string())) } - } else { - None + } + Err(_) => None, + }; + let (release_channel, wanted_version) = cx.update(|cx| { + let release_channel = ReleaseChannel::global(cx); + let wanted_version = match release_channel { + ReleaseChannel::Nightly => { + AppCommitSha::try_global(cx).map(|sha| ServerVersion::Commit(sha.0)) + } + ReleaseChannel::Dev => None, + _ => Some(ServerVersion::Semantic(AppVersion::global(cx))), }; + (release_channel, wanted_version) + })?; - if let Some(installed_version) = installed_version { - use ServerVersion::*; - match (installed_version, new_server_version) { - (Semantic(installed), Semantic(new)) if installed == new => { - log::info!("remote development server present and matching client version"); - return Ok(()); - } - (Semantic(installed), Semantic(new)) if installed > new => { - let error = anyhow!("The version of the remote server ({}) is newer than the Zed version ({}). Please update Zed.", installed, new); - return Err(error); - } - (Commit(installed), Commit(new)) if installed == new => { - log::info!( - "remote development server present and matching client version {}", - installed - ); - return Ok(()); - } - (installed, _) => { - log::info!( - "remote development server has version: {}. updating...", - installed - ); - } + match (¤t_version, &wanted_version) { + (Some(current), Some(wanted)) if current == wanted => { + log::info!("remote development server present and matching client version"); + return Ok(()); + } + (Some(ServerVersion::Semantic(current)), Some(ServerVersion::Semantic(wanted))) + if current > wanted => + { + anyhow::bail!("The version of the remote server ({}) is newer than the Zed version ({}). Please update Zed.", current, wanted); + } + _ => { + log::info!("Installing remote development server"); + } + } + + if self.is_binary_in_use(dst_path).await? { + // When we're not in dev mode, we don't want to switch out the binary if it's + // still open. + // In dev mode, that's fine, since we often kill Zed processes with Ctrl-C and want + // to still replace the binary. + if cfg!(not(debug_assertions)) { + anyhow::bail!("The remote server version ({:?}) does not match the wanted version ({:?}), but is in use by another Zed client so cannot be upgraded.", ¤t_version, &wanted_version) + } else { + log::info!("Binary is currently in use, ignoring because this is a dev build") + } + } + + if wanted_version.is_none() { + if std::env::var("ZED_BUILD_REMOTE_SERVER").is_err() { + if let Some(current_version) = current_version { + log::warn!( + "In development, using cached remote server binary version ({})", + current_version + ); + + return Ok(()); + } else { + anyhow::bail!( + "ZED_BUILD_REMOTE_SERVER is not set, but no remote server exists at ({:?})", + dst_path + ) + } + } + + #[cfg(debug_assertions)] + { + let src_path = self.build_local(platform, delegate, cx).await?; + + return self + .upload_local_server_binary(&src_path, dst_path, delegate, cx) + .await; + } + + #[cfg(not(debug_assertions))] + anyhow::bail!("Running development build in release mode, cannot cross compile (unset ZED_BUILD_REMOTE_SERVER)") + } + + let upload_binary_over_ssh = self.socket.connection_options.upload_binary_over_ssh; + + if !upload_binary_over_ssh { + let (url, body) = delegate + .get_download_params( + platform, + release_channel, + wanted_version.clone().and_then(|v| v.semantic_version()), + cx, + ) + .await?; + + match self + .download_binary_on_server(&url, &body, dst_path, delegate, cx) + .await + { + Ok(_) => return Ok(()), + Err(e) => { + log::error!( + "Failed to download binary on server, attempting to upload server: {}", + e + ) } } } - match binary { - ServerBinary::LocalBinary(src_path) => { - self.upload_local_server_binary(&src_path, dst_path, delegate, cx) - .await - } - ServerBinary::ReleaseUrl { url, body } => { - self.download_binary_on_server(&url, &body, dst_path, delegate, cx) - .await - } - } + let src_path = delegate + .download_server_binary_locally( + platform, + release_channel, + wanted_version.and_then(|v| v.semantic_version()), + cx, + ) + .await?; + + self.upload_local_server_binary(&src_path, dst_path, delegate, cx) + .await } async fn is_binary_in_use(&self, binary_path: &Path) -> Result { @@ -1973,6 +2029,113 @@ impl SshRemoteConnection { )) } } + + #[cfg(debug_assertions)] + async fn build_local( + &self, + platform: SshPlatform, + delegate: &Arc, + cx: &mut AsyncAppContext, + ) -> Result { + use smol::process::{Command, Stdio}; + + async fn run_cmd(command: &mut Command) -> Result<()> { + let output = command + .kill_on_drop(true) + .stderr(Stdio::inherit()) + .output() + .await?; + if !output.status.success() { + Err(anyhow!("Failed to run command: {:?}", command))?; + } + Ok(()) + } + + if platform.arch == std::env::consts::ARCH && platform.os == std::env::consts::OS { + delegate.set_status(Some("Building remote server binary from source"), cx); + log::info!("building remote server binary from source"); + run_cmd(Command::new("cargo").args([ + "build", + "--package", + "remote_server", + "--features", + "debug-embed", + "--target-dir", + "target/remote_server", + ])) + .await?; + + delegate.set_status(Some("Compressing binary"), cx); + + run_cmd(Command::new("gzip").args([ + "-9", + "-f", + "target/remote_server/debug/remote_server", + ])) + .await?; + + let path = std::env::current_dir()?.join("target/remote_server/debug/remote_server.gz"); + return Ok(path); + } + let Some(triple) = platform.triple() else { + anyhow::bail!("can't cross compile for: {:?}", platform); + }; + smol::fs::create_dir_all("target/remote_server").await?; + + delegate.set_status(Some("Installing cross.rs for cross-compilation"), cx); + log::info!("installing cross"); + run_cmd(Command::new("cargo").args([ + "install", + "cross", + "--git", + "https://github.com/cross-rs/cross", + ])) + .await?; + + delegate.set_status( + Some(&format!( + "Building remote server binary from source for {} with Docker", + &triple + )), + cx, + ); + log::info!("building remote server binary from source for {}", &triple); + run_cmd( + Command::new("cross") + .args([ + "build", + "--package", + "remote_server", + "--features", + "debug-embed", + "--target-dir", + "target/remote_server", + "--target", + &triple, + ]) + .env( + "CROSS_CONTAINER_OPTS", + "--mount type=bind,src=./target,dst=/app/target", + ), + ) + .await?; + + delegate.set_status(Some("Compressing binary"), cx); + + run_cmd(Command::new("gzip").args([ + "-9", + "-f", + &format!("target/remote_server/{}/debug/remote_server", triple), + ])) + .await?; + + let path = std::env::current_dir()?.join(format!( + "target/remote_server/{}/debug/remote_server.gz", + triple + )); + + return Ok(path); + } } type ResponseChannels = Mutex)>>>; @@ -2294,12 +2457,12 @@ mod fake { }, select_biased, FutureExt, SinkExt, StreamExt, }; - use gpui::{AsyncAppContext, Task, TestAppContext}; + use gpui::{AsyncAppContext, SemanticVersion, Task, TestAppContext}; + use release_channel::ReleaseChannel; use rpc::proto::Envelope; use super::{ - ChannelClient, RemoteConnection, ServerBinary, ServerVersion, SshClientDelegate, - SshConnectionOptions, SshPlatform, + ChannelClient, RemoteConnection, SshClientDelegate, SshConnectionOptions, SshPlatform, }; pub(super) struct FakeRemoteConnection { @@ -2411,23 +2574,36 @@ mod fake { ) -> oneshot::Receiver> { unreachable!() } - fn remote_server_binary_path( + + fn download_server_binary_locally( &self, _: SshPlatform, + _: ReleaseChannel, + _: Option, _: &mut AsyncAppContext, - ) -> Result { + ) -> Task> { unreachable!() } - fn get_server_binary( + + fn get_download_params( &self, - _: SshPlatform, - _: bool, - _: &mut AsyncAppContext, - ) -> oneshot::Receiver> { + _platform: SshPlatform, + _release_channel: ReleaseChannel, + _version: Option, + _cx: &mut AsyncAppContext, + ) -> Task> { unreachable!() } fn set_status(&self, _: Option<&str>, _: &mut AsyncAppContext) {} + + fn remote_server_binary_path( + &self, + _platform: SshPlatform, + _cx: &mut AsyncAppContext, + ) -> Result { + unreachable!() + } } } From 383e868af0c71335b3120ccaef1120e54a2fa102 Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Thu, 31 Oct 2024 03:28:13 +0000 Subject: [PATCH 05/45] docs: SSH no longer requires Zed Preview (#20003) --- docs/src/remote-development.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/remote-development.md b/docs/src/remote-development.md index 771be830bc..17ae23bb63 100644 --- a/docs/src/remote-development.md +++ b/docs/src/remote-development.md @@ -16,7 +16,7 @@ On your local machine, Zed runs its UI, talks to language models, uses Tree-sitt ## Setup -1. Download and install the latest [Zed Preview](https://zed.dev/releases/preview). You need at least Zed v0.159. +1. Download and install the latest [Zed](https://zed.dev/releases). You need at least Zed v0.159. 1. Open the remote projects dialogue with cmd-shift-p remote or cmd-control-o. 1. Click "Connect New Server" and enter the command you use to SSH into the server. See [Supported SSH options](#supported-ssh-options) for options you can pass. 1. Your local machine will attempt to connect to the remote server using the `ssh` binary on your path. Assuming the connection is successful, Zed will download the server on the remote host and start it. From 10226a39927c898d2065f7390752b7b711728a06 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Thu, 31 Oct 2024 09:36:05 +0100 Subject: [PATCH 06/45] docs: Document inline blame options (#20006) Release Notes: - N/A --- assets/settings/default.json | 6 ++++++ docs/src/configuring-zed.md | 26 ++++++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/assets/settings/default.json b/assets/settings/default.json index 5295052215..c2f0275744 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -652,6 +652,12 @@ // Sets a delay after which the inline blame information is shown. // Delay is restarted with every cursor movement. // "delay_ms": 600 + // + // Whether or not do display the git commit summary on the same line. + // "show_commit_summary": false + // + // The minimum column number to show the inline blame information at + // "min_column": 0 } }, // Configuration for how direnv configuration should be loaded. May take 2 values: diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index d8105b4537..11c6fdc09d 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -1043,6 +1043,32 @@ To interpret all `.c` files as C++, files called `MyLockFile` as TOML and files } ``` +3. Show a commit summary next to the commit date and author: + +```json +{ + "git": { + "inline_blame": { + "enabled": true, + "show_commit_summary": true + } + } +} +``` + +4. Use this as the minimum column at which to display inline blame information: + +```json +{ + "git": { + "inline_blame": { + "enabled": true, + "min_column": 80 + } + } +} +``` + ## Indent Guides - Description: Configuration related to indent guides. Indent guides can be configured separately for each language. From 7fd334fddbb3b4787e030b306608a4f98fa81eca Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Thu, 31 Oct 2024 09:36:18 +0100 Subject: [PATCH 07/45] proto: Remove unused UpdateUserSettings message (#20005) Release Notes: - N/A --- crates/proto/proto/zed.proto | 13 +------------ crates/proto/src/proto.rs | 2 -- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index 90fbc397f1..e43b622545 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -257,7 +257,6 @@ message Envelope { FindSearchCandidatesResponse find_search_candidates_response = 244; CloseBuffer close_buffer = 245; - UpdateUserSettings update_user_settings = 246; ShutdownRemoteServer shutdown_remote_server = 257; @@ -309,6 +308,7 @@ message Envelope { reserved 205 to 206; reserved 221; reserved 224 to 229; + reserved 246; reserved 247 to 254; reserved 255 to 256; } @@ -2361,17 +2361,6 @@ message AddWorktreeResponse { string canonicalized_path = 2; } -message UpdateUserSettings { - uint64 project_id = 1; - string content = 2; - optional Kind kind = 3; - - enum Kind { - Settings = 0; - Tasks = 1; - } -} - message GetPathMetadata { uint64 project_id = 1; string path = 2; diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index ca0403ed72..e143f24a47 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -342,7 +342,6 @@ messages!( (FindSearchCandidates, Background), (FindSearchCandidatesResponse, Background), (CloseBuffer, Foreground), - (UpdateUserSettings, Foreground), (ShutdownRemoteServer, Foreground), (RemoveWorktree, Foreground), (LanguageServerLog, Foreground), @@ -559,7 +558,6 @@ entity_messages!( UpdateContext, SynchronizeContexts, LspExtSwitchSourceHeader, - UpdateUserSettings, LanguageServerLog, Toast, HideToast, From 633b665379c18a06959102c3857889b8bcbde4bb Mon Sep 17 00:00:00 2001 From: Auf keinen Fall Jens Date: Thu, 31 Oct 2024 09:39:57 +0100 Subject: [PATCH 08/45] Option to insert comment character(s) at the beginning of the line(s) (#19746) Closes #19459 This PR adds the optional setting to insert comment character(s) at the beginning of the line(s) instead of after the indentation. It can be enabled via keybindings: ``` "ctrl-/": ["editor::ToggleComments", { "ignore_indent": true }] ``` As suggested by @notpeter in #19459, this is implemented in `toggle_comments` (editor.rs) taking the existing `advance_downwards` option as example. There's also a test case for the setting, which mimics the test case for the regular comment toggling behavior. --- I am not entirely happy with the name `ignore_indent`. The default would be a double negative now `ignore_indent=false`. A positive wording would probably easier to understand, but I could not think of anything concise. `insert_at_line_start` or just `at_line_start` might work, but didn't convince me either. That said, I am happy to change the name if there are better ideas. --- Release Notes: - Added optional setting to insert comment character(s) at the beginning of the line(s) instead of after the indentation. It can be used by changing the default mapping to toggle comments like this: `"ctrl-/": ["editor::ToggleComments", { "ignore_indent": true }]` --- crates/editor/src/actions.rs | 2 + crates/editor/src/editor.rs | 23 +++++- crates/editor/src/editor_tests.rs | 126 ++++++++++++++++++++++++++++++ 3 files changed, 149 insertions(+), 2 deletions(-) diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index 83379e13ae..474655700c 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -80,6 +80,8 @@ pub struct ConfirmCodeAction { pub struct ToggleComments { #[serde(default)] pub advance_downwards: bool, + #[serde(default)] + pub ignore_indent: bool, } #[derive(PartialEq, Clone, Deserialize, Default)] diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 2e88df6b92..16ba3d8b8b 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -8664,14 +8664,22 @@ impl Editor { let snapshot = this.buffer.read(cx).read(cx); let empty_str: Arc = Arc::default(); let mut suffixes_inserted = Vec::new(); + let ignore_indent = action.ignore_indent; fn comment_prefix_range( snapshot: &MultiBufferSnapshot, row: MultiBufferRow, comment_prefix: &str, comment_prefix_whitespace: &str, + ignore_indent: bool, ) -> Range { - let start = Point::new(row.0, snapshot.indent_size_for_line(row).len); + let indent_size = if ignore_indent { + 0 + } else { + snapshot.indent_size_for_line(row).len + }; + + let start = Point::new(row.0, indent_size); let mut line_bytes = snapshot .bytes_in_range(start..snapshot.max_point()) @@ -8767,7 +8775,16 @@ impl Editor { } // If the language has line comments, toggle those. - let full_comment_prefixes = language.line_comment_prefixes(); + let mut full_comment_prefixes = language.line_comment_prefixes().to_vec(); + + // If ignore_indent is set, trim spaces from the right side of all full_comment_prefixes + if ignore_indent { + full_comment_prefixes = full_comment_prefixes + .into_iter() + .map(|s| Arc::from(s.trim_end())) + .collect(); + } + if !full_comment_prefixes.is_empty() { let first_prefix = full_comment_prefixes .first() @@ -8794,6 +8811,7 @@ impl Editor { row, &prefix[..trimmed_prefix_len], &prefix[trimmed_prefix_len..], + ignore_indent, ) }) .max_by_key(|range| range.end.column - range.start.column) @@ -8834,6 +8852,7 @@ impl Editor { start_row, comment_prefix, comment_prefix_whitespace, + ignore_indent, ); let suffix_range = comment_suffix_range( snapshot.deref(), diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index d56b22b454..02583889bc 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -8533,6 +8533,131 @@ async fn test_toggle_comment(cx: &mut gpui::TestAppContext) { "}); } +#[gpui::test] +async fn test_toggle_comment_ignore_indent(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + let language = Arc::new(Language::new( + LanguageConfig { + line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()], + ..Default::default() + }, + Some(tree_sitter_rust::LANGUAGE.into()), + )); + cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); + + let toggle_comments = &ToggleComments { + advance_downwards: false, + ignore_indent: true, + }; + + // If multiple selections intersect a line, the line is only toggled once. + cx.set_state(indoc! {" + fn a() { + // «b(); + // c(); + // ˇ» d(); + } + "}); + + cx.update_editor(|e, cx| e.toggle_comments(toggle_comments, cx)); + + cx.assert_editor_state(indoc! {" + fn a() { + «b(); + c(); + ˇ» d(); + } + "}); + + // The comment prefix is inserted at the beginning of each line + cx.update_editor(|e, cx| e.toggle_comments(toggle_comments, cx)); + + cx.assert_editor_state(indoc! {" + fn a() { + // «b(); + // c(); + // ˇ» d(); + } + "}); + + // If a selection ends at the beginning of a line, that line is not toggled. + cx.set_selections_state(indoc! {" + fn a() { + // b(); + // «c(); + ˇ»// d(); + } + "}); + + cx.update_editor(|e, cx| e.toggle_comments(toggle_comments, cx)); + + cx.assert_editor_state(indoc! {" + fn a() { + // b(); + «c(); + ˇ»// d(); + } + "}); + + // If a selection span a single line and is empty, the line is toggled. + cx.set_state(indoc! {" + fn a() { + a(); + b(); + ˇ + } + "}); + + cx.update_editor(|e, cx| e.toggle_comments(toggle_comments, cx)); + + cx.assert_editor_state(indoc! {" + fn a() { + a(); + b(); + //ˇ + } + "}); + + // If a selection span multiple lines, empty lines are not toggled. + cx.set_state(indoc! {" + fn a() { + «a(); + + c();ˇ» + } + "}); + + cx.update_editor(|e, cx| e.toggle_comments(toggle_comments, cx)); + + cx.assert_editor_state(indoc! {" + fn a() { + // «a(); + + // c();ˇ» + } + "}); + + // If a selection includes multiple comment prefixes, all lines are uncommented. + cx.set_state(indoc! {" + fn a() { + // «a(); + /// b(); + //! c();ˇ» + } + "}); + + cx.update_editor(|e, cx| e.toggle_comments(toggle_comments, cx)); + + cx.assert_editor_state(indoc! {" + fn a() { + «a(); + b(); + c();ˇ» + } + "}); +} + #[gpui::test] async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); @@ -8554,6 +8679,7 @@ async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext) let toggle_comments = &ToggleComments { advance_downwards: true, + ignore_indent: false, }; // Single cursor on one line -> advance From 293e080f03b3b5f5d0624f7aeca4cd0256e5bc39 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Thu, 31 Oct 2024 14:25:57 +0100 Subject: [PATCH 09/45] tasks: Add `editor: Spawn Nearest Task` action (#19901) This spawns the runnable task that that's closest to the cursor. One thing missing right now is that it doesn't find tasks that are attached to non-outline symbols, such as subtests in Go. Release Notes: - Added a new reveal option for tasks: `"no_focus"`. If used, the tasks terminal panel will be opened and shown, but not focused. - Added a new `editor: spawn nearest task` action that spawns the task with a run indicator icon nearest to the cursor. It can be configured to also use a `reveal` strategy. Example: ```json { "context": "EmptyPane || SharedScreen || vim_mode == normal", "bindings": { ", r t": ["editor::SpawnNearestTask", { "reveal": "no_focus" }], } } ``` Demo: https://github.com/user-attachments/assets/0d1818f0-7ae4-4200-8c3e-0ed47550c298 --------- Co-authored-by: Bennet --- assets/settings/initial_tasks.json | 1 + crates/editor/src/actions.rs | 8 + crates/editor/src/editor.rs | 173 +++++++++++++++++---- crates/editor/src/editor_tests.rs | 83 ++++++++++ crates/editor/src/element.rs | 3 +- crates/task/src/task_template.rs | 2 + crates/terminal_view/src/terminal_panel.rs | 26 +++- docs/src/tasks.md | 1 + 8 files changed, 259 insertions(+), 38 deletions(-) diff --git a/assets/settings/initial_tasks.json b/assets/settings/initial_tasks.json index 72f1da0173..31808ac632 100644 --- a/assets/settings/initial_tasks.json +++ b/assets/settings/initial_tasks.json @@ -16,6 +16,7 @@ "allow_concurrent_runs": false, // What to do with the terminal pane and tab, after the command was started: // * `always` — always show the terminal pane, add and focus the corresponding task's tab in it (default) + // * `no_focus` — always show the terminal pane, add/reuse the task's tab there, but don't focus it // * `never` — avoid changing current terminal pane focus, but still add/reuse the task's tab there "reveal": "always", // What to do with the terminal pane and tab, after the command had finished: diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index 474655700c..3d932b931a 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -159,6 +159,13 @@ pub struct DeleteToPreviousWordStart { pub struct FoldAtLevel { pub level: u32, } + +#[derive(PartialEq, Clone, Deserialize, Default)] +pub struct SpawnNearestTask { + #[serde(default)] + pub reveal: task::RevealStrategy, +} + impl_actions!( editor, [ @@ -184,6 +191,7 @@ impl_actions!( SelectToBeginningOfLine, SelectToEndOfLine, SelectUpByLines, + SpawnNearestTask, ShowCompletions, ToggleCodeActions, ToggleComments, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 16ba3d8b8b..27bccb40b3 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -502,6 +502,19 @@ struct RunnableTasks { context_range: Range, } +impl RunnableTasks { + fn resolve<'a>( + &'a self, + cx: &'a task::TaskContext, + ) -> impl Iterator + 'a { + self.templates.iter().filter_map(|(kind, template)| { + template + .resolve_task(&kind.to_id_base(), cx) + .map(|task| (kind.clone(), task)) + }) + } +} + #[derive(Clone)] struct ResolvedTasks { templates: SmallVec<[(TaskSourceKind, ResolvedTask); 1]>, @@ -4723,29 +4736,7 @@ impl Editor { .as_ref() .zip(editor.project.clone()) .map(|(tasks, project)| { - let position = Point::new(buffer_row, tasks.column); - let range_start = buffer.read(cx).anchor_at(position, Bias::Right); - let location = Location { - buffer: buffer.clone(), - range: range_start..range_start, - }; - // Fill in the environmental variables from the tree-sitter captures - let mut captured_task_variables = TaskVariables::default(); - for (capture_name, value) in tasks.extra_variables.clone() { - captured_task_variables.insert( - task::VariableName::Custom(capture_name.into()), - value.clone(), - ); - } - project.update(cx, |project, cx| { - project.task_store().update(cx, |task_store, cx| { - task_store.task_context_for_location( - captured_task_variables, - location, - cx, - ) - }) - }) + Self::build_tasks_context(&project, &buffer, buffer_row, tasks, cx) }); Some(cx.spawn(|editor, mut cx| async move { @@ -4756,15 +4747,7 @@ impl Editor { let resolved_tasks = tasks.zip(task_context).map(|(tasks, task_context)| { Arc::new(ResolvedTasks { - templates: tasks - .templates - .iter() - .filter_map(|(kind, template)| { - template - .resolve_task(&kind.to_id_base(), &task_context) - .map(|task| (kind.clone(), task)) - }) - .collect(), + templates: tasks.resolve(&task_context).collect(), position: snapshot.buffer_snapshot.anchor_before(Point::new( multibuffer_point.row, tasks.column, @@ -5470,6 +5453,132 @@ impl Editor { } } + fn build_tasks_context( + project: &Model, + buffer: &Model, + buffer_row: u32, + tasks: &Arc, + cx: &mut ViewContext, + ) -> Task> { + let position = Point::new(buffer_row, tasks.column); + let range_start = buffer.read(cx).anchor_at(position, Bias::Right); + let location = Location { + buffer: buffer.clone(), + range: range_start..range_start, + }; + // Fill in the environmental variables from the tree-sitter captures + let mut captured_task_variables = TaskVariables::default(); + for (capture_name, value) in tasks.extra_variables.clone() { + captured_task_variables.insert( + task::VariableName::Custom(capture_name.into()), + value.clone(), + ); + } + project.update(cx, |project, cx| { + project.task_store().update(cx, |task_store, cx| { + task_store.task_context_for_location(captured_task_variables, location, cx) + }) + }) + } + + pub fn spawn_nearest_task(&mut self, action: &SpawnNearestTask, cx: &mut ViewContext) { + let Some((workspace, _)) = self.workspace.clone() else { + return; + }; + let Some(project) = self.project.clone() else { + return; + }; + + // Try to find a closest, enclosing node using tree-sitter that has a + // task + let Some((buffer, buffer_row, tasks)) = self + .find_enclosing_node_task(cx) + // Or find the task that's closest in row-distance. + .or_else(|| self.find_closest_task(cx)) + else { + return; + }; + + let reveal_strategy = action.reveal; + let task_context = Self::build_tasks_context(&project, &buffer, buffer_row, &tasks, cx); + cx.spawn(|_, mut cx| async move { + let context = task_context.await?; + let (task_source_kind, mut resolved_task) = tasks.resolve(&context).next()?; + + let resolved = resolved_task.resolved.as_mut()?; + resolved.reveal = reveal_strategy; + + workspace + .update(&mut cx, |workspace, cx| { + workspace::tasks::schedule_resolved_task( + workspace, + task_source_kind, + resolved_task, + false, + cx, + ); + }) + .ok() + }) + .detach(); + } + + fn find_closest_task( + &mut self, + cx: &mut ViewContext, + ) -> Option<(Model, u32, Arc)> { + let cursor_row = self.selections.newest_adjusted(cx).head().row; + + let ((buffer_id, row), tasks) = self + .tasks + .iter() + .min_by_key(|((_, row), _)| cursor_row.abs_diff(*row))?; + + let buffer = self.buffer.read(cx).buffer(*buffer_id)?; + let tasks = Arc::new(tasks.to_owned()); + Some((buffer, *row, tasks)) + } + + fn find_enclosing_node_task( + &mut self, + cx: &mut ViewContext, + ) -> Option<(Model, u32, Arc)> { + let snapshot = self.buffer.read(cx).snapshot(cx); + let offset = self.selections.newest::(cx).head(); + let excerpt = snapshot.excerpt_containing(offset..offset)?; + let buffer_id = excerpt.buffer().remote_id(); + + let layer = excerpt.buffer().syntax_layer_at(offset)?; + let mut cursor = layer.node().walk(); + + while cursor.goto_first_child_for_byte(offset).is_some() { + if cursor.node().end_byte() == offset { + cursor.goto_next_sibling(); + } + } + + // Ascend to the smallest ancestor that contains the range and has a task. + loop { + let node = cursor.node(); + let node_range = node.byte_range(); + let symbol_start_row = excerpt.buffer().offset_to_point(node.start_byte()).row; + + // Check if this node contains our offset + if node_range.start <= offset && node_range.end >= offset { + // If it contains offset, check for task + if let Some(tasks) = self.tasks.get(&(buffer_id, symbol_start_row)) { + let buffer = self.buffer.read(cx).buffer(buffer_id)?; + return Some((buffer, symbol_start_row, Arc::new(tasks.to_owned()))); + } + } + + if !cursor.goto_parent() { + break; + } + } + None + } + fn render_run_indicator( &self, _style: &EditorStyle, diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 02583889bc..92d62fac7f 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -13330,6 +13330,89 @@ async fn test_goto_definition_with_find_all_references_fallback(cx: &mut gpui::T }); } +#[gpui::test] +async fn test_find_enclosing_node_with_task(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let language = Arc::new(Language::new( + LanguageConfig::default(), + Some(tree_sitter_rust::LANGUAGE.into()), + )); + + let text = r#" + #[cfg(test)] + mod tests() { + #[test] + fn runnable_1() { + let a = 1; + } + + #[test] + fn runnable_2() { + let a = 1; + let b = 2; + } + } + "# + .unindent(); + + let fs = FakeFs::new(cx.executor()); + fs.insert_file("/file.rs", Default::default()).await; + + let project = Project::test(fs, ["/a".as_ref()], cx).await; + let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx); + let buffer = cx.new_model(|cx| Buffer::local(text, cx).with_language(language, cx)); + let multi_buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer.clone(), cx)); + + let editor = cx.new_view(|cx| { + Editor::new( + EditorMode::Full, + multi_buffer, + Some(project.clone()), + true, + cx, + ) + }); + + editor.update(cx, |editor, cx| { + editor.tasks.insert( + (buffer.read(cx).remote_id(), 3), + RunnableTasks { + templates: vec![], + offset: MultiBufferOffset(43), + column: 0, + extra_variables: HashMap::default(), + context_range: BufferOffset(43)..BufferOffset(85), + }, + ); + editor.tasks.insert( + (buffer.read(cx).remote_id(), 8), + RunnableTasks { + templates: vec![], + offset: MultiBufferOffset(86), + column: 0, + extra_variables: HashMap::default(), + context_range: BufferOffset(86)..BufferOffset(191), + }, + ); + + // Test finding task when cursor is inside function body + editor.change_selections(None, cx, |s| { + s.select_ranges([Point::new(4, 5)..Point::new(4, 5)]) + }); + let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap(); + assert_eq!(row, 3, "Should find task for cursor inside runnable_1"); + + // Test finding task when cursor is on function name + editor.change_selections(None, cx, |s| { + s.select_ranges([Point::new(8, 4)..Point::new(8, 4)]) + }); + let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap(); + assert_eq!(row, 8, "Should find task when cursor is on function name"); + }); +} + fn empty_range(row: usize, column: usize) -> Range { let point = DisplayPoint::new(DisplayRow(row as u32), column as u32); point..point diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index ac4d5d2340..a3d634378a 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -449,7 +449,8 @@ impl EditorElement { register_action(view, cx, Editor::apply_all_diff_hunks); register_action(view, cx, Editor::apply_selected_diff_hunks); register_action(view, cx, Editor::open_active_item_in_terminal); - register_action(view, cx, Editor::reload_file) + register_action(view, cx, Editor::reload_file); + register_action(view, cx, Editor::spawn_nearest_task); } fn register_key_listeners(&self, cx: &mut WindowContext, layout: &EditorLayout) { diff --git a/crates/task/src/task_template.rs b/crates/task/src/task_template.rs index 2bf40f52ae..b72a0d25f8 100644 --- a/crates/task/src/task_template.rs +++ b/crates/task/src/task_template.rs @@ -66,6 +66,8 @@ pub enum RevealStrategy { /// Always show the terminal pane, add and focus the corresponding task's tab in it. #[default] Always, + /// Always show the terminal pane, add the task's tab in it, but don't focus it. + NoFocus, /// Do not change terminal pane focus, but still add/reuse the task's tab there. Never, } diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 582d0d78c3..6d64ac1a48 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -575,9 +575,9 @@ impl TerminalPanel { .collect() } - fn activate_terminal_view(&self, item_index: usize, cx: &mut WindowContext) { + fn activate_terminal_view(&self, item_index: usize, focus: bool, cx: &mut WindowContext) { self.pane.update(cx, |pane, cx| { - pane.activate_item(item_index, true, true, cx) + pane.activate_item(item_index, true, focus, cx) }) } @@ -616,8 +616,14 @@ impl TerminalPanel { pane.add_item(terminal_view, true, focus, None, cx); }); - if reveal_strategy == RevealStrategy::Always { - workspace.focus_panel::(cx); + match reveal_strategy { + RevealStrategy::Always => { + workspace.focus_panel::(cx); + } + RevealStrategy::NoFocus => { + workspace.open_panel::(cx); + } + RevealStrategy::Never => {} } Ok(terminal) })?; @@ -698,7 +704,7 @@ impl TerminalPanel { match reveal { RevealStrategy::Always => { - self.activate_terminal_view(terminal_item_index, cx); + self.activate_terminal_view(terminal_item_index, true, cx); let task_workspace = self.workspace.clone(); cx.spawn(|_, mut cx| async move { task_workspace @@ -707,6 +713,16 @@ impl TerminalPanel { }) .detach(); } + RevealStrategy::NoFocus => { + self.activate_terminal_view(terminal_item_index, false, cx); + let task_workspace = self.workspace.clone(); + cx.spawn(|_, mut cx| async move { + task_workspace + .update(&mut cx, |workspace, cx| workspace.open_panel::(cx)) + .ok() + }) + .detach(); + } RevealStrategy::Never => {} } diff --git a/docs/src/tasks.md b/docs/src/tasks.md index 42a1ac4c38..3f81aefc39 100644 --- a/docs/src/tasks.md +++ b/docs/src/tasks.md @@ -18,6 +18,7 @@ Zed supports ways to spawn (and rerun) commands using its integrated terminal to "allow_concurrent_runs": false, // What to do with the terminal pane and tab, after the command was started: // * `always` — always show the terminal pane, add and focus the corresponding task's tab in it (default) + // * `no_focus` — always show the terminal pane, add/reuse the task's tab there, but don't focus it // * `never` — avoid changing current terminal pane focus, but still add/reuse the task's tab there "reveal": "always", // What to do with the terminal pane and tab, after the command had finished: From 5b6401519bb731f839c05831927821df03d74d3e Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Thu, 31 Oct 2024 14:33:36 +0100 Subject: [PATCH 10/45] activity indicator: Reset formatting failure on click (#20029) Release Notes: - N/A --- crates/activity_indicator/src/activity_indicator.rs | 5 ++++- crates/project/src/lsp_store.rs | 4 ++++ crates/project/src/project.rs | 5 +++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/crates/activity_indicator/src/activity_indicator.rs b/crates/activity_indicator/src/activity_indicator.rs index 90410d534c..f06ebe4b23 100644 --- a/crates/activity_indicator/src/activity_indicator.rs +++ b/crates/activity_indicator/src/activity_indicator.rs @@ -352,7 +352,10 @@ impl ActivityIndicator { .into_any_element(), ), message: format!("Formatting failed: {}. Click to see logs.", failure), - on_click: Some(Arc::new(|_, cx| { + on_click: Some(Arc::new(|indicator, cx| { + indicator.project.update(cx, |project, cx| { + project.reset_last_formatting_failure(cx); + }); cx.dispatch_action(Box::new(workspace::OpenLog)); })), }); diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index fe4127d536..bc0bfdf416 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -5270,6 +5270,10 @@ impl LspStore { self.last_formatting_failure.as_deref() } + pub fn reset_last_formatting_failure(&mut self) { + self.last_formatting_failure = None; + } + pub fn environment_for_buffer( &self, buffer: &Model, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 788de66996..41b5f8bfbc 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -2414,6 +2414,11 @@ impl Project { self.lsp_store.read(cx).last_formatting_failure() } + pub fn reset_last_formatting_failure(&self, cx: &mut AppContext) { + self.lsp_store + .update(cx, |store, _| store.reset_last_formatting_failure()); + } + pub fn update_diagnostics( &mut self, language_server_id: LanguageServerId, From 9dad897d49f93b4e0b0cc4e9127ffee29e5cdaf7 Mon Sep 17 00:00:00 2001 From: Kyle Kelley Date: Thu, 31 Oct 2024 07:01:46 -0700 Subject: [PATCH 11/45] Clean up notebook item creation in project (#20030) * Implement `clone_on_split` to allow splitting a notebook into another pane * Switched to `tab_content` in `impl Item for NotebookEditor` to show both the notebook name and an icon * Added placeholder methods and TODOs for future work, such as saving, reloading, and search functionality within the notebook editor. * Started moving more core `Model` bits into `NotebookItem`, including pulling the language of the notebook (which affects every code cell) * Loaded notebook asynchronously using `fs` Release Notes: - N/A --------- Co-authored-by: Mikayla --- crates/repl/src/notebook/notebook_ui.rs | 246 ++++++++++++++++-------- 1 file changed, 166 insertions(+), 80 deletions(-) diff --git a/crates/repl/src/notebook/notebook_ui.rs b/crates/repl/src/notebook/notebook_ui.rs index 36d6e29385..32f07ce626 100644 --- a/crates/repl/src/notebook/notebook_ui.rs +++ b/crates/repl/src/notebook/notebook_ui.rs @@ -1,18 +1,22 @@ #![allow(unused, dead_code)] +use std::future::Future; use std::{path::PathBuf, sync::Arc}; +use anyhow::{Context as _, Result}; use client::proto::ViewId; use collections::HashMap; use feature_flags::{FeatureFlagAppExt as _, NotebookFeatureFlag}; +use futures::future::Shared; use futures::FutureExt; use gpui::{ - actions, list, prelude::*, AppContext, EventEmitter, FocusHandle, FocusableView, - ListScrollEvent, ListState, Model, Task, + actions, list, prelude::*, AnyElement, AppContext, EventEmitter, FocusHandle, FocusableView, + ListScrollEvent, ListState, Model, Point, Task, View, }; -use language::LanguageRegistry; +use language::{Language, LanguageRegistry}; use project::{Project, ProjectEntryId, ProjectPath}; use ui::{prelude::*, Tooltip}; -use workspace::item::ItemEvent; +use workspace::item::{ItemEvent, TabContentParams}; +use workspace::searchable::SearchableItemHandle; use workspace::{Item, ItemHandle, ProjectItem, ToolbarItemLocation}; use workspace::{ToolbarItemEvent, ToolbarItemView}; @@ -21,9 +25,6 @@ use super::{Cell, CellPosition, RenderableCell}; use nbformat::v4::CellId; use nbformat::v4::Metadata as NotebookMetadata; -pub(crate) const DEFAULT_NOTEBOOK_FORMAT: i32 = 4; -pub(crate) const DEFAULT_NOTEBOOK_FORMAT_MINOR: i32 = 0; - actions!( notebook, [ @@ -65,17 +66,14 @@ pub fn init(cx: &mut AppContext) { pub struct NotebookEditor { languages: Arc, + project: Model, focus_handle: FocusHandle, - project: Model, - path: ProjectPath, + notebook_item: Model, remote_id: Option, cell_list: ListState, - metadata: NotebookMetadata, - nbformat: i32, - nbformat_minor: i32, selected_cell_index: usize, cell_order: Vec, cell_map: HashMap, @@ -89,47 +87,23 @@ impl NotebookEditor { ) -> Self { let focus_handle = cx.focus_handle(); - let notebook = notebook_item.read(cx).notebook.clone(); - let languages = project.read(cx).languages().clone(); + let language_name = notebook_item.read(cx).language_name(); - let metadata = notebook.metadata; - let nbformat = notebook.nbformat; - let nbformat_minor = notebook.nbformat_minor; + let notebook_language = notebook_item.read(cx).notebook_language(); + let notebook_language = cx.spawn(|_, _| notebook_language).shared(); - let language_name = metadata - .language_info - .as_ref() - .map(|l| l.name.clone()) - .or(metadata - .kernelspec - .as_ref() - .and_then(|spec| spec.language.clone())); + let mut cell_order = vec![]; // Vec + let mut cell_map = HashMap::default(); // HashMap - let notebook_language = if let Some(language_name) = language_name { - cx.spawn(|_, _| { - let languages = languages.clone(); - async move { languages.language_for_name(&language_name).await.ok() } - }) - .shared() - } else { - Task::ready(None).shared() - }; - - let languages = project.read(cx).languages().clone(); - let notebook_language = cx - .spawn(|_, _| { - // todo: pull from notebook metadata - const TODO: &'static str = "Python"; - let languages = languages.clone(); - async move { languages.language_for_name(TODO).await.ok() } - }) - .shared(); - - let mut cell_order = vec![]; - let mut cell_map = HashMap::default(); - - for (index, cell) in notebook.cells.iter().enumerate() { + for (index, cell) in notebook_item + .read(cx) + .notebook + .clone() + .cells + .iter() + .enumerate() + { let cell_id = cell.id(); cell_order.push(cell_id.clone()); cell_map.insert( @@ -140,44 +114,35 @@ impl NotebookEditor { let view = cx.view().downgrade(); let cell_count = cell_order.len(); - let cell_order_for_list = cell_order.clone(); - let cell_map_for_list = cell_map.clone(); + let this = cx.view(); let cell_list = ListState::new( cell_count, gpui::ListAlignment::Top, - // TODO: This is a totally random number, - // not sure what this should be - px(3000.), + px(1000.), move |ix, cx| { - let cell_order_for_list = cell_order_for_list.clone(); - let cell_id = cell_order_for_list[ix].clone(); - if let Some(view) = view.upgrade() { - let cell_id = cell_id.clone(); - if let Some(cell) = cell_map_for_list.clone().get(&cell_id) { - view.update(cx, |view, cx| { - view.render_cell(ix, cell, cx).into_any_element() + view.upgrade() + .and_then(|notebook_handle| { + notebook_handle.update(cx, |notebook, cx| { + notebook + .cell_order + .get(ix) + .and_then(|cell_id| notebook.cell_map.get(cell_id)) + .map(|cell| notebook.render_cell(ix, cell, cx).into_any_element()) }) - } else { - div().into_any() - } - } else { - div().into_any() - } + }) + .unwrap_or_else(|| div().into_any()) }, ); Self { + project, languages: languages.clone(), focus_handle, - project, - path: notebook_item.read(cx).project_path.clone(), + notebook_item, remote_id: None, cell_list, selected_cell_index: 0, - metadata, - nbformat, - nbformat_minor, cell_order: cell_order.clone(), cell_map: cell_map.clone(), } @@ -524,10 +489,15 @@ impl FocusableView for NotebookEditor { } } +// Intended to be a NotebookBuffer pub struct NotebookItem { path: PathBuf, project_path: ProjectPath, + languages: Arc, + // Raw notebook data notebook: nbformat::v4::Notebook, + // Store our version of the notebook in memory (cell_order, cell_map) + id: ProjectEntryId, } impl project::Item for NotebookItem { @@ -538,6 +508,8 @@ impl project::Item for NotebookItem { ) -> Option>>> { let path = path.clone(); let project = project.clone(); + let fs = project.read(cx).fs().clone(); + let languages = project.read(cx).languages().clone(); if path.path.extension().unwrap_or_default() == "ipynb" { Some(cx.spawn(|mut cx| async move { @@ -545,26 +517,36 @@ impl project::Item for NotebookItem { .read_with(&cx, |project, cx| project.absolute_path(&path, cx))? .ok_or_else(|| anyhow::anyhow!("Failed to find the absolute path"))?; - let file_content = std::fs::read_to_string(abs_path.clone())?; + // todo: watch for changes to the file + let file_content = fs.load(&abs_path.as_path()).await?; let notebook = nbformat::parse_notebook(&file_content); let notebook = match notebook { Ok(nbformat::Notebook::V4(notebook)) => notebook, + // 4.1 - 4.4 are converted to 4.5 Ok(nbformat::Notebook::Legacy(legacy_notebook)) => { // todo!(): Decide if we want to mutate the notebook by including Cell IDs // and any other conversions let notebook = nbformat::upgrade_legacy_notebook(legacy_notebook)?; notebook } + // Bad notebooks and notebooks v4.0 and below are not supported Err(e) => { anyhow::bail!("Failed to parse notebook: {:?}", e); } }; + let id = project + .update(&mut cx, |project, cx| project.entry_for_path(&path, cx))? + .context("Entry not found")? + .id; + cx.new_model(|_| NotebookItem { path: abs_path, project_path: path, + languages, notebook, + id, }) })) } else { @@ -573,7 +555,7 @@ impl project::Item for NotebookItem { } fn entry_id(&self, _: &AppContext) -> Option { - None + Some(self.id) } fn project_path(&self, _: &AppContext) -> Option { @@ -581,6 +563,35 @@ impl project::Item for NotebookItem { } } +impl NotebookItem { + pub fn language_name(&self) -> Option { + self.notebook + .metadata + .language_info + .as_ref() + .map(|l| l.name.clone()) + .or(self + .notebook + .metadata + .kernelspec + .as_ref() + .and_then(|spec| spec.language.clone())) + } + + pub fn notebook_language(&self) -> impl Future>> { + let language_name = self.language_name(); + let languages = self.languages.clone(); + + async move { + if let Some(language_name) = language_name { + languages.language_for_name(&language_name).await.ok() + } else { + None + } + } + } +} + impl EventEmitter<()> for NotebookEditor {} // pub struct NotebookControls { @@ -631,12 +642,41 @@ impl EventEmitter<()> for NotebookEditor {} impl Item for NotebookEditor { type Event = (); - fn tab_content_text(&self, _cx: &WindowContext) -> Option { - let path = self.path.path.clone(); + fn clone_on_split( + &self, + _workspace_id: Option, + cx: &mut ViewContext, + ) -> Option> + where + Self: Sized, + { + Some(cx.new_view(|cx| Self::new(self.project.clone(), self.notebook_item.clone(), cx))) + } - path.file_stem() - .map(|stem| stem.to_string_lossy().into_owned()) - .map(SharedString::from) + fn for_each_project_item( + &self, + cx: &AppContext, + f: &mut dyn FnMut(gpui::EntityId, &dyn project::Item), + ) { + f(self.notebook_item.entity_id(), self.notebook_item.read(cx)) + } + + fn is_singleton(&self, _cx: &AppContext) -> bool { + true + } + + fn tab_content(&self, params: TabContentParams, cx: &WindowContext) -> AnyElement { + let path = &self.notebook_item.read(cx).path; + let title = path + .file_name() + .unwrap_or_else(|| path.as_os_str()) + .to_string_lossy() + .to_string(); + Label::new(title) + .single_line() + .color(params.text_color()) + .italic(params.preview) + .into_any_element() } fn tab_icon(&self, _cx: &ui::WindowContext) -> Option { @@ -647,8 +687,54 @@ impl Item for NotebookEditor { false } + // TODO + fn pixel_position_of_cursor(&self, _: &AppContext) -> Option> { + None + } + + // TODO + fn as_searchable(&self, _: &View) -> Option> { + None + } + + fn set_nav_history(&mut self, _: workspace::ItemNavHistory, _: &mut ViewContext) { + // TODO + } + + // TODO + fn can_save(&self, _cx: &AppContext) -> bool { + false + } + // TODO + fn save( + &mut self, + _format: bool, + _project: Model, + _cx: &mut ViewContext, + ) -> Task> { + unimplemented!("save() must be implemented if can_save() returns true") + } + + // TODO + fn save_as( + &mut self, + _project: Model, + _path: ProjectPath, + _cx: &mut ViewContext, + ) -> Task> { + unimplemented!("save_as() must be implemented if can_save() returns true") + } + // TODO + fn reload( + &mut self, + _project: Model, + _cx: &mut ViewContext, + ) -> Task> { + unimplemented!("reload() must be implemented if can_save() returns true") + } + fn is_dirty(&self, cx: &AppContext) -> bool { - // self.is_dirty(cx) + // self.is_dirty(cx) TODO false } } From f766f6ceae5ecebc674872eb4d82652f3dd2e6c5 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 31 Oct 2024 16:29:19 +0200 Subject: [PATCH 12/45] Do less work when revealing entries in the outline panel (#20031) Before this change, we were trying to determine current element before debouncing, causing a lot of extra work on caret movement. Now, we only do this for the task that managed to wait the entire debounce period. Closes https://github.com/zed-industries/zed/issues/19817 Closes https://github.com/zed-industries/zed/issues/14235 Release Notes: - Fixed outline panel-related performance issues when selections change in the large document ([#19817](https://github.com/zed-industries/zed/issues/19817)), ([#14235](https://github.com/zed-industries/zed/issues/14235)) --- crates/outline_panel/src/outline_panel.rs | 45 ++++++++++++++--------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index 6ffac21021..7de8872c64 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -1440,26 +1440,26 @@ impl OutlinePanel { } } - fn reveal_entry_for_selection( - &mut self, - editor: &View, - cx: &mut ViewContext<'_, Self>, - ) { + fn reveal_entry_for_selection(&mut self, editor: View, cx: &mut ViewContext<'_, Self>) { if !self.active { return; } if !OutlinePanelSettings::get_global(cx).auto_reveal_entries { return; } - let Some(entry_with_selection) = self.location_for_editor_selection(editor, cx) else { - self.selected_entry = SelectedEntry::None; - cx.notify(); - return; - }; - let project = self.project.clone(); self.reveal_selection_task = cx.spawn(|outline_panel, mut cx| async move { cx.background_executor().timer(UPDATE_DEBOUNCE).await; + let entry_with_selection = outline_panel.update(&mut cx, |outline_panel, cx| { + outline_panel.location_for_editor_selection(&editor, cx) + })?; + let Some(entry_with_selection) = entry_with_selection else { + outline_panel.update(&mut cx, |outline_panel, cx| { + outline_panel.selected_entry = SelectedEntry::None; + cx.notify(); + })?; + return Ok(()); + }; let related_buffer_entry = match &entry_with_selection { PanelEntry::Fs(FsEntry::File(worktree_id, _, buffer_id, _)) => { project.update(&mut cx, |project, cx| { @@ -2436,7 +2436,7 @@ impl OutlinePanel { } fn location_for_editor_selection( - &mut self, + &self, editor: &View, cx: &mut ViewContext, ) -> Option { @@ -2500,7 +2500,7 @@ impl OutlinePanel { } fn outline_location( - &mut self, + &self, buffer_id: BufferId, excerpt_id: ExcerptId, multi_buffer_snapshot: editor::MultiBufferSnapshot, @@ -4321,7 +4321,7 @@ fn subscribe_for_editor_events( editor, move |outline_panel, editor, e: &EditorEvent, cx| match e { EditorEvent::SelectionsChanged { local: true } => { - outline_panel.reveal_entry_for_selection(&editor, cx); + outline_panel.reveal_entry_for_selection(editor, cx); cx.notify(); } EditorEvent::ExcerptsAdded { excerpts, .. } => { @@ -4479,7 +4479,13 @@ mod tests { cx.executor() .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); cx.run_until_parked(); - outline_panel.update(cx, |outline_panel, _| { + outline_panel.update(cx, |outline_panel, cx| { + // Project search re-adds items to the buffer, removing the caret from it. + // Select the first entry and move 4 elements down. + for _ in 0..6 { + outline_panel.select_next(&SelectNext, cx); + } + assert_eq!( display_entries( &outline_panel.cached_entries, @@ -4795,7 +4801,7 @@ mod tests { r#"/ public/lottie/ syntax-tree.json - search: { "something": "static" } <==== selected + search: { "something": "static" } src/ app/(site)/ (about)/jobs/[slug]/ @@ -4811,8 +4817,11 @@ mod tests { }); outline_panel.update(cx, |outline_panel, cx| { - outline_panel.select_next(&SelectNext, cx); - outline_panel.select_next(&SelectNext, cx); + // After the search is done, we have updated the outline panel contents and caret is not in any excerot, so there are no selections. + // Move to 5th element in the list (0th action will selection the first element) + for _ in 0..6 { + outline_panel.select_next(&SelectNext, cx); + } outline_panel.collapse_selected_entry(&CollapseSelectedEntry, cx); }); cx.run_until_parked(); From 8d1f377bf0651394103e1fb94f07d55de969508e Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 31 Oct 2024 10:53:47 -0400 Subject: [PATCH 13/45] assistant: Add example streaming slash command (#20034) This PR adds a `/streaming-example` slash command for the purposes of showcasing streaming during development. This slash command is only available to staff and isn't intended to be shipped to the general public. Release Notes: - N/A --- crates/assistant/src/assistant.rs | 14 ++ crates/assistant/src/slash_command.rs | 1 + .../streaming_example_command.rs | 136 ++++++++++++++++++ 3 files changed, 151 insertions(+) create mode 100644 crates/assistant/src/slash_command/streaming_example_command.rs diff --git a/crates/assistant/src/assistant.rs b/crates/assistant/src/assistant.rs index c2857d06d4..6e6f7a823e 100644 --- a/crates/assistant/src/assistant.rs +++ b/crates/assistant/src/assistant.rs @@ -51,6 +51,7 @@ use std::sync::Arc; pub(crate) use streaming_diff::*; use util::ResultExt; +use crate::slash_command::streaming_example_command; use crate::slash_command_settings::SlashCommandSettings; actions!( @@ -468,6 +469,19 @@ fn register_slash_commands(prompt_builder: Option>, cx: &mut }) .detach(); + cx.observe_flag::({ + let slash_command_registry = slash_command_registry.clone(); + move |is_enabled, _cx| { + if is_enabled { + slash_command_registry.register_command( + streaming_example_command::StreamingExampleSlashCommand, + false, + ); + } + } + }) + .detach(); + update_slash_commands_from_settings(cx); cx.observe_global::(update_slash_commands_from_settings) .detach(); diff --git a/crates/assistant/src/slash_command.rs b/crates/assistant/src/slash_command.rs index ed20791d95..2209308081 100644 --- a/crates/assistant/src/slash_command.rs +++ b/crates/assistant/src/slash_command.rs @@ -31,6 +31,7 @@ pub mod now_command; pub mod project_command; pub mod prompt_command; pub mod search_command; +pub mod streaming_example_command; pub mod symbols_command; pub mod tab_command; pub mod terminal_command; diff --git a/crates/assistant/src/slash_command/streaming_example_command.rs b/crates/assistant/src/slash_command/streaming_example_command.rs new file mode 100644 index 0000000000..ae805669d2 --- /dev/null +++ b/crates/assistant/src/slash_command/streaming_example_command.rs @@ -0,0 +1,136 @@ +use std::sync::atomic::AtomicBool; +use std::sync::Arc; +use std::time::Duration; + +use anyhow::Result; +use assistant_slash_command::{ + ArgumentCompletion, SlashCommand, SlashCommandContent, SlashCommandEvent, + SlashCommandOutputSection, SlashCommandResult, +}; +use feature_flags::FeatureFlag; +use futures::channel::mpsc; +use gpui::{Task, WeakView}; +use language::{BufferSnapshot, LspAdapterDelegate}; +use smol::stream::StreamExt; +use smol::Timer; +use ui::prelude::*; +use workspace::Workspace; + +pub struct StreamingExampleSlashCommandFeatureFlag; + +impl FeatureFlag for StreamingExampleSlashCommandFeatureFlag { + const NAME: &'static str = "streaming-example-slash-command"; +} + +pub(crate) struct StreamingExampleSlashCommand; + +impl SlashCommand for StreamingExampleSlashCommand { + fn name(&self) -> String { + "streaming-example".into() + } + + fn description(&self) -> String { + "An example slash command that showcases streaming.".into() + } + + fn menu_text(&self) -> String { + self.description() + } + + fn requires_argument(&self) -> bool { + false + } + + fn complete_argument( + self: Arc, + _arguments: &[String], + _cancel: Arc, + _workspace: Option>, + _cx: &mut WindowContext, + ) -> Task>> { + Task::ready(Ok(Vec::new())) + } + + fn run( + self: Arc, + _arguments: &[String], + _context_slash_command_output_sections: &[SlashCommandOutputSection], + _context_buffer: BufferSnapshot, + _workspace: WeakView, + _delegate: Option>, + cx: &mut WindowContext, + ) -> Task { + let (events_tx, events_rx) = mpsc::unbounded(); + cx.background_executor() + .spawn(async move { + events_tx.unbounded_send(Ok(SlashCommandEvent::StartSection { + icon: IconName::FileRust, + label: "Section 1".into(), + metadata: None, + }))?; + events_tx.unbounded_send(Ok(SlashCommandEvent::Content( + SlashCommandContent::Text { + text: "Hello".into(), + run_commands_in_text: false, + }, + )))?; + events_tx.unbounded_send(Ok(SlashCommandEvent::EndSection { metadata: None }))?; + events_tx.unbounded_send(Ok(SlashCommandEvent::Content( + SlashCommandContent::Text { + text: "\n".into(), + run_commands_in_text: false, + }, + )))?; + + Timer::after(Duration::from_secs(1)).await; + + events_tx.unbounded_send(Ok(SlashCommandEvent::StartSection { + icon: IconName::FileRust, + label: "Section 2".into(), + metadata: None, + }))?; + events_tx.unbounded_send(Ok(SlashCommandEvent::Content( + SlashCommandContent::Text { + text: "World".into(), + run_commands_in_text: false, + }, + )))?; + events_tx.unbounded_send(Ok(SlashCommandEvent::EndSection { metadata: None }))?; + events_tx.unbounded_send(Ok(SlashCommandEvent::Content( + SlashCommandContent::Text { + text: "\n".into(), + run_commands_in_text: false, + }, + )))?; + + for n in 1..=10 { + Timer::after(Duration::from_secs(1)).await; + + events_tx.unbounded_send(Ok(SlashCommandEvent::StartSection { + icon: IconName::StarFilled, + label: format!("Section {n}").into(), + metadata: None, + }))?; + events_tx.unbounded_send(Ok(SlashCommandEvent::Content( + SlashCommandContent::Text { + text: "lorem ipsum ".repeat(n).trim().into(), + run_commands_in_text: false, + }, + )))?; + events_tx + .unbounded_send(Ok(SlashCommandEvent::EndSection { metadata: None }))?; + events_tx.unbounded_send(Ok(SlashCommandEvent::Content( + SlashCommandContent::Text { + text: "\n".into(), + run_commands_in_text: false, + }, + )))?; + } + + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + + Task::ready(Ok(events_rx.boxed())) + } +} From 9c77bcc827bca332d896b224dcdd500272687f5d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 31 Oct 2024 11:28:37 -0400 Subject: [PATCH 14/45] Update actions/setup-node digest to 39370e3 (#19979) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [actions/setup-node](https://redirect.github.com/actions/setup-node) | action | digest | `0a44ba7` -> `39370e3` | --- ### Configuration 📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone America/New_York, Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- Release Notes: - N/A Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/actions/run_tests/action.yml | 2 +- .github/workflows/ci.yml | 2 +- .github/workflows/danger.yml | 2 +- .github/workflows/randomized_tests.yml | 2 +- .github/workflows/release_nightly.yml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/actions/run_tests/action.yml b/.github/actions/run_tests/action.yml index 07284e2f58..df714be003 100644 --- a/.github/actions/run_tests/action.yml +++ b/.github/actions/run_tests/action.yml @@ -10,7 +10,7 @@ runs: cargo install cargo-nextest - name: Install Node - uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4 + uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4 with: node-version: "18" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 84ed0dd5d4..e78b24255c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -232,7 +232,7 @@ jobs: DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }} steps: - name: Install Node - uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4 + uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4 with: node-version: "18" diff --git a/.github/workflows/danger.yml b/.github/workflows/danger.yml index 132af3bda2..897d4b47c5 100644 --- a/.github/workflows/danger.yml +++ b/.github/workflows/danger.yml @@ -21,7 +21,7 @@ jobs: version: 9 - name: Setup Node - uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4 + uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4 with: node-version: "20" cache: "pnpm" diff --git a/.github/workflows/randomized_tests.yml b/.github/workflows/randomized_tests.yml index dd7163dc5e..1ecf511100 100644 --- a/.github/workflows/randomized_tests.yml +++ b/.github/workflows/randomized_tests.yml @@ -22,7 +22,7 @@ jobs: - buildjet-16vcpu-ubuntu-2204 steps: - name: Install Node - uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4 + uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4 with: node-version: "18" diff --git a/.github/workflows/release_nightly.yml b/.github/workflows/release_nightly.yml index 534855cd21..8e409f4947 100644 --- a/.github/workflows/release_nightly.yml +++ b/.github/workflows/release_nightly.yml @@ -70,7 +70,7 @@ jobs: ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }} steps: - name: Install Node - uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4 + uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4 with: node-version: "18" From a347c4def7f1d88f9c4fbcd1a82cd5d465a8d386 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Thu, 31 Oct 2024 11:40:38 -0400 Subject: [PATCH 15/45] Add theme preview (#20039) This PR adds a theme preview tab to help get an at a glance overview of the styles in a theme. ![CleanShot 2024-10-31 at 11 27 18@2x](https://github.com/user-attachments/assets/798e97cf-9f80-4994-b2fd-ac1dcd58e4d9) You can open it using `debug: open theme preview`. The next major theme preview PR will move this into it's own crate, as it will grow substantially as we add content. Next for theme preview: - Update layout to two columns, with controls on the right for selecting theme, layer/elevation-index, etc. - Cover more UI elements in preview - Display theme colors in a more helpful way - Add syntax & markdown previews Release Notes: - Added a way to preview the current theme's styles with the `debug: open theme preview` command. --- Cargo.lock | 1 + crates/gpui/src/color.rs | 15 +- crates/theme/Cargo.toml | 1 + crates/theme/src/styles/colors.rs | 237 +++++++++++++- crates/ui/src/styles/elevation.rs | 35 +- crates/ui/src/utils.rs | 2 + crates/ui/src/utils/color_contrast.rs | 70 ++++ crates/workspace/src/theme_preview.rs | 454 ++++++++++++++++++++++++++ crates/workspace/src/workspace.rs | 2 + 9 files changed, 813 insertions(+), 4 deletions(-) create mode 100644 crates/ui/src/utils/color_contrast.rs create mode 100644 crates/workspace/src/theme_preview.rs diff --git a/Cargo.lock b/Cargo.lock index 10e4622534..bea5b56126 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12093,6 +12093,7 @@ dependencies = [ "serde_json_lenient", "serde_repr", "settings", + "strum 0.25.0", "util", "uuid", ] diff --git a/crates/gpui/src/color.rs b/crates/gpui/src/color.rs index 24edace593..6a1f375b65 100644 --- a/crates/gpui/src/color.rs +++ b/crates/gpui/src/color.rs @@ -1,7 +1,7 @@ use anyhow::{bail, Context}; use serde::de::{self, Deserialize, Deserializer, Visitor}; use std::{ - fmt, + fmt::{self, Display, Formatter}, hash::{Hash, Hasher}, }; @@ -279,6 +279,19 @@ impl Hash for Hsla { } } +impl Display for Hsla { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "hsla({:.2}, {:.2}%, {:.2}%, {:.2})", + self.h * 360., + self.s * 100., + self.l * 100., + self.a + ) + } +} + /// Construct an [`Hsla`] object from plain values pub fn hsla(h: f32, s: f32, l: f32, a: f32) -> Hsla { Hsla { diff --git a/crates/theme/Cargo.toml b/crates/theme/Cargo.toml index b751bea727..c3e3a197cb 100644 --- a/crates/theme/Cargo.toml +++ b/crates/theme/Cargo.toml @@ -35,6 +35,7 @@ serde_json.workspace = true serde_json_lenient.workspace = true serde_repr.workspace = true settings.workspace = true +strum.workspace = true util.workspace = true uuid.workspace = true diff --git a/crates/theme/src/styles/colors.rs b/crates/theme/src/styles/colors.rs index d9ea58813c..99c1656215 100644 --- a/crates/theme/src/styles/colors.rs +++ b/crates/theme/src/styles/colors.rs @@ -1,11 +1,13 @@ #![allow(missing_docs)] -use gpui::{Hsla, WindowBackgroundAppearance}; +use gpui::{Hsla, SharedString, WindowBackgroundAppearance, WindowContext}; use refineable::Refineable; use std::sync::Arc; +use strum::{AsRefStr, EnumIter, IntoEnumIterator}; use crate::{ - AccentColors, PlayerColors, StatusColors, StatusColorsRefinement, SyntaxTheme, SystemColors, + AccentColors, ActiveTheme, PlayerColors, StatusColors, StatusColorsRefinement, SyntaxTheme, + SystemColors, }; #[derive(Refineable, Clone, Debug, PartialEq)] @@ -249,6 +251,237 @@ pub struct ThemeColors { pub link_text_hover: Hsla, } +#[derive(EnumIter, Debug, Clone, Copy, AsRefStr)] +#[strum(serialize_all = "snake_case")] +pub enum ThemeColorField { + Border, + BorderVariant, + BorderFocused, + BorderSelected, + BorderTransparent, + BorderDisabled, + ElevatedSurfaceBackground, + SurfaceBackground, + Background, + ElementBackground, + ElementHover, + ElementActive, + ElementSelected, + ElementDisabled, + DropTargetBackground, + GhostElementBackground, + GhostElementHover, + GhostElementActive, + GhostElementSelected, + GhostElementDisabled, + Text, + TextMuted, + TextPlaceholder, + TextDisabled, + TextAccent, + Icon, + IconMuted, + IconDisabled, + IconPlaceholder, + IconAccent, + StatusBarBackground, + TitleBarBackground, + TitleBarInactiveBackground, + ToolbarBackground, + TabBarBackground, + TabInactiveBackground, + TabActiveBackground, + SearchMatchBackground, + PanelBackground, + PanelFocusedBorder, + PanelIndentGuide, + PanelIndentGuideHover, + PanelIndentGuideActive, + PaneFocusedBorder, + PaneGroupBorder, + ScrollbarThumbBackground, + ScrollbarThumbHoverBackground, + ScrollbarThumbBorder, + ScrollbarTrackBackground, + ScrollbarTrackBorder, + EditorForeground, + EditorBackground, + EditorGutterBackground, + EditorSubheaderBackground, + EditorActiveLineBackground, + EditorHighlightedLineBackground, + EditorLineNumber, + EditorActiveLineNumber, + EditorInvisible, + EditorWrapGuide, + EditorActiveWrapGuide, + EditorIndentGuide, + EditorIndentGuideActive, + EditorDocumentHighlightReadBackground, + EditorDocumentHighlightWriteBackground, + EditorDocumentHighlightBracketBackground, + TerminalBackground, + TerminalForeground, + TerminalBrightForeground, + TerminalDimForeground, + TerminalAnsiBackground, + TerminalAnsiBlack, + TerminalAnsiBrightBlack, + TerminalAnsiDimBlack, + TerminalAnsiRed, + TerminalAnsiBrightRed, + TerminalAnsiDimRed, + TerminalAnsiGreen, + TerminalAnsiBrightGreen, + TerminalAnsiDimGreen, + TerminalAnsiYellow, + TerminalAnsiBrightYellow, + TerminalAnsiDimYellow, + TerminalAnsiBlue, + TerminalAnsiBrightBlue, + TerminalAnsiDimBlue, + TerminalAnsiMagenta, + TerminalAnsiBrightMagenta, + TerminalAnsiDimMagenta, + TerminalAnsiCyan, + TerminalAnsiBrightCyan, + TerminalAnsiDimCyan, + TerminalAnsiWhite, + TerminalAnsiBrightWhite, + TerminalAnsiDimWhite, + LinkTextHover, +} + +impl ThemeColors { + pub fn color(&self, field: ThemeColorField) -> Hsla { + match field { + ThemeColorField::Border => self.border, + ThemeColorField::BorderVariant => self.border_variant, + ThemeColorField::BorderFocused => self.border_focused, + ThemeColorField::BorderSelected => self.border_selected, + ThemeColorField::BorderTransparent => self.border_transparent, + ThemeColorField::BorderDisabled => self.border_disabled, + ThemeColorField::ElevatedSurfaceBackground => self.elevated_surface_background, + ThemeColorField::SurfaceBackground => self.surface_background, + ThemeColorField::Background => self.background, + ThemeColorField::ElementBackground => self.element_background, + ThemeColorField::ElementHover => self.element_hover, + ThemeColorField::ElementActive => self.element_active, + ThemeColorField::ElementSelected => self.element_selected, + ThemeColorField::ElementDisabled => self.element_disabled, + ThemeColorField::DropTargetBackground => self.drop_target_background, + ThemeColorField::GhostElementBackground => self.ghost_element_background, + ThemeColorField::GhostElementHover => self.ghost_element_hover, + ThemeColorField::GhostElementActive => self.ghost_element_active, + ThemeColorField::GhostElementSelected => self.ghost_element_selected, + ThemeColorField::GhostElementDisabled => self.ghost_element_disabled, + ThemeColorField::Text => self.text, + ThemeColorField::TextMuted => self.text_muted, + ThemeColorField::TextPlaceholder => self.text_placeholder, + ThemeColorField::TextDisabled => self.text_disabled, + ThemeColorField::TextAccent => self.text_accent, + ThemeColorField::Icon => self.icon, + ThemeColorField::IconMuted => self.icon_muted, + ThemeColorField::IconDisabled => self.icon_disabled, + ThemeColorField::IconPlaceholder => self.icon_placeholder, + ThemeColorField::IconAccent => self.icon_accent, + ThemeColorField::StatusBarBackground => self.status_bar_background, + ThemeColorField::TitleBarBackground => self.title_bar_background, + ThemeColorField::TitleBarInactiveBackground => self.title_bar_inactive_background, + ThemeColorField::ToolbarBackground => self.toolbar_background, + ThemeColorField::TabBarBackground => self.tab_bar_background, + ThemeColorField::TabInactiveBackground => self.tab_inactive_background, + ThemeColorField::TabActiveBackground => self.tab_active_background, + ThemeColorField::SearchMatchBackground => self.search_match_background, + ThemeColorField::PanelBackground => self.panel_background, + ThemeColorField::PanelFocusedBorder => self.panel_focused_border, + ThemeColorField::PanelIndentGuide => self.panel_indent_guide, + ThemeColorField::PanelIndentGuideHover => self.panel_indent_guide_hover, + ThemeColorField::PanelIndentGuideActive => self.panel_indent_guide_active, + ThemeColorField::PaneFocusedBorder => self.pane_focused_border, + ThemeColorField::PaneGroupBorder => self.pane_group_border, + ThemeColorField::ScrollbarThumbBackground => self.scrollbar_thumb_background, + ThemeColorField::ScrollbarThumbHoverBackground => self.scrollbar_thumb_hover_background, + ThemeColorField::ScrollbarThumbBorder => self.scrollbar_thumb_border, + ThemeColorField::ScrollbarTrackBackground => self.scrollbar_track_background, + ThemeColorField::ScrollbarTrackBorder => self.scrollbar_track_border, + ThemeColorField::EditorForeground => self.editor_foreground, + ThemeColorField::EditorBackground => self.editor_background, + ThemeColorField::EditorGutterBackground => self.editor_gutter_background, + ThemeColorField::EditorSubheaderBackground => self.editor_subheader_background, + ThemeColorField::EditorActiveLineBackground => self.editor_active_line_background, + ThemeColorField::EditorHighlightedLineBackground => { + self.editor_highlighted_line_background + } + ThemeColorField::EditorLineNumber => self.editor_line_number, + ThemeColorField::EditorActiveLineNumber => self.editor_active_line_number, + ThemeColorField::EditorInvisible => self.editor_invisible, + ThemeColorField::EditorWrapGuide => self.editor_wrap_guide, + ThemeColorField::EditorActiveWrapGuide => self.editor_active_wrap_guide, + ThemeColorField::EditorIndentGuide => self.editor_indent_guide, + ThemeColorField::EditorIndentGuideActive => self.editor_indent_guide_active, + ThemeColorField::EditorDocumentHighlightReadBackground => { + self.editor_document_highlight_read_background + } + ThemeColorField::EditorDocumentHighlightWriteBackground => { + self.editor_document_highlight_write_background + } + ThemeColorField::EditorDocumentHighlightBracketBackground => { + self.editor_document_highlight_bracket_background + } + ThemeColorField::TerminalBackground => self.terminal_background, + ThemeColorField::TerminalForeground => self.terminal_foreground, + ThemeColorField::TerminalBrightForeground => self.terminal_bright_foreground, + ThemeColorField::TerminalDimForeground => self.terminal_dim_foreground, + ThemeColorField::TerminalAnsiBackground => self.terminal_ansi_background, + ThemeColorField::TerminalAnsiBlack => self.terminal_ansi_black, + ThemeColorField::TerminalAnsiBrightBlack => self.terminal_ansi_bright_black, + ThemeColorField::TerminalAnsiDimBlack => self.terminal_ansi_dim_black, + ThemeColorField::TerminalAnsiRed => self.terminal_ansi_red, + ThemeColorField::TerminalAnsiBrightRed => self.terminal_ansi_bright_red, + ThemeColorField::TerminalAnsiDimRed => self.terminal_ansi_dim_red, + ThemeColorField::TerminalAnsiGreen => self.terminal_ansi_green, + ThemeColorField::TerminalAnsiBrightGreen => self.terminal_ansi_bright_green, + ThemeColorField::TerminalAnsiDimGreen => self.terminal_ansi_dim_green, + ThemeColorField::TerminalAnsiYellow => self.terminal_ansi_yellow, + ThemeColorField::TerminalAnsiBrightYellow => self.terminal_ansi_bright_yellow, + ThemeColorField::TerminalAnsiDimYellow => self.terminal_ansi_dim_yellow, + ThemeColorField::TerminalAnsiBlue => self.terminal_ansi_blue, + ThemeColorField::TerminalAnsiBrightBlue => self.terminal_ansi_bright_blue, + ThemeColorField::TerminalAnsiDimBlue => self.terminal_ansi_dim_blue, + ThemeColorField::TerminalAnsiMagenta => self.terminal_ansi_magenta, + ThemeColorField::TerminalAnsiBrightMagenta => self.terminal_ansi_bright_magenta, + ThemeColorField::TerminalAnsiDimMagenta => self.terminal_ansi_dim_magenta, + ThemeColorField::TerminalAnsiCyan => self.terminal_ansi_cyan, + ThemeColorField::TerminalAnsiBrightCyan => self.terminal_ansi_bright_cyan, + ThemeColorField::TerminalAnsiDimCyan => self.terminal_ansi_dim_cyan, + ThemeColorField::TerminalAnsiWhite => self.terminal_ansi_white, + ThemeColorField::TerminalAnsiBrightWhite => self.terminal_ansi_bright_white, + ThemeColorField::TerminalAnsiDimWhite => self.terminal_ansi_dim_white, + ThemeColorField::LinkTextHover => self.link_text_hover, + } + } + + pub fn iter(&self) -> impl Iterator + '_ { + ThemeColorField::iter().map(move |field| (field, self.color(field))) + } + + pub fn to_vec(&self) -> Vec<(ThemeColorField, Hsla)> { + self.iter().collect() + } +} + +pub fn all_theme_colors(cx: &WindowContext) -> Vec<(Hsla, SharedString)> { + let theme = cx.theme(); + ThemeColorField::iter() + .map(|field| { + let color = theme.colors().color(field); + let name = field.as_ref().to_string(); + (color, SharedString::from(name)) + }) + .collect() +} + #[derive(Refineable, Clone, PartialEq)] pub struct ThemeStyles { /// The background appearance of the window. diff --git a/crates/ui/src/styles/elevation.rs b/crates/ui/src/styles/elevation.rs index 722111b46c..932fd3a944 100644 --- a/crates/ui/src/styles/elevation.rs +++ b/crates/ui/src/styles/elevation.rs @@ -1,5 +1,8 @@ -use gpui::{hsla, point, px, BoxShadow}; +use std::fmt::{self, Display, Formatter}; + +use gpui::{hsla, point, px, BoxShadow, Hsla, WindowContext}; use smallvec::{smallvec, SmallVec}; +use theme::ActiveTheme; /// Today, elevation is primarily used to add shadows to elements, and set the correct background for elements like buttons. /// @@ -15,6 +18,8 @@ pub enum ElevationIndex { Background, /// The primary surface – Contains panels, panes, containers, etc. Surface, + /// The same elevation as the primary surface, but used for the editable areas, like buffers + EditorSurface, /// 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. @@ -25,11 +30,26 @@ pub enum ElevationIndex { DraggedElement, } +impl Display for ElevationIndex { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + ElevationIndex::Background => write!(f, "Background"), + ElevationIndex::Surface => write!(f, "Surface"), + ElevationIndex::EditorSurface => write!(f, "Editor Surface"), + ElevationIndex::ElevatedSurface => write!(f, "Elevated Surface"), + ElevationIndex::Wash => write!(f, "Wash"), + ElevationIndex::ModalSurface => write!(f, "Modal Surface"), + ElevationIndex::DraggedElement => write!(f, "Dragged Element"), + } + } +} + impl ElevationIndex { /// Returns an appropriate shadow for the given elevation index. pub fn shadow(self) -> SmallVec<[BoxShadow; 2]> { match self { ElevationIndex::Surface => smallvec![], + ElevationIndex::EditorSurface => smallvec![], ElevationIndex::ElevatedSurface => smallvec![BoxShadow { color: hsla(0., 0., 0., 0.12), @@ -62,4 +82,17 @@ impl ElevationIndex { _ => smallvec![], } } + + /// Returns the background color for the given elevation index. + pub fn bg(&self, cx: &WindowContext) -> Hsla { + match self { + ElevationIndex::Background => cx.theme().colors().background, + ElevationIndex::Surface => cx.theme().colors().surface_background, + ElevationIndex::EditorSurface => cx.theme().colors().editor_background, + ElevationIndex::ElevatedSurface => cx.theme().colors().elevated_surface_background, + ElevationIndex::Wash => gpui::transparent_black(), + ElevationIndex::ModalSurface => cx.theme().colors().elevated_surface_background, + ElevationIndex::DraggedElement => gpui::transparent_black(), + } + } } diff --git a/crates/ui/src/utils.rs b/crates/ui/src/utils.rs index b68d6e6bbd..25477194dc 100644 --- a/crates/ui/src/utils.rs +++ b/crates/ui/src/utils.rs @@ -1,7 +1,9 @@ //! UI-related utilities +mod color_contrast; mod format_distance; mod with_rem_size; +pub use color_contrast::*; pub use format_distance::*; pub use with_rem_size::*; diff --git a/crates/ui/src/utils/color_contrast.rs b/crates/ui/src/utils/color_contrast.rs new file mode 100644 index 0000000000..2a6b4bf281 --- /dev/null +++ b/crates/ui/src/utils/color_contrast.rs @@ -0,0 +1,70 @@ +use gpui::{Hsla, Rgba}; + +/// Calculates the contrast ratio between two colors according to WCAG 2.0 standards. +/// +/// The formula used is: +/// (L1 + 0.05) / (L2 + 0.05), where L1 is the lighter of the two luminances and L2 is the darker. +/// +/// Returns a float representing the contrast ratio. A higher value indicates more contrast. +/// The range of the returned value is 1 to 21 (commonly written as 1:1 to 21:1). +pub fn calculate_contrast_ratio(fg: Hsla, bg: Hsla) -> f32 { + let l1 = relative_luminance(fg); + let l2 = relative_luminance(bg); + + let (lighter, darker) = if l1 > l2 { (l1, l2) } else { (l2, l1) }; + + (lighter + 0.05) / (darker + 0.05) +} + +/// Calculates the relative luminance of a color. +/// +/// The relative luminance is the relative brightness of any point in a colorspace, +/// normalized to 0 for darkest black and 1 for lightest white. +fn relative_luminance(color: Hsla) -> f32 { + let rgba: Rgba = color.into(); + let r = linearize(rgba.r); + let g = linearize(rgba.g); + let b = linearize(rgba.b); + + 0.2126 * r + 0.7152 * g + 0.0722 * b +} + +/// Linearizes an RGB component. +fn linearize(component: f32) -> f32 { + if component <= 0.03928 { + component / 12.92 + } else { + ((component + 0.055) / 1.055).powf(2.4) + } +} + +#[cfg(test)] +mod tests { + use gpui::hsla; + + use super::*; + + // Test the contrast ratio formula with some common color combinations to + // prevent regressions in either the color conversions or the formula itself. + #[test] + fn test_contrast_ratio_formula() { + // White on Black (should be close to 21:1) + let white = hsla(0.0, 0.0, 1.0, 1.0); + let black = hsla(0.0, 0.0, 0.0, 1.0); + assert!((calculate_contrast_ratio(white, black) - 21.0).abs() < 0.1); + + // Black on White (should be close to 21:1) + assert!((calculate_contrast_ratio(black, white) - 21.0).abs() < 0.1); + + // Mid-gray on Black (should be close to 5.32:1) + let mid_gray = hsla(0.0, 0.0, 0.5, 1.0); + assert!((calculate_contrast_ratio(mid_gray, black) - 5.32).abs() < 0.1); + + // White on Mid-gray (should be close to 3.95:1) + assert!((calculate_contrast_ratio(white, mid_gray) - 3.95).abs() < 0.1); + + // Same color (should be 1:1) + let red = hsla(0.0, 1.0, 0.5, 1.0); + assert!((calculate_contrast_ratio(red, red) - 1.0).abs() < 0.01); + } +} diff --git a/crates/workspace/src/theme_preview.rs b/crates/workspace/src/theme_preview.rs new file mode 100644 index 0000000000..620b66d702 --- /dev/null +++ b/crates/workspace/src/theme_preview.rs @@ -0,0 +1,454 @@ +#![allow(unused, dead_code)] +use gpui::{actions, AppContext, EventEmitter, FocusHandle, FocusableView, Hsla}; +use theme::all_theme_colors; +use ui::{ + prelude::*, utils::calculate_contrast_ratio, AudioStatus, Availability, Avatar, + AvatarAudioStatusIndicator, AvatarAvailabilityIndicator, ButtonLike, ElevationIndex, Facepile, + TintColor, Tooltip, +}; + +use crate::{Item, Workspace}; + +actions!(debug, [OpenThemePreview]); + +pub fn init(cx: &mut AppContext) { + cx.observe_new_views(|workspace: &mut Workspace, _| { + workspace.register_action(|workspace, _: &OpenThemePreview, cx| { + let theme_preview = cx.new_view(ThemePreview::new); + workspace.add_item_to_active_pane(Box::new(theme_preview), None, true, cx) + }); + }) + .detach(); +} + +struct ThemePreview { + focus_handle: FocusHandle, +} + +impl ThemePreview { + pub fn new(cx: &mut ViewContext) -> Self { + Self { + focus_handle: cx.focus_handle(), + } + } +} + +impl EventEmitter<()> for ThemePreview {} + +impl FocusableView for ThemePreview { + fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle { + self.focus_handle.clone() + } +} +impl ThemePreview {} + +impl Item for ThemePreview { + type Event = (); + + fn to_item_events(_: &Self::Event, _: impl FnMut(crate::item::ItemEvent)) {} + + fn tab_content_text(&self, cx: &WindowContext) -> Option { + let name = cx.theme().name.clone(); + Some(format!("{} Preview", name).into()) + } + + fn telemetry_event_text(&self) -> Option<&'static str> { + None + } + + fn clone_on_split( + &self, + _workspace_id: Option, + cx: &mut ViewContext, + ) -> Option> + where + Self: Sized, + { + Some(cx.new_view(Self::new)) + } +} + +const AVATAR_URL: &str = "https://avatars.githubusercontent.com/u/1714999?v=4"; + +impl ThemePreview { + fn preview_bg(cx: &WindowContext) -> Hsla { + cx.theme().colors().editor_background + } + + fn render_avatars(&self, cx: &ViewContext) -> impl IntoElement { + v_flex() + .gap_1() + .child( + Headline::new("Avatars") + .size(HeadlineSize::Small) + .color(Color::Muted), + ) + .child( + h_flex() + .items_start() + .gap_4() + .child(Avatar::new(AVATAR_URL).size(px(24.))) + .child(Avatar::new(AVATAR_URL).size(px(24.)).grayscale(true)) + .child( + Avatar::new(AVATAR_URL) + .size(px(24.)) + .indicator(AvatarAudioStatusIndicator::new(AudioStatus::Muted)), + ) + .child( + Avatar::new(AVATAR_URL) + .size(px(24.)) + .indicator(AvatarAudioStatusIndicator::new(AudioStatus::Deafened)), + ) + .child( + Avatar::new(AVATAR_URL) + .size(px(24.)) + .indicator(AvatarAvailabilityIndicator::new(Availability::Free)), + ) + .child( + Avatar::new(AVATAR_URL) + .size(px(24.)) + .indicator(AvatarAvailabilityIndicator::new(Availability::Free)), + ) + .child( + Facepile::empty() + .child( + Avatar::new(AVATAR_URL) + .border_color(Self::preview_bg(cx)) + .size(px(22.)) + .into_any_element(), + ) + .child( + Avatar::new(AVATAR_URL) + .border_color(Self::preview_bg(cx)) + .size(px(22.)) + .into_any_element(), + ) + .child( + Avatar::new(AVATAR_URL) + .border_color(Self::preview_bg(cx)) + .size(px(22.)) + .into_any_element(), + ) + .child( + Avatar::new(AVATAR_URL) + .border_color(Self::preview_bg(cx)) + .size(px(22.)) + .into_any_element(), + ), + ), + ) + } + + fn render_buttons(&self, layer: ElevationIndex, cx: &ViewContext) -> impl IntoElement { + v_flex() + .gap_1() + .child( + Headline::new("Buttons") + .size(HeadlineSize::Small) + .color(Color::Muted), + ) + .child( + h_flex() + .items_start() + .gap_px() + .child( + IconButton::new("icon_button_transparent", IconName::Check) + .style(ButtonStyle::Transparent), + ) + .child( + IconButton::new("icon_button_subtle", IconName::Check) + .style(ButtonStyle::Subtle), + ) + .child( + IconButton::new("icon_button_filled", IconName::Check) + .style(ButtonStyle::Filled), + ) + .child( + IconButton::new("icon_button_selected_accent", IconName::Check) + .selected_style(ButtonStyle::Tinted(TintColor::Accent)) + .selected(true), + ) + .child(IconButton::new("icon_button_selected", IconName::Check).selected(true)) + .child( + IconButton::new("icon_button_positive", IconName::Check) + .style(ButtonStyle::Tinted(TintColor::Positive)), + ) + .child( + IconButton::new("icon_button_warning", IconName::Check) + .style(ButtonStyle::Tinted(TintColor::Warning)), + ) + .child( + IconButton::new("icon_button_negative", IconName::Check) + .style(ButtonStyle::Tinted(TintColor::Negative)), + ), + ) + .child( + h_flex() + .gap_px() + .child( + Button::new("button_transparent", "Transparent") + .style(ButtonStyle::Transparent), + ) + .child(Button::new("button_subtle", "Subtle").style(ButtonStyle::Subtle)) + .child(Button::new("button_filled", "Filled").style(ButtonStyle::Filled)) + .child( + Button::new("button_selected", "Selected") + .selected_style(ButtonStyle::Tinted(TintColor::Accent)) + .selected(true), + ) + .child( + Button::new("button_selected_tinted", "Selected (Tinted)") + .selected_style(ButtonStyle::Tinted(TintColor::Accent)) + .selected(true), + ) + .child( + Button::new("button_positive", "Tint::Positive") + .style(ButtonStyle::Tinted(TintColor::Positive)), + ) + .child( + Button::new("button_warning", "Tint::Warning") + .style(ButtonStyle::Tinted(TintColor::Warning)), + ) + .child( + Button::new("button_negative", "Tint::Negative") + .style(ButtonStyle::Tinted(TintColor::Negative)), + ), + ) + } + + fn render_text(&self, layer: ElevationIndex, cx: &ViewContext) -> impl IntoElement { + let bg = layer.bg(cx); + + let label_with_contrast = |label: &str, fg: Hsla| { + let contrast = calculate_contrast_ratio(fg, bg); + format!("{} ({:.2})", label, contrast) + }; + + v_flex() + .gap_1() + .child(Headline::new("Text").size(HeadlineSize::Small).color(Color::Muted)) + .child( + h_flex() + .items_start() + .gap_4() + .child( + v_flex() + .gap_1() + .child(Headline::new("Headline Sizes").size(HeadlineSize::Small).color(Color::Muted)) + .child(Headline::new("XLarge Headline").size(HeadlineSize::XLarge)) + .child(Headline::new("Large Headline").size(HeadlineSize::Large)) + .child(Headline::new("Medium Headline").size(HeadlineSize::Medium)) + .child(Headline::new("Small Headline").size(HeadlineSize::Small)) + .child(Headline::new("XSmall Headline").size(HeadlineSize::XSmall)), + ) + .child( + v_flex() + .gap_1() + .child(Headline::new("Text Colors").size(HeadlineSize::Small).color(Color::Muted)) + .child( + Label::new(label_with_contrast( + "Default Text", + Color::Default.color(cx), + )) + .color(Color::Default), + ) + .child( + Label::new(label_with_contrast( + "Accent Text", + Color::Accent.color(cx), + )) + .color(Color::Accent), + ) + .child( + Label::new(label_with_contrast( + "Conflict Text", + Color::Conflict.color(cx), + )) + .color(Color::Conflict), + ) + .child( + Label::new(label_with_contrast( + "Created Text", + Color::Created.color(cx), + )) + .color(Color::Created), + ) + .child( + Label::new(label_with_contrast( + "Deleted Text", + Color::Deleted.color(cx), + )) + .color(Color::Deleted), + ) + .child( + Label::new(label_with_contrast( + "Disabled Text", + Color::Disabled.color(cx), + )) + .color(Color::Disabled), + ) + .child( + Label::new(label_with_contrast( + "Error Text", + Color::Error.color(cx), + )) + .color(Color::Error), + ) + .child( + Label::new(label_with_contrast( + "Hidden Text", + Color::Hidden.color(cx), + )) + .color(Color::Hidden), + ) + .child( + Label::new(label_with_contrast( + "Hint Text", + Color::Hint.color(cx), + )) + .color(Color::Hint), + ) + .child( + Label::new(label_with_contrast( + "Ignored Text", + Color::Ignored.color(cx), + )) + .color(Color::Ignored), + ) + .child( + Label::new(label_with_contrast( + "Info Text", + Color::Info.color(cx), + )) + .color(Color::Info), + ) + .child( + Label::new(label_with_contrast( + "Modified Text", + Color::Modified.color(cx), + )) + .color(Color::Modified), + ) + .child( + Label::new(label_with_contrast( + "Muted Text", + Color::Muted.color(cx), + )) + .color(Color::Muted), + ) + .child( + Label::new(label_with_contrast( + "Placeholder Text", + Color::Placeholder.color(cx), + )) + .color(Color::Placeholder), + ) + .child( + Label::new(label_with_contrast( + "Selected Text", + Color::Selected.color(cx), + )) + .color(Color::Selected), + ) + .child( + Label::new(label_with_contrast( + "Success Text", + Color::Success.color(cx), + )) + .color(Color::Success), + ) + .child( + Label::new(label_with_contrast( + "Warning Text", + Color::Warning.color(cx), + )) + .color(Color::Warning), + ) + ) + .child( + v_flex() + .gap_1() + .child(Headline::new("Wrapping Text").size(HeadlineSize::Small).color(Color::Muted)) + .child( + div().max_w(px(200.)).child( + "This is a longer piece of text that should wrap to multiple lines. It demonstrates how text behaves when it exceeds the width of its container." + )) + ) + ) + } + + fn render_colors(&self, layer: ElevationIndex, cx: &ViewContext) -> impl IntoElement { + let bg = layer.bg(cx); + let all_colors = all_theme_colors(cx); + + v_flex() + .gap_1() + .child( + Headline::new("Colors") + .size(HeadlineSize::Small) + .color(Color::Muted), + ) + .child( + h_flex() + .flex_wrap() + .gap_1() + .children(all_colors.into_iter().map(|(color, name)| { + let id = ElementId::Name(format!("{:?}-preview", color).into()); + let name = name.clone(); + div().size_8().flex_none().child( + ButtonLike::new(id) + .child( + div() + .size_8() + .bg(color) + .border_1() + .border_color(cx.theme().colors().border) + .overflow_hidden(), + ) + .size(ButtonSize::None) + .style(ButtonStyle::Transparent) + .tooltip(move |cx| { + let name = name.clone(); + Tooltip::with_meta(name, None, format!("{:?}", color), cx) + }), + ) + })), + ) + } + + fn render_theme_layer( + &self, + layer: ElevationIndex, + cx: &ViewContext, + ) -> impl IntoElement { + v_flex() + .p_4() + .bg(layer.bg(cx)) + .text_color(cx.theme().colors().text) + .gap_2() + .child(Headline::new(layer.clone().to_string()).size(HeadlineSize::Medium)) + .child(self.render_avatars(cx)) + .child(self.render_buttons(layer, cx)) + .child(self.render_text(layer, cx)) + .child(self.render_colors(layer, cx)) + } +} + +impl Render for ThemePreview { + fn render(&mut self, cx: &mut ViewContext) -> impl ui::IntoElement { + v_flex() + .id("theme-preview") + .key_context("ThemePreview") + .overflow_scroll() + .size_full() + .max_h_full() + .p_4() + .track_focus(&self.focus_handle) + .bg(Self::preview_bg(cx)) + .gap_4() + .child(self.render_theme_layer(ElevationIndex::Background, cx)) + .child(self.render_theme_layer(ElevationIndex::Surface, cx)) + .child(self.render_theme_layer(ElevationIndex::EditorSurface, cx)) + .child(self.render_theme_layer(ElevationIndex::ElevatedSurface, cx)) + } +} diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 24c681083b..9715381684 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -9,6 +9,7 @@ pub mod searchable; pub mod shared_screen; mod status_bar; pub mod tasks; +mod theme_preview; mod toolbar; mod workspace_settings; @@ -323,6 +324,7 @@ pub fn init_settings(cx: &mut AppContext) { pub fn init(app_state: Arc, cx: &mut AppContext) { init_settings(cx); notifications::init(cx); + theme_preview::init(cx); cx.on_action(Workspace::close_global); cx.on_action(reload); From a0988508f0c525d1db48a1a82f1866095a0ba43f Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 31 Oct 2024 16:10:03 -0600 Subject: [PATCH 16/45] SSHHELL escaping.... (#20046) Closes #20027 Closes #19976 (again) Release Notes: - Remoting: Fixed remotes with non-sh/bash/zsh default shells - Remoting: Fixed remotes running busybox's version of gunzip --- Cargo.lock | 1 + crates/remote/Cargo.toml | 1 + crates/remote/src/ssh_session.rs | 171 +++++++++++++++++++------------ 3 files changed, 105 insertions(+), 68 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bea5b56126..ad47e4326d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9535,6 +9535,7 @@ dependencies = [ "fs", "futures 0.3.30", "gpui", + "itertools 0.13.0", "log", "parking_lot", "prost", diff --git a/crates/remote/Cargo.toml b/crates/remote/Cargo.toml index 086e718c35..06feee996c 100644 --- a/crates/remote/Cargo.toml +++ b/crates/remote/Cargo.toml @@ -24,6 +24,7 @@ collections.workspace = true fs.workspace = true futures.workspace = true gpui.workspace = true +itertools.workspace = true log.workspace = true parking_lot.workspace = true prost.workspace = true diff --git a/crates/remote/src/ssh_session.rs b/crates/remote/src/ssh_session.rs index 1e708e4b0a..9cae283749 100644 --- a/crates/remote/src/ssh_session.rs +++ b/crates/remote/src/ssh_session.rs @@ -20,6 +20,7 @@ use gpui::{ AppContext, AsyncAppContext, BorrowAppContext, Context, EventEmitter, Global, Model, ModelContext, SemanticVersion, Task, WeakModel, }; +use itertools::Itertools; use parking_lot::Mutex; use release_channel::{AppCommitSha, AppVersion, ReleaseChannel}; use rpc::{ @@ -34,8 +35,7 @@ use smol::{ use std::{ any::TypeId, collections::VecDeque, - ffi::OsStr, - fmt, + fmt, iter, ops::ControlFlow, path::{Path, PathBuf}, sync::{ @@ -70,6 +70,18 @@ pub struct SshConnectionOptions { pub upload_binary_over_ssh: bool, } +#[macro_export] +macro_rules! shell_script { + ($fmt:expr, $($name:ident = $arg:expr),+ $(,)?) => {{ + format!( + $fmt, + $( + $name = shlex::try_quote($arg).unwrap() + ),+ + ) + }}; +} + impl SshConnectionOptions { pub fn parse_command_line(input: &str) -> Result { let input = input.trim_start_matches("ssh "); @@ -281,14 +293,26 @@ pub trait SshClientDelegate: Send + Sync { } impl SshSocket { - fn ssh_command>(&self, program: S) -> process::Command { + // :WARNING: ssh unquotes arguments when executing on the remote :WARNING: + // e.g. $ ssh host sh -c 'ls -l' is equivalent to $ ssh host sh -c ls -l + // and passes -l as an argument to sh, not to ls. + // You need to do it like this: $ ssh host "sh -c 'ls -l /tmp'" + fn ssh_command(&self, program: &str, args: &[&str]) -> process::Command { let mut command = process::Command::new("ssh"); + let to_run = iter::once(&program) + .chain(args.iter()) + .map(|token| shlex::try_quote(token).unwrap()) + .join(" "); self.ssh_options(&mut command) .arg(self.connection_options.ssh_url()) - .arg(program); + .arg(to_run); command } + fn shell_script(&self, script: impl AsRef) -> process::Command { + return self.ssh_command("sh", &["-c", script.as_ref()]); + } + fn ssh_options<'a>(&self, command: &'a mut process::Command) -> &'a mut process::Command { command .stdin(Stdio::piped()) @@ -309,7 +333,7 @@ impl SshSocket { } } -async fn run_cmd(command: &mut process::Command) -> Result { +async fn run_cmd(mut command: process::Command) -> Result { let output = command.output().await?; if output.status.success() { Ok(String::from_utf8_lossy(&output.stdout).to_string()) @@ -1236,7 +1260,7 @@ impl RemoteConnection for SshRemoteConnection { } let socket = self.socket.clone(); - run_cmd(socket.ssh_command(&remote_binary_path).arg("version")).await?; + run_cmd(socket.ssh_command(&remote_binary_path.to_string_lossy(), &["version"])).await?; Ok(remote_binary_path) } @@ -1253,22 +1277,33 @@ impl RemoteConnection for SshRemoteConnection { ) -> Task> { delegate.set_status(Some("Starting proxy"), cx); - let mut start_proxy_command = format!( - "RUST_LOG={} {} {:?} proxy --identifier {}", - std::env::var("RUST_LOG").unwrap_or_default(), - std::env::var("RUST_BACKTRACE") - .map(|b| { format!("RUST_BACKTRACE={}", b) }) - .unwrap_or_default(), - remote_binary_path, - unique_identifier, + let mut start_proxy_command = shell_script!( + "exec {binary_path} proxy --identifier {identifier}", + binary_path = &remote_binary_path.to_string_lossy(), + identifier = &unique_identifier, ); + + if let Some(rust_log) = std::env::var("RUST_LOG").ok() { + start_proxy_command = format!( + "RUST_LOG={} {}", + shlex::try_quote(&rust_log).unwrap(), + start_proxy_command + ) + } + if let Some(rust_backtrace) = std::env::var("RUST_BACKTRACE").ok() { + start_proxy_command = format!( + "RUST_BACKTRACE={} {}", + shlex::try_quote(&rust_backtrace).unwrap(), + start_proxy_command + ) + } if reconnect { start_proxy_command.push_str(" --reconnect"); } let ssh_proxy_process = match self .socket - .ssh_command(start_proxy_command) + .shell_script(start_proxy_command) // IMPORTANT: we kill this process when we drop the task that uses it. .kill_on_drop(true) .spawn() @@ -1450,8 +1485,8 @@ impl SshRemoteConnection { socket_path, }; - let os = run_cmd(socket.ssh_command("uname").arg("-s")).await?; - let arch = run_cmd(socket.ssh_command("uname").arg("-m")).await?; + let os = run_cmd(socket.ssh_command("uname", &["-s"])).await?; + let arch = run_cmd(socket.ssh_command("uname", &["-m"])).await?; let os = match os.trim() { "Darwin" => "macos", @@ -1649,14 +1684,9 @@ impl SshRemoteConnection { } async fn get_ssh_source_port(&self) -> Result { - let output = run_cmd( - self.socket - .ssh_command("sh") - .arg("-c") - .arg(r#""echo $SSH_CLIENT | cut -d' ' -f2""#), - ) - .await - .context("failed to get source port from SSH_CLIENT on host")?; + let output = run_cmd(self.socket.shell_script("echo $SSH_CLIENT | cut -d' ' -f2")) + .await + .context("failed to get source port from SSH_CLIENT on host")?; Ok(output.trim().to_string()) } @@ -1667,13 +1697,13 @@ impl SshRemoteConnection { .ok_or_else(|| anyhow!("Lock file path has no parent directory"))?; let script = format!( - r#"'mkdir -p "{parent_dir}" && [ ! -f "{lock_file}" ] && echo "{content}" > "{lock_file}" && echo "created" || echo "exists"'"#, + r#"mkdir -p "{parent_dir}" && [ ! -f "{lock_file}" ] && echo "{content}" > "{lock_file}" && echo "created" || echo "exists""#, parent_dir = parent_dir.display(), lock_file = lock_file.display(), content = content, ); - let output = run_cmd(self.socket.ssh_command("sh").arg("-c").arg(&script)) + let output = run_cmd(self.socket.shell_script(&script)) .await .with_context(|| format!("failed to create a lock file at {:?}", lock_file))?; @@ -1681,7 +1711,7 @@ impl SshRemoteConnection { } fn generate_stale_check_script(lock_file: &Path, max_age: u64) -> String { - format!( + shell_script!( r#" if [ ! -f "{lock_file}" ]; then echo "lock file does not exist" @@ -1709,18 +1739,15 @@ impl SshRemoteConnection { else echo "recent" fi"#, - lock_file = lock_file.display(), - max_age = max_age + lock_file = &lock_file.to_string_lossy(), + max_age = &max_age.to_string() ) } async fn is_lock_stale(&self, lock_file: &Path, max_age: &Duration) -> Result { - let script = format!( - "'{}'", - Self::generate_stale_check_script(lock_file, max_age.as_secs()) - ); + let script = Self::generate_stale_check_script(lock_file, max_age.as_secs()); - let output = run_cmd(self.socket.ssh_command("sh").arg("-c").arg(&script)) + let output = run_cmd(self.socket.shell_script(script)) .await .with_context(|| { format!("failed to check whether lock file {:?} is stale", lock_file) @@ -1733,9 +1760,12 @@ impl SshRemoteConnection { } async fn remove_lock_file(&self, lock_file: &Path) -> Result<()> { - run_cmd(self.socket.ssh_command("rm").arg("-f").arg(lock_file)) - .await - .context("failed to remove lock file")?; + run_cmd( + self.socket + .ssh_command("rm", &["-f", &lock_file.to_string_lossy()]), + ) + .await + .context("failed to remove lock file")?; Ok(()) } @@ -1746,7 +1776,11 @@ impl SshRemoteConnection { platform: SshPlatform, cx: &mut AsyncAppContext, ) -> Result<()> { - let current_version = match run_cmd(self.socket.ssh_command(dst_path).arg("version")).await + let current_version = match run_cmd( + self.socket + .ssh_command(&dst_path.to_string_lossy(), &["version"]), + ) + .await { Ok(version_output) => { if let Ok(version) = version_output.trim().parse::() { @@ -1866,26 +1900,25 @@ impl SshRemoteConnection { } async fn is_binary_in_use(&self, binary_path: &Path) -> Result { - let script = format!( - r#"' + let script = shell_script!( + r#" if command -v lsof >/dev/null 2>&1; then - if lsof "{}" >/dev/null 2>&1; then + if lsof "{binary_path}" >/dev/null 2>&1; then echo "in_use" exit 0 fi elif command -v fuser >/dev/null 2>&1; then - if fuser "{}" >/dev/null 2>&1; then + if fuser "{binary_path}" >/dev/null 2>&1; then echo "in_use" exit 0 fi fi echo "not_in_use" - '"#, - binary_path.display(), - binary_path.display(), + "#, + binary_path = &binary_path.to_string_lossy(), ); - let output = run_cmd(self.socket.ssh_command("sh").arg("-c").arg(script)) + let output = run_cmd(self.socket.shell_script(script)) .await .context("failed to check if binary is in use")?; @@ -1904,30 +1937,32 @@ impl SshRemoteConnection { dst_path_gz.set_extension("gz"); if let Some(parent) = dst_path.parent() { - run_cmd(self.socket.ssh_command("mkdir").arg("-p").arg(parent)).await?; + run_cmd( + self.socket + .ssh_command("mkdir", &["-p", &parent.to_string_lossy()]), + ) + .await?; } delegate.set_status(Some("Downloading remote development server on host"), cx); - let body = shlex::try_quote(body).unwrap(); - let url = shlex::try_quote(url).unwrap(); - let dst_str = dst_path_gz.to_string_lossy(); - let dst_escaped = shlex::try_quote(&dst_str).unwrap(); - - let script = format!( + let script = shell_script!( r#" if command -v curl >/dev/null 2>&1; then - curl -f -L -X GET -H "Content-Type: application/json" -d {body} {url} -o {dst_escaped} && echo "curl" + curl -f -L -X GET -H "Content-Type: application/json" -d {body} {url} -o {dst_path} && echo "curl" elif command -v wget >/dev/null 2>&1; then - wget --max-redirect=5 --method=GET --header="Content-Type: application/json" --body-data={body} {url} -O {dst_escaped} && echo "wget" + wget --max-redirect=5 --method=GET --header="Content-Type: application/json" --body-data={body} {url} -O {dst_path} && echo "wget" else echo "Neither curl nor wget is available" >&2 exit 1 fi - "# + "#, + body = body, + url = url, + dst_path = &dst_path_gz.to_string_lossy(), ); - let output = run_cmd(self.socket.ssh_command("sh").arg("-c").arg(script)) + let output = run_cmd(self.socket.shell_script(script)) .await .context("Failed to download server binary")?; @@ -1950,7 +1985,11 @@ impl SshRemoteConnection { dst_path_gz.set_extension("gz"); if let Some(parent) = dst_path.parent() { - run_cmd(self.socket.ssh_command("mkdir").arg("-p").arg(parent)).await?; + run_cmd( + self.socket + .ssh_command("mkdir", &["-p", &parent.to_string_lossy()]), + ) + .await?; } let src_stat = fs::metadata(&src_path).await?; @@ -1978,20 +2017,16 @@ impl SshRemoteConnection { delegate.set_status(Some("Extracting remote development server"), cx); run_cmd( self.socket - .ssh_command("gunzip") - .arg("--force") - .arg(&dst_path_gz), + .ssh_command("gunzip", &["-f", &dst_path_gz.to_string_lossy()]), ) .await?; let server_mode = 0o755; delegate.set_status(Some("Marking remote development server executable"), cx); - run_cmd( - self.socket - .ssh_command("chmod") - .arg(format!("{:o}", server_mode)) - .arg(dst_path), - ) + run_cmd(self.socket.ssh_command( + "chmod", + &[&format!("{:o}", server_mode), &dst_path.to_string_lossy()], + )) .await?; Ok(()) From b87c4a1e13480e5372ced9a6d114b2e4d998874f Mon Sep 17 00:00:00 2001 From: Boris Cherny Date: Thu, 31 Oct 2024 16:21:26 -0700 Subject: [PATCH 17/45] assistant: Add health telemetry (#19928) This PR adds a bit of telemetry for Anthropic models, in order to understand model health. With this logging, we can monitor and diagnose dips in performance, for example due to model rollouts. Release Notes: - N/A --------- Co-authored-by: Max Brunsfeld --- Cargo.lock | 1 + crates/assistant/src/context.rs | 27 ++- crates/assistant/src/inline_assistant.rs | 192 +++++++++--------- .../src/terminal_inline_assistant.rs | 112 +++++++--- crates/client/src/telemetry.rs | 7 + crates/language_model/Cargo.toml | 1 + crates/language_model/src/language_model.rs | 50 ++++- crates/language_model/src/logging.rs | 90 ++++++++ .../language_model/src/provider/anthropic.rs | 14 +- .../telemetry_events/src/telemetry_events.rs | 4 + 10 files changed, 354 insertions(+), 144 deletions(-) create mode 100644 crates/language_model/src/logging.rs diff --git a/Cargo.lock b/Cargo.lock index ad47e4326d..e510a07bee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6309,6 +6309,7 @@ dependencies = [ "settings", "smol", "strum 0.25.0", + "telemetry_events", "text", "theme", "thiserror", diff --git a/crates/assistant/src/context.rs b/crates/assistant/src/context.rs index a1de9d3b40..5b4cff01b6 100644 --- a/crates/assistant/src/context.rs +++ b/crates/assistant/src/context.rs @@ -24,6 +24,7 @@ use gpui::{ use language::{AnchorRangeExt, Bias, Buffer, LanguageRegistry, OffsetRangeExt, Point, ToOffset}; use language_model::{ + logging::report_assistant_event, provider::cloud::{MaxMonthlySpendReachedError, PaymentRequiredError}, LanguageModel, LanguageModelCacheConfiguration, LanguageModelCompletionEvent, LanguageModelImage, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, @@ -1955,6 +1956,7 @@ impl Context { }); match event { + LanguageModelCompletionEvent::StartMessage { .. } => {} LanguageModelCompletionEvent::Stop(reason) => { stop_reason = reason; } @@ -2060,23 +2062,28 @@ impl Context { None }; - 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 { + let language_name = this + .buffer + .read(cx) + .language() + .map(|language| language.name()); + report_assistant_event( + AssistantEvent { conversation_id: Some(this.id.0.clone()), kind: AssistantKind::Panel, phase: AssistantPhase::Response, + message_id: None, model: model.telemetry_id(), model_provider: model.provider_id().to_string(), response_latency, error_message, language_name: language_name.map(|name| name.to_proto()), - }); - } + }, + this.telemetry.clone(), + cx.http_client(), + model.api_key(cx), + cx.background_executor(), + ); if let Ok(stop_reason) = result { match stop_reason { @@ -2543,7 +2550,7 @@ impl Context { let mut messages = stream.await?; let mut replaced = !replace_old; - while let Some(message) = messages.next().await { + while let Some(message) = messages.stream.next().await { let text = message?; let mut lines = text.lines(); this.update(&mut cx, |this, cx| { diff --git a/crates/assistant/src/inline_assistant.rs b/crates/assistant/src/inline_assistant.rs index fdf00c8b04..934c2dd5d3 100644 --- a/crates/assistant/src/inline_assistant.rs +++ b/crates/assistant/src/inline_assistant.rs @@ -21,9 +21,7 @@ use fs::Fs; use futures::{ channel::mpsc, future::{BoxFuture, LocalBoxFuture}, - join, - stream::{self, BoxStream}, - SinkExt, Stream, StreamExt, + join, SinkExt, Stream, StreamExt, }; use gpui::{ anchored, deferred, point, AnyElement, AppContext, ClickEvent, EventEmitter, FocusHandle, @@ -32,7 +30,8 @@ use gpui::{ }; use language::{Buffer, IndentKind, Point, Selection, TransactionId}; use language_model::{ - LanguageModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role, + logging::report_assistant_event, LanguageModel, LanguageModelRegistry, LanguageModelRequest, + LanguageModelRequestMessage, LanguageModelTextStream, Role, }; use multi_buffer::MultiBufferRow; use parking_lot::Mutex; @@ -241,12 +240,13 @@ impl InlineAssistant { }; codegen_ranges.push(start..end); - if let Some(telemetry) = self.telemetry.as_ref() { - if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() { + if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() { + if let Some(telemetry) = self.telemetry.as_ref() { telemetry.report_assistant_event(AssistantEvent { conversation_id: None, kind: AssistantKind::Inline, phase: AssistantPhase::Invoked, + message_id: None, model: model.telemetry_id(), model_provider: model.provider_id().to_string(), response_latency: None, @@ -754,33 +754,6 @@ impl InlineAssistant { pub fn finish_assist(&mut self, assist_id: InlineAssistId, undo: bool, cx: &mut WindowContext) { 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: language_name.map(|name| name.to_proto()), - }); - } - } - 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) { @@ -815,12 +788,45 @@ impl InlineAssistant { } } + let active_alternative = assist.codegen.read(cx).active_alternative().clone(); + let message_id = active_alternative.read(cx).message_id.clone(); + + 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()) + }); + report_assistant_event( + AssistantEvent { + conversation_id: None, + kind: AssistantKind::Inline, + message_id, + 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: language_name.map(|name| name.to_proto()), + }, + self.telemetry.clone(), + cx.http_client(), + model.api_key(cx), + cx.background_executor(), + ); + } + if undo { assist.codegen.update(cx, |codegen, cx| codegen.undo(cx)); } else { - let confirmed_alternative = assist.codegen.read(cx).active_alternative().clone(); - self.confirmed_assists - .insert(assist_id, confirmed_alternative); + self.confirmed_assists.insert(assist_id, active_alternative); } } } @@ -2497,6 +2503,7 @@ pub struct CodegenAlternative { line_operations: Vec, request: Option, elapsed_time: Option, + message_id: Option, } enum CodegenStatus { @@ -2555,6 +2562,7 @@ impl CodegenAlternative { buffer: buffer.clone(), old_buffer, edit_position: None, + message_id: None, snapshot, last_equal_ranges: Default::default(), transformation_transaction_id: None, @@ -2659,20 +2667,20 @@ impl CodegenAlternative { self.edit_position = Some(self.range.start.bias_right(&self.snapshot)); + let api_key = model.api_key(cx); let telemetry_id = model.telemetry_id(); let provider_id = model.provider_id(); - let chunks: LocalBoxFuture>>> = + let stream: LocalBoxFuture> = if user_prompt.trim().to_lowercase() == "delete" { - async { Ok(stream::empty().boxed()) }.boxed_local() + async { Ok(LanguageModelTextStream::default()) }.boxed_local() } else { let request = self.build_request(user_prompt, assistant_panel_context, cx)?; self.request = Some(request.clone()); - let chunks = cx - .spawn(|_, cx| async move { model.stream_completion_text(request, &cx).await }); - async move { Ok(chunks.await?.boxed()) }.boxed_local() + cx.spawn(|_, cx| async move { model.stream_completion_text(request, &cx).await }) + .boxed_local() }; - self.handle_stream(telemetry_id, provider_id.to_string(), chunks, cx); + self.handle_stream(telemetry_id, provider_id.to_string(), api_key, stream, cx); Ok(()) } @@ -2737,7 +2745,8 @@ impl CodegenAlternative { &mut self, model_telemetry_id: String, model_provider_id: String, - stream: impl 'static + Future>>>, + model_api_key: Option, + stream: impl 'static + Future>, cx: &mut ModelContext, ) { let start_time = Instant::now(); @@ -2767,6 +2776,7 @@ impl CodegenAlternative { } } + let http_client = cx.http_client().clone(); let telemetry = self.telemetry.clone(); let language_name = { let multibuffer = self.buffer.read(cx); @@ -2782,15 +2792,21 @@ impl CodegenAlternative { let mut edit_start = self.range.start.to_offset(&snapshot); self.generation = cx.spawn(|codegen, mut cx| { async move { - let chunks = stream.await; + let stream = stream.await; + let message_id = stream + .as_ref() + .ok() + .and_then(|stream| stream.message_id.clone()); let generate = async { let (mut diff_tx, mut diff_rx) = mpsc::channel(1); + let executor = cx.background_executor().clone(); + let message_id = message_id.clone(); let line_based_stream_diff: Task> = cx.background_executor().spawn(async move { let mut response_latency = None; let request_start = Instant::now(); let diff = async { - let chunks = StripInvalidSpans::new(chunks?); + let chunks = StripInvalidSpans::new(stream?.stream); futures::pin_mut!(chunks); let mut diff = StreamingDiff::new(selected_text.to_string()); let mut line_diff = LineDiff::default(); @@ -2886,9 +2902,10 @@ impl CodegenAlternative { let error_message = result.as_ref().err().map(|error| error.to_string()); - if let Some(telemetry) = telemetry { - telemetry.report_assistant_event(AssistantEvent { + report_assistant_event( + AssistantEvent { conversation_id: None, + message_id, kind: AssistantKind::Inline, phase: AssistantPhase::Response, model: model_telemetry_id, @@ -2896,8 +2913,12 @@ impl CodegenAlternative { response_latency, error_message, language_name: language_name.map(|name| name.to_proto()), - }); - } + }, + telemetry, + http_client, + model_api_key, + &executor, + ); result?; Ok(()) @@ -2961,6 +2982,7 @@ impl CodegenAlternative { codegen .update(&mut cx, |this, cx| { + this.message_id = message_id; this.last_equal_ranges.clear(); if let Err(error) = result { this.status = CodegenStatus::Error(error); @@ -3512,15 +3534,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, - ) - }); + let chunks_tx = simulate_response_stream(codegen.clone(), cx); let mut new_text = concat!( " let mut x = 0;\n", @@ -3584,15 +3598,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, - ) - }); + let chunks_tx = simulate_response_stream(codegen.clone(), cx); cx.background_executor.run_until_parked(); @@ -3659,15 +3665,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, - ) - }); + let chunks_tx = simulate_response_stream(codegen.clone(), cx); cx.background_executor.run_until_parked(); @@ -3733,16 +3731,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, - ) - }); - + let chunks_tx = simulate_response_stream(codegen.clone(), cx); let new_text = concat!( "func main() {\n", "\tx := 0\n", @@ -3797,16 +3786,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, - ) - }); - + let chunks_tx = simulate_response_stream(codegen.clone(), cx); chunks_tx .unbounded_send("let mut x = 0;\nx += 1;".to_string()) .unwrap(); @@ -3880,6 +3860,26 @@ mod tests { } } + fn simulate_response_stream( + codegen: Model, + cx: &mut TestAppContext, + ) -> mpsc::UnboundedSender { + let (chunks_tx, chunks_rx) = mpsc::unbounded(); + codegen.update(cx, |codegen, cx| { + codegen.handle_stream( + String::new(), + String::new(), + None, + future::ready(Ok(LanguageModelTextStream { + message_id: None, + stream: chunks_rx.map(Ok).boxed(), + })), + cx, + ); + }); + chunks_tx + } + fn rust_lang() -> Language { Language::new( LanguageConfig { diff --git a/crates/assistant/src/terminal_inline_assistant.rs b/crates/assistant/src/terminal_inline_assistant.rs index 3e472ae4a9..2fb4b4ffda 100644 --- a/crates/assistant/src/terminal_inline_assistant.rs +++ b/crates/assistant/src/terminal_inline_assistant.rs @@ -17,7 +17,8 @@ use gpui::{ }; use language::Buffer; use language_model::{ - LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role, + logging::report_assistant_event, LanguageModelRegistry, LanguageModelRequest, + LanguageModelRequestMessage, Role, }; use settings::Settings; use std::{ @@ -306,6 +307,33 @@ impl TerminalInlineAssistant { this.focus_handle(cx).focus(cx); }) .log_err(); + + if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() { + let codegen = assist.codegen.read(cx); + let executor = cx.background_executor().clone(); + report_assistant_event( + AssistantEvent { + conversation_id: None, + kind: AssistantKind::InlineTerminal, + message_id: codegen.message_id.clone(), + 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: None, + }, + codegen.telemetry.clone(), + cx.http_client(), + model.api_key(cx), + &executor, + ); + } + assist.codegen.update(cx, |codegen, cx| { if undo { codegen.undo(cx); @@ -1016,6 +1044,7 @@ pub struct Codegen { telemetry: Option>, terminal: Model, generation: Task<()>, + message_id: Option, transaction: Option, } @@ -1026,6 +1055,7 @@ impl Codegen { telemetry, status: CodegenStatus::Idle, generation: Task::ready(()), + message_id: None, transaction: None, } } @@ -1035,6 +1065,8 @@ impl Codegen { return; }; + let model_api_key = model.api_key(cx); + let http_client = cx.http_client(); let telemetry = self.telemetry.clone(); self.status = CodegenStatus::Pending; self.transaction = Some(TerminalTransaction::start(self.terminal.clone())); @@ -1043,44 +1075,62 @@ impl Codegen { let model_provider_id = model.provider_id(); let response = model.stream_completion_text(prompt, &cx).await; let generate = async { + let message_id = response + .as_ref() + .ok() + .and_then(|response| response.message_id.clone()); + let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1); - let task = cx.background_executor().spawn(async move { - let mut response_latency = None; - let request_start = Instant::now(); - let task = async { - let mut chunks = response?; - while let Some(chunk) = chunks.next().await { - if response_latency.is_none() { - response_latency = Some(request_start.elapsed()); + let task = cx.background_executor().spawn({ + let message_id = message_id.clone(); + let executor = cx.background_executor().clone(); + async move { + let mut response_latency = None; + let request_start = Instant::now(); + let task = async { + let mut chunks = response?.stream; + while let Some(chunk) = chunks.next().await { + if response_latency.is_none() { + response_latency = Some(request_start.elapsed()); + } + let chunk = chunk?; + hunks_tx.send(chunk).await?; } - let chunk = chunk?; - hunks_tx.send(chunk).await?; - } + anyhow::Ok(()) + }; + + let result = task.await; + + let error_message = result.as_ref().err().map(|error| error.to_string()); + report_assistant_event( + AssistantEvent { + conversation_id: None, + kind: AssistantKind::InlineTerminal, + message_id, + phase: AssistantPhase::Response, + model: model_telemetry_id, + model_provider: model_provider_id.to_string(), + response_latency, + error_message, + language_name: None, + }, + telemetry, + http_client, + model_api_key, + &executor, + ); + + result?; anyhow::Ok(()) - }; - - let result = task.await; - - let error_message = result.as_ref().err().map(|error| error.to_string()); - if let Some(telemetry) = telemetry { - telemetry.report_assistant_event(AssistantEvent { - conversation_id: None, - kind: AssistantKind::Inline, - phase: AssistantPhase::Response, - model: model_telemetry_id, - model_provider: model_provider_id.to_string(), - response_latency, - error_message, - language_name: None, - }); } - - result?; - anyhow::Ok(()) }); + this.update(&mut cx, |this, _| { + this.message_id = message_id; + })?; + while let Some(hunk) = hunks_rx.next().await { this.update(&mut cx, |this, cx| { if let Some(transaction) = &mut this.transaction { diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index ba03255d54..25f8709ff1 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -341,6 +341,13 @@ impl Telemetry { .detach(); } + pub fn metrics_enabled(self: &Arc) -> bool { + let state = self.state.lock(); + let enabled = state.settings.metrics; + drop(state); + return enabled; + } + pub fn set_authenticated_user_info( self: &Arc, metrics_id: Option, diff --git a/crates/language_model/Cargo.toml b/crates/language_model/Cargo.toml index 685b022340..e88675bbae 100644 --- a/crates/language_model/Cargo.toml +++ b/crates/language_model/Cargo.toml @@ -46,6 +46,7 @@ serde_json.workspace = true settings.workspace = true smol.workspace = true strum.workspace = true +telemetry_events.workspace = true theme.workspace = true thiserror.workspace = true tiktoken-rs.workspace = true diff --git a/crates/language_model/src/language_model.rs b/crates/language_model/src/language_model.rs index 81d0c874dc..a2f5a072a9 100644 --- a/crates/language_model/src/language_model.rs +++ b/crates/language_model/src/language_model.rs @@ -1,3 +1,4 @@ +pub mod logging; mod model; pub mod provider; mod rate_limiter; @@ -59,6 +60,7 @@ pub enum LanguageModelCompletionEvent { Stop(StopReason), Text(String), ToolUse(LanguageModelToolUse), + StartMessage { message_id: String }, } #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] @@ -76,6 +78,20 @@ pub struct LanguageModelToolUse { pub input: serde_json::Value, } +pub struct LanguageModelTextStream { + pub message_id: Option, + pub stream: BoxStream<'static, Result>, +} + +impl Default for LanguageModelTextStream { + fn default() -> Self { + Self { + message_id: None, + stream: Box::pin(futures::stream::empty()), + } + } +} + pub trait LanguageModel: Send + Sync { fn id(&self) -> LanguageModelId; fn name(&self) -> LanguageModelName; @@ -87,6 +103,10 @@ pub trait LanguageModel: Send + Sync { fn provider_name(&self) -> LanguageModelProviderName; fn telemetry_id(&self) -> String; + fn api_key(&self, _cx: &AppContext) -> Option { + None + } + /// Returns the availability of this language model. fn availability(&self) -> LanguageModelAvailability { LanguageModelAvailability::Public @@ -113,21 +133,39 @@ pub trait LanguageModel: Send + Sync { &self, request: LanguageModelRequest, cx: &AsyncAppContext, - ) -> BoxFuture<'static, Result>>> { + ) -> BoxFuture<'static, Result> { let events = self.stream_completion(request, cx); async move { - Ok(events - .await? - .filter_map(|result| async move { + let mut events = events.await?; + let mut message_id = None; + let mut first_item_text = None; + + if let Some(first_event) = events.next().await { + match first_event { + Ok(LanguageModelCompletionEvent::StartMessage { message_id: id }) => { + message_id = Some(id.clone()); + } + Ok(LanguageModelCompletionEvent::Text(text)) => { + first_item_text = Some(text); + } + _ => (), + } + } + + let stream = futures::stream::iter(first_item_text.map(Ok)) + .chain(events.filter_map(|result| async move { match result { + Ok(LanguageModelCompletionEvent::StartMessage { .. }) => None, Ok(LanguageModelCompletionEvent::Text(text)) => Some(Ok(text)), Ok(LanguageModelCompletionEvent::Stop(_)) => None, Ok(LanguageModelCompletionEvent::ToolUse(_)) => None, Err(err) => Some(Err(err)), } - }) - .boxed()) + })) + .boxed(); + + Ok(LanguageModelTextStream { message_id, stream }) } .boxed() } diff --git a/crates/language_model/src/logging.rs b/crates/language_model/src/logging.rs new file mode 100644 index 0000000000..d5156125c4 --- /dev/null +++ b/crates/language_model/src/logging.rs @@ -0,0 +1,90 @@ +use anthropic::{AnthropicError, ANTHROPIC_API_URL}; +use anyhow::{anyhow, Context, Result}; +use client::telemetry::Telemetry; +use gpui::BackgroundExecutor; +use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest}; +use std::env; +use std::sync::Arc; +use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase}; +use util::ResultExt; + +use crate::provider::anthropic::PROVIDER_ID as ANTHROPIC_PROVIDER_ID; + +pub fn report_assistant_event( + event: AssistantEvent, + telemetry: Option>, + client: Arc, + model_api_key: Option, + executor: &BackgroundExecutor, +) { + if let Some(telemetry) = telemetry.as_ref() { + telemetry.report_assistant_event(event.clone()); + if telemetry.metrics_enabled() && event.model_provider == ANTHROPIC_PROVIDER_ID { + executor + .spawn(async move { + report_anthropic_event(event, client, model_api_key) + .await + .log_err(); + }) + .detach(); + } + } +} + +async fn report_anthropic_event( + event: AssistantEvent, + client: Arc, + model_api_key: Option, +) -> Result<(), AnthropicError> { + let api_key = match model_api_key { + Some(key) => key, + None => { + return Err(AnthropicError::Other(anyhow!( + "Anthropic API key is not set" + ))); + } + }; + + let uri = format!("{ANTHROPIC_API_URL}/v1/log/zed"); + let request_builder = HttpRequest::builder() + .method(Method::POST) + .uri(uri) + .header("X-Api-Key", api_key) + .header("Content-Type", "application/json"); + let serialized_event: serde_json::Value = serde_json::json!({ + "completion_type": match event.kind { + AssistantKind::Inline => "natural_language_completion_in_editor", + AssistantKind::InlineTerminal => "natural_language_completion_in_terminal", + AssistantKind::Panel => "conversation_message", + }, + "event": match event.phase { + AssistantPhase::Response => "response", + AssistantPhase::Invoked => "invoke", + AssistantPhase::Accepted => "accept", + AssistantPhase::Rejected => "reject", + }, + "metadata": { + "language_name": event.language_name, + "message_id": event.message_id, + "platform": env::consts::OS, + } + }); + + let request = request_builder + .body(AsyncBody::from(serialized_event.to_string())) + .context("failed to construct request body")?; + + let response = client + .send(request) + .await + .context("failed to send request to Anthropic")?; + + if response.status().is_success() { + return Ok(()); + } + + return Err(AnthropicError::Other(anyhow!( + "Failed to log: {}", + response.status(), + ))); +} diff --git a/crates/language_model/src/provider/anthropic.rs b/crates/language_model/src/provider/anthropic.rs index b7e65650b5..c19526b92f 100644 --- a/crates/language_model/src/provider/anthropic.rs +++ b/crates/language_model/src/provider/anthropic.rs @@ -26,7 +26,7 @@ use theme::ThemeSettings; use ui::{prelude::*, Icon, IconName, Tooltip}; use util::{maybe, ResultExt}; -const PROVIDER_ID: &str = "anthropic"; +pub const PROVIDER_ID: &str = "anthropic"; const PROVIDER_NAME: &str = "Anthropic"; #[derive(Default, Clone, Debug, PartialEq)] @@ -356,6 +356,10 @@ impl LanguageModel for AnthropicModel { format!("anthropic/{}", self.model.id()) } + fn api_key(&self, cx: &AppContext) -> Option { + self.state.read(cx).api_key.clone() + } + fn max_token_count(&self) -> usize { self.model.max_token_count() } @@ -520,6 +524,14 @@ pub fn map_to_language_model_completion_events( )); } } + Event::MessageStart { message } => { + return Some(( + Some(Ok(LanguageModelCompletionEvent::StartMessage { + message_id: message.id, + })), + state, + )) + } Event::MessageDelta { delta, .. } => { if let Some(stop_reason) = delta.stop_reason.as_deref() { let stop_reason = match stop_reason { diff --git a/crates/telemetry_events/src/telemetry_events.rs b/crates/telemetry_events/src/telemetry_events.rs index 26db3cf8d8..43757f85d8 100644 --- a/crates/telemetry_events/src/telemetry_events.rs +++ b/crates/telemetry_events/src/telemetry_events.rs @@ -47,6 +47,7 @@ pub struct EventWrapper { pub enum AssistantKind { Panel, Inline, + InlineTerminal, } impl Display for AssistantKind { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -56,6 +57,7 @@ impl Display for AssistantKind { match self { Self::Panel => "panel", Self::Inline => "inline", + Self::InlineTerminal => "inline_terminal", } ) } @@ -140,6 +142,8 @@ pub struct CallEvent { pub struct AssistantEvent { /// Unique random identifier for each assistant tab (None for inline assist) pub conversation_id: Option, + /// Server-generated message ID (only supported for some providers) + pub message_id: Option, /// The kind of assistant (Panel, Inline) pub kind: AssistantKind, #[serde(default)] From 5b6578247f289085b9c73a90000563f2c0c93607 Mon Sep 17 00:00:00 2001 From: Kyle Kelley Date: Thu, 31 Oct 2024 17:58:36 -0700 Subject: [PATCH 18/45] Upgrade nbformat and runtimelib (#20050) Fixes an issue on load of notebooks that have `text/*` output in `Vec` rather than `String`. This ensures that Markdown output will render correctly. image Release Notes: - N/A --- Cargo.lock | 16 ++++++++-------- Cargo.toml | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e510a07bee..a14680f981 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5585,7 +5585,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2 0.4.10", + "socket2 0.5.7", "tokio", "tower-service", "tracing", @@ -6157,9 +6157,9 @@ dependencies = [ [[package]] name = "jupyter-serde" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a444fb3f87ee6885eb316028cc998c7d84811663ef95d78c419419423d5a054" +checksum = "77b96de099fc23d5c21e05de32cc087c8326983895b7f6c242562af01f7d4c81" dependencies = [ "anyhow", "chrono", @@ -6492,7 +6492,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" dependencies = [ "cfg-if", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] @@ -7157,9 +7157,9 @@ dependencies = [ [[package]] name = "nbformat" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146074ad45cab20f5d98ccded164826158471f21d04f96e40b9872529e10979d" +checksum = "84f8a9ab08b34237c2c1d0504b794c2ff01c08dfc46a060d160f004a7f479c31" dependencies = [ "anyhow", "chrono", @@ -9970,9 +9970,9 @@ dependencies = [ [[package]] name = "runtimelib" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "263588fe9593333c4bfde258c9021fc64e766ea434e070c6b67c7100536d6499" +checksum = "bc7fe3c17675445fe89de68d130be00b7115104924fbcf53a9b0a84b0283fc81" dependencies = [ "anyhow", "async-dispatcher", diff --git a/Cargo.toml b/Cargo.toml index fda3254cc5..18dc85994a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -370,7 +370,7 @@ linkify = "0.10.0" log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] } markup5ever_rcdom = "0.3.0" nanoid = "0.4" -nbformat = "0.3.1" +nbformat = "0.3.2" nix = "0.29" num-format = "0.4.4" once_cell = "1.19.0" @@ -403,7 +403,7 @@ reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "fd110f "stream", ] } rsa = "0.9.6" -runtimelib = { version = "0.16.0", default-features = false, features = [ +runtimelib = { version = "0.16.1", default-features = false, features = [ "async-dispatcher-runtime", ] } rustc-demangle = "0.1.23" From 155854d9a9b6789830140c886689eae3bd9e543c Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 31 Oct 2024 20:45:11 -0600 Subject: [PATCH 19/45] Fix trigger release? (#20053) Release Notes: - N/A --- .github/workflows/bump_patch_version.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/bump_patch_version.yml b/.github/workflows/bump_patch_version.yml index e3468274a1..8bbefe64e8 100644 --- a/.github/workflows/bump_patch_version.yml +++ b/.github/workflows/bump_patch_version.yml @@ -43,6 +43,8 @@ jobs: esac which cargo-set-version > /dev/null || cargo install cargo-edit output=$(cargo set-version -p zed --bump patch 2>&1 | sed 's/.* //') + export GIT_COMMITTER_NAME="Zed Bot" + export GIT_COMMITTER_EMAIL="hi@zed.dev" git commit -am "Bump to $output for @$GITHUB_ACTOR" --author "Zed Bot " git tag v${output}${tag_suffix} git push origin HEAD v${output}${tag_suffix} From f8ab86f9302b3544c093e3a843a9506da0ccd512 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 31 Oct 2024 22:24:24 -0600 Subject: [PATCH 20/45] Simplify line normalization (#19712) Release Notes: - Added \u2028 and \u2029 to invisible characters. Previously these were treated as \n. --- crates/text/src/text.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index 380ced5253..1d214fe0e1 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -43,9 +43,8 @@ use undo_map::UndoMap; #[cfg(any(test, feature = "test-support"))] use util::RandomCharIter; -static LINE_SEPARATORS_REGEX: LazyLock = LazyLock::new(|| { - Regex::new(r"\r\n|\r|\u{2028}|\u{2029}").expect("Failed to create LINE_SEPARATORS_REGEX") -}); +static LINE_SEPARATORS_REGEX: LazyLock = + LazyLock::new(|| Regex::new(r"\r\n|\r").expect("Failed to create LINE_SEPARATORS_REGEX")); pub type TransactionId = clock::Lamport; From 75f1862268b960f01c3abf95ee454c203a0aeb3d Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 31 Oct 2024 23:25:42 -0600 Subject: [PATCH 21/45] vim: Add (half of) ctrl-v/ctrl-q (#19585) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Release Notes: - vim: Add `ctrl-v`/`ctrl-q` to type any unicode code point. For example `ctrl-v escape` inserts an escape character(U+001B), or `ctrl-v u 1 0 E 2` types ტ (U+10E2). As in vim `ctrl-v ctrl-j` inserts U+0000 not U+000A. Zed does not yet implement insertion of the vim-specific representation of the typed keystroke for other keystrokes. - vim: Add `ctrl-shift-v` as an alias for paste on Linux --- assets/keymaps/vim.json | 55 ++++- crates/vim/src/digraph.rs | 200 +++++++++++++++++- crates/vim/src/mode_indicator.rs | 6 +- crates/vim/src/state.rs | 15 ++ crates/vim/src/vim.rs | 26 ++- crates/vim/test_data/test_ctrl_v.json | 24 +++ crates/vim/test_data/test_ctrl_v_control.json | 11 + crates/vim/test_data/test_ctrl_v_escape.json | 10 + 8 files changed, 337 insertions(+), 10 deletions(-) create mode 100644 crates/vim/test_data/test_ctrl_v.json create mode 100644 crates/vim/test_data/test_ctrl_v_control.json create mode 100644 crates/vim/test_data/test_ctrl_v_escape.json diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 8b2a728df3..24ea1defb0 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -339,6 +339,10 @@ "ctrl-t": "vim::Indent", "ctrl-d": "vim::Outdent", "ctrl-k": ["vim::PushOperator", { "Digraph": {} }], + "ctrl-v": ["vim::PushOperator", { "Literal": {} }], + "ctrl-shift-v": "editor::Paste", // note: this is *very* similar to ctrl-v in vim, but ctrl-shift-v on linux is the typical shortcut for paste when ctrl-v is already in use. + "ctrl-q": ["vim::PushOperator", { "Literal": {} }], + "ctrl-shift-q": ["vim::PushOperator", { "Literal": {} }], "ctrl-r": ["vim::PushOperator", "Register"], "insert": "vim::ToggleReplace" } @@ -357,6 +361,10 @@ "ctrl-c": "vim::NormalBefore", "ctrl-[": "vim::NormalBefore", "ctrl-k": ["vim::PushOperator", { "Digraph": {} }], + "ctrl-v": ["vim::PushOperator", { "Literal": {} }], + "ctrl-shift-v": "editor::Paste", // note: this is *very* similar to ctrl-v in vim, but ctrl-shift-v on linux is the typical shortcut for paste when ctrl-v is already in use. + "ctrl-q": ["vim::PushOperator", { "Literal": {} }], + "ctrl-shift-q": ["vim::PushOperator", { "Literal": {} }], "backspace": "vim::UndoReplace", "tab": "vim::Tab", "enter": "vim::Enter", @@ -371,7 +379,9 @@ "escape": "vim::ClearOperators", "ctrl-c": "vim::ClearOperators", "ctrl-[": "vim::ClearOperators", - "ctrl-k": ["vim::PushOperator", { "Digraph": {} }] + "ctrl-k": ["vim::PushOperator", { "Digraph": {} }], + "ctrl-v": ["vim::PushOperator", { "Literal": {} }], + "ctrl-q": ["vim::PushOperator", { "Literal": {} }] } }, { @@ -485,6 +495,49 @@ "c": "vim::CurrentLine" } }, + { + "context": "vim_mode == literal", + "bindings": { + "ctrl-@": ["vim::Literal", ["ctrl-@", "\u0000"]], + "ctrl-a": ["vim::Literal", ["ctrl-a", "\u0001"]], + "ctrl-b": ["vim::Literal", ["ctrl-b", "\u0002"]], + "ctrl-c": ["vim::Literal", ["ctrl-c", "\u0003"]], + "ctrl-d": ["vim::Literal", ["ctrl-d", "\u0004"]], + "ctrl-e": ["vim::Literal", ["ctrl-e", "\u0005"]], + "ctrl-f": ["vim::Literal", ["ctrl-f", "\u0006"]], + "ctrl-g": ["vim::Literal", ["ctrl-g", "\u0007"]], + "ctrl-h": ["vim::Literal", ["ctrl-h", "\u0008"]], + "ctrl-i": ["vim::Literal", ["ctrl-i", "\u0009"]], + "ctrl-j": ["vim::Literal", ["ctrl-j", "\u000A"]], + "ctrl-k": ["vim::Literal", ["ctrl-k", "\u000B"]], + "ctrl-l": ["vim::Literal", ["ctrl-l", "\u000C"]], + "ctrl-m": ["vim::Literal", ["ctrl-m", "\u000D"]], + "ctrl-n": ["vim::Literal", ["ctrl-n", "\u000E"]], + "ctrl-o": ["vim::Literal", ["ctrl-o", "\u000F"]], + "ctrl-p": ["vim::Literal", ["ctrl-p", "\u0010"]], + "ctrl-q": ["vim::Literal", ["ctrl-q", "\u0011"]], + "ctrl-r": ["vim::Literal", ["ctrl-r", "\u0012"]], + "ctrl-s": ["vim::Literal", ["ctrl-s", "\u0013"]], + "ctrl-t": ["vim::Literal", ["ctrl-t", "\u0014"]], + "ctrl-u": ["vim::Literal", ["ctrl-u", "\u0015"]], + "ctrl-v": ["vim::Literal", ["ctrl-v", "\u0016"]], + "ctrl-w": ["vim::Literal", ["ctrl-w", "\u0017"]], + "ctrl-x": ["vim::Literal", ["ctrl-x", "\u0018"]], + "ctrl-y": ["vim::Literal", ["ctrl-y", "\u0019"]], + "ctrl-z": ["vim::Literal", ["ctrl-z", "\u001A"]], + "ctrl-[": ["vim::Literal", ["ctrl-[", "\u001B"]], + "ctrl-\\": ["vim::Literal", ["ctrl-\\", "\u001C"]], + "ctrl-]": ["vim::Literal", ["ctrl-]", "\u001D"]], + "ctrl-^": ["vim::Literal", ["ctrl-^", "\u001E"]], + "ctrl-_": ["vim::Literal", ["ctrl-_", "\u001F"]], + "escape": ["vim::Literal", ["escape", "\u001B"]], + "enter": ["vim::Literal", ["enter", "\u000D"]], + "tab": ["vim::Literal", ["tab", "\u0009"]], + // zed extensions: + "backspace": ["vim::Literal", ["backspace", "\u0008"]], + "delete": ["vim::Literal", ["delete", "\u007F"]] + } + }, { "context": "BufferSearchBar && !in_replace", "bindings": { diff --git a/crates/vim/src/digraph.rs b/crates/vim/src/digraph.rs index 443b7ff378..4c09dd3e33 100644 --- a/crates/vim/src/digraph.rs +++ b/crates/vim/src/digraph.rs @@ -1,15 +1,25 @@ use std::sync::Arc; use collections::HashMap; -use gpui::AppContext; +use editor::Editor; +use gpui::{impl_actions, AppContext, Keystroke, KeystrokeEvent}; +use serde::Deserialize; use settings::Settings; use std::sync::LazyLock; use ui::ViewContext; -use crate::{Vim, VimSettings}; +use crate::{state::Operator, Vim, VimSettings}; mod default; +#[derive(PartialEq, Clone, Deserialize)] +struct Literal(String, char); +impl_actions!(vim, [Literal]); + +pub(crate) fn register(editor: &mut Editor, cx: &mut ViewContext) { + Vim::action(editor, cx, Vim::literal) +} + static DEFAULT_DIGRAPHS_MAP: LazyLock>> = LazyLock::new(|| { let mut map = HashMap::default(); for &(a, b, c) in default::DEFAULT_DIGRAPHS { @@ -50,6 +60,153 @@ impl Vim { self.input_ignored(text, cx); } } + + fn literal(&mut self, action: &Literal, cx: &mut ViewContext) { + if let Some(Operator::Literal { prefix }) = self.active_operator() { + if let Some(prefix) = prefix { + if let Some(keystroke) = Keystroke::parse(&action.0).ok() { + cx.window_context().defer(|cx| { + cx.dispatch_keystroke(keystroke); + }); + } + return self.handle_literal_input(prefix, "", cx); + } + } + + self.insert_literal(Some(action.1), "", cx); + } + + pub fn handle_literal_keystroke( + &mut self, + keystroke_event: &KeystrokeEvent, + prefix: String, + cx: &mut ViewContext, + ) { + // handled by handle_literal_input + if keystroke_event.keystroke.ime_key.is_some() { + return; + }; + + if prefix.len() > 0 { + self.handle_literal_input(prefix, "", cx); + } else { + self.pop_operator(cx); + } + + // give another chance to handle the binding outside + // of waiting mode. + if keystroke_event.action.is_none() { + let keystroke = keystroke_event.keystroke.clone(); + cx.window_context().defer(|cx| { + cx.dispatch_keystroke(keystroke); + }); + } + return; + } + + pub fn handle_literal_input( + &mut self, + mut prefix: String, + text: &str, + cx: &mut ViewContext, + ) { + let first = prefix.chars().next(); + let next = text.chars().next().unwrap_or(' '); + match first { + Some('o' | 'O') => { + if next.is_digit(8) { + prefix.push(next); + if prefix.len() == 4 { + let ch: char = u8::from_str_radix(&prefix[1..], 8).unwrap_or(255).into(); + return self.insert_literal(Some(ch), "", cx); + } + } else { + let ch = if prefix.len() > 1 { + Some(u8::from_str_radix(&prefix[1..], 8).unwrap_or(255).into()) + } else { + None + }; + return self.insert_literal(ch, text, cx); + } + } + Some('x' | 'X' | 'u' | 'U') => { + let max_len = match first.unwrap() { + 'x' => 3, + 'X' => 3, + 'u' => 5, + 'U' => 9, + _ => unreachable!(), + }; + if next.is_ascii_hexdigit() { + prefix.push(next); + if prefix.len() == max_len { + let ch: char = u32::from_str_radix(&prefix[1..], 16) + .ok() + .and_then(|n| n.try_into().ok()) + .unwrap_or('\u{FFFD}'); + return self.insert_literal(Some(ch), "", cx); + } + } else { + let ch = if prefix.len() > 1 { + Some( + u32::from_str_radix(&prefix[1..], 16) + .ok() + .and_then(|n| n.try_into().ok()) + .unwrap_or('\u{FFFD}'), + ) + } else { + None + }; + return self.insert_literal(ch, text, cx); + } + } + Some('0'..='9') => { + if next.is_ascii_hexdigit() { + prefix.push(next); + if prefix.len() == 3 { + let ch: char = u8::from_str_radix(&prefix, 10).unwrap_or(255).into(); + return self.insert_literal(Some(ch), "", cx); + } + } else { + let ch: char = u8::from_str_radix(&prefix, 10).unwrap_or(255).into(); + return self.insert_literal(Some(ch), "", cx); + } + } + None if matches!(next, 'o' | 'O' | 'x' | 'X' | 'u' | 'U' | '0'..='9') => { + prefix.push(next) + } + _ => { + return self.insert_literal(None, text, cx); + } + }; + + self.pop_operator(cx); + self.push_operator( + Operator::Literal { + prefix: Some(prefix), + }, + cx, + ); + } + + fn insert_literal(&mut self, ch: Option, suffix: &str, cx: &mut ViewContext) { + self.pop_operator(cx); + let mut text = String::new(); + if let Some(c) = ch { + if c == '\n' { + text.push('\x00') + } else { + text.push(c) + } + } + text.push_str(suffix); + + if self.editor_input_enabled() { + self.update_editor(cx, |_, editor, cx| editor.insert(&text, cx)); + } else { + self.input_ignored(text.into(), cx); + } + } } #[cfg(test)] @@ -154,4 +311,43 @@ mod test { cx.simulate_shared_keystrokes("a ctrl-k s , escape").await; cx.shared_state().await.assert_eq("Helloˇş"); } + + #[gpui::test] + async fn test_ctrl_v(cx: &mut gpui::TestAppContext) { + let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state("ˇ").await; + cx.simulate_shared_keystrokes("i ctrl-v 0 0 0").await; + cx.shared_state().await.assert_eq("\x00ˇ"); + + cx.simulate_shared_keystrokes("ctrl-v j").await; + cx.shared_state().await.assert_eq("\x00jˇ"); + cx.simulate_shared_keystrokes("ctrl-v x 6 5").await; + cx.shared_state().await.assert_eq("\x00jeˇ"); + cx.simulate_shared_keystrokes("ctrl-v U 1 F 6 4 0 space") + .await; + cx.shared_state().await.assert_eq("\x00je🙀 ˇ"); + } + + #[gpui::test] + async fn test_ctrl_v_escape(cx: &mut gpui::TestAppContext) { + let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await; + cx.set_shared_state("ˇ").await; + cx.simulate_shared_keystrokes("i ctrl-v 9 escape").await; + cx.shared_state().await.assert_eq("ˇ\t"); + cx.simulate_shared_keystrokes("i ctrl-v escape").await; + cx.shared_state().await.assert_eq("\x1bˇ\t"); + } + + #[gpui::test] + async fn test_ctrl_v_control(cx: &mut gpui::TestAppContext) { + let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await; + cx.set_shared_state("ˇ").await; + cx.simulate_shared_keystrokes("i ctrl-v ctrl-d").await; + cx.shared_state().await.assert_eq("\x04ˇ"); + cx.simulate_shared_keystrokes("ctrl-v ctrl-j").await; + cx.shared_state().await.assert_eq("\x04\x00ˇ"); + cx.simulate_shared_keystrokes("ctrl-v tab").await; + cx.shared_state().await.assert_eq("\x04\x00\x09ˇ"); + } } diff --git a/crates/vim/src/mode_indicator.rs b/crates/vim/src/mode_indicator.rs index 214462bc8d..e5bb31944b 100644 --- a/crates/vim/src/mode_indicator.rs +++ b/crates/vim/src/mode_indicator.rs @@ -70,7 +70,11 @@ impl ModeIndicator { recording .chain(vim.pre_count.map(|count| format!("{}", count))) .chain(vim.selected_register.map(|reg| format!("\"{reg}"))) - .chain(vim.operator_stack.iter().map(|item| item.id().to_string())) + .chain( + vim.operator_stack + .iter() + .map(|item| item.status().to_string()), + ) .chain(vim.post_count.map(|count| format!("{}", count))) .collect::>() .join("") diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index f9dfcdd2c3..eb1abf1553 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -77,6 +77,7 @@ pub enum Operator { Uppercase, OppositeCase, Digraph { first_char: Option }, + Literal { prefix: Option }, Register, RecordRegister, ReplayRegister, @@ -444,6 +445,7 @@ impl Operator { Operator::Yank => "y", Operator::Replace => "r", Operator::Digraph { .. } => "^K", + Operator::Literal { .. } => "^V", Operator::FindForward { before: false } => "f", Operator::FindForward { before: true } => "t", Operator::FindBackward { after: false } => "F", @@ -467,6 +469,18 @@ impl Operator { } } + pub fn status(&self) -> String { + match self { + Operator::Digraph { + first_char: Some(first_char), + } => format!("^K{first_char}"), + Operator::Literal { + prefix: Some(prefix), + } => format!("^V{prefix}"), + _ => self.id().to_string(), + } + } + pub fn is_waiting(&self, mode: Mode) -> bool { match self { Operator::AddSurrounds { target } => target.is_some() || mode.is_visual(), @@ -479,6 +493,7 @@ impl Operator { | Operator::ReplayRegister | Operator::Replace | Operator::Digraph { .. } + | Operator::Literal { .. } | Operator::ChangeSurrounds { target: Some(_) } | Operator::DeleteSurrounds => true, Operator::Change diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 6ec708d8b8..e265b0201e 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -296,6 +296,7 @@ impl Vim { object::register(editor, cx); visual::register(editor, cx); change_list::register(editor, cx); + digraph::register(editor, cx); cx.defer(|vim, cx| { vim.focused(false, cx); @@ -359,9 +360,15 @@ impl Vim { } if let Some(operator) = self.active_operator() { - if !operator.is_waiting(self.mode) { - self.clear_operator(cx); - self.stop_recording_immediately(Box::new(ClearOperators), cx) + match operator { + Operator::Literal { prefix } => { + self.handle_literal_keystroke(keystroke_event, prefix.unwrap_or_default(), cx); + } + _ if !operator.is_waiting(self.mode) => { + self.clear_operator(cx); + self.stop_recording_immediately(Box::new(ClearOperators), cx) + } + _ => {} } } } @@ -602,14 +609,18 @@ impl Vim { if let Some(active_operator) = active_operator { if active_operator.is_waiting(self.mode) { - mode = "waiting".to_string(); + if matches!(active_operator, Operator::Literal { .. }) { + mode = "literal".to_string(); + } else { + mode = "waiting".to_string(); + } } else { - mode = "operator".to_string(); operator_id = active_operator.id(); + mode = "operator".to_string(); } } - if mode != "waiting" && mode != "insert" && mode != "replace" { + if mode == "normal" || mode == "visual" || mode == "operator" { context.add("VimControl"); } context.set("vim_mode", mode); @@ -998,6 +1009,9 @@ impl Vim { self.push_operator(Operator::Digraph { first_char }, cx); } } + Some(Operator::Literal { prefix }) => { + self.handle_literal_input(prefix.unwrap_or_default(), &text, cx) + } Some(Operator::AddSurrounds { target }) => match self.mode { Mode::Normal => { if let Some(target) = target { diff --git a/crates/vim/test_data/test_ctrl_v.json b/crates/vim/test_data/test_ctrl_v.json new file mode 100644 index 0000000000..dfc090ab18 --- /dev/null +++ b/crates/vim/test_data/test_ctrl_v.json @@ -0,0 +1,24 @@ +{"Put":{"state":"ˇ"}} +{"Key":"i"} +{"Key":"ctrl-v"} +{"Key":"0"} +{"Key":"0"} +{"Key":"0"} +{"Get":{"state":"\u0000ˇ","mode":"Insert"}} +{"Key":"ctrl-v"} +{"Key":"j"} +{"Get":{"state":"\u0000jˇ","mode":"Insert"}} +{"Key":"ctrl-v"} +{"Key":"x"} +{"Key":"6"} +{"Key":"5"} +{"Get":{"state":"\u0000jeˇ","mode":"Insert"}} +{"Key":"ctrl-v"} +{"Key":"U"} +{"Key":"1"} +{"Key":"F"} +{"Key":"6"} +{"Key":"4"} +{"Key":"0"} +{"Key":"space"} +{"Get":{"state":"\u0000je🙀 ˇ","mode":"Insert"}} diff --git a/crates/vim/test_data/test_ctrl_v_control.json b/crates/vim/test_data/test_ctrl_v_control.json new file mode 100644 index 0000000000..a5a55cf6d5 --- /dev/null +++ b/crates/vim/test_data/test_ctrl_v_control.json @@ -0,0 +1,11 @@ +{"Put":{"state":"ˇ"}} +{"Key":"i"} +{"Key":"ctrl-v"} +{"Key":"ctrl-d"} +{"Get":{"state":"\u0004ˇ","mode":"Insert"}} +{"Key":"ctrl-v"} +{"Key":"ctrl-j"} +{"Get":{"state":"\u0004\u0000ˇ","mode":"Insert"}} +{"Key":"ctrl-v"} +{"Key":"tab"} +{"Get":{"state":"\u0004\u0000\tˇ","mode":"Insert"}} diff --git a/crates/vim/test_data/test_ctrl_v_escape.json b/crates/vim/test_data/test_ctrl_v_escape.json new file mode 100644 index 0000000000..8c0397ef0a --- /dev/null +++ b/crates/vim/test_data/test_ctrl_v_escape.json @@ -0,0 +1,10 @@ +{"Put":{"state":"ˇ"}} +{"Key":"i"} +{"Key":"ctrl-v"} +{"Key":"9"} +{"Key":"escape"} +{"Get":{"state":"ˇ\t","mode":"Normal"}} +{"Key":"i"} +{"Key":"ctrl-v"} +{"Key":"escape"} +{"Get":{"state":"\u001bˇ\t","mode":"Insert"}} From ecb874db62099873286012789569e4bd6ad80037 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 31 Oct 2024 23:25:54 -0600 Subject: [PATCH 22/45] vim: Fix gU$ (#20057) Closes: #19380 Release Notes: - vim: Fixed `gu$` missing last character of the line --- crates/vim/src/normal/case.rs | 7 ++++++- crates/vim/test_data/test_change_case_motion.json | 4 ++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/crates/vim/src/normal/case.rs b/crates/vim/src/normal/case.rs index 463bc62750..52e37bed4c 100644 --- a/crates/vim/src/normal/case.rs +++ b/crates/vim/src/normal/case.rs @@ -28,6 +28,7 @@ impl Vim { ) { self.stop_recording(cx); self.update_editor(cx, |_, editor, cx| { + editor.set_clip_at_line_ends(false, cx); let text_layout_details = editor.text_layout_details(cx); editor.transact(cx, |editor, cx| { let mut selection_starts: HashMap<_, _> = Default::default(); @@ -52,6 +53,7 @@ impl Vim { }); }); }); + editor.set_clip_at_line_ends(true, cx); }); } @@ -261,7 +263,7 @@ mod test { #[gpui::test] async fn test_change_case_motion(cx: &mut gpui::TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx).await; - // works in visual mode + cx.set_shared_state("ˇabc def").await; cx.simulate_shared_keystrokes("g shift-u w").await; cx.shared_state().await.assert_eq("ˇABC def"); @@ -281,5 +283,8 @@ mod test { cx.simulate_shared_keystrokes(".").await; cx.shared_state().await.assert_eq("ˇabc def"); + + cx.simulate_shared_keystrokes("g shift-u $").await; + cx.shared_state().await.assert_eq("ˇABC DEF"); } } diff --git a/crates/vim/test_data/test_change_case_motion.json b/crates/vim/test_data/test_change_case_motion.json index 4d3600508f..18921f08a2 100644 --- a/crates/vim/test_data/test_change_case_motion.json +++ b/crates/vim/test_data/test_change_case_motion.json @@ -21,3 +21,7 @@ {"Get":{"state":"ˇABC def","mode":"Normal"}} {"Key":"."} {"Get":{"state":"ˇabc def","mode":"Normal"}} +{"Key":"g"} +{"Key":"shift-u"} +{"Key":"$"} +{"Get":{"state":"ˇABC DEF","mode":"Normal"}} From f757e5a6c308fe6a050b3b9d5c003d0753449f30 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 31 Oct 2024 23:25:59 -0600 Subject: [PATCH 23/45] vim: Add :noh[lsearch] (#20056) Closes: #18590 Release Notes: - vim: Add :noh[lsearch] --- crates/search/src/buffer_search.rs | 3 +++ crates/vim/src/command.rs | 1 + 2 files changed, 4 insertions(+) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 5846a6efc5..0428787570 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -546,6 +546,9 @@ impl BufferSearchBar { registrar.register_handler(ForDeployed(|this, _: &editor::actions::Cancel, cx| { this.dismiss(&Dismiss, cx); })); + registrar.register_handler(ForDeployed(|this, _: &Dismiss, cx| { + this.dismiss(&Dismiss, cx); + })); // register deploy buffer search for both search bar states, since we want to focus into the search bar // when the deploy action is triggered in the buffer. diff --git a/crates/vim/src/command.rs b/crates/vim/src/command.rs index 47975f4cfd..8aa84acc14 100644 --- a/crates/vim/src/command.rs +++ b/crates/vim/src/command.rs @@ -682,6 +682,7 @@ fn generate_commands(_: &AppContext) -> Vec { VimCommand::str(("Ch", "at"), "chat_panel::ToggleFocus"), VimCommand::str(("No", "tifications"), "notification_panel::ToggleFocus"), VimCommand::str(("A", "I"), "assistant::ToggleFocus"), + VimCommand::new(("noh", "lsearch"), search::buffer_search::Dismiss), VimCommand::new(("$", ""), EndOfDocument), VimCommand::new(("%", ""), EndOfDocument), VimCommand::new(("0", ""), StartOfDocument), From daa9939c039a4e19c89c4d374daea7b1e87c72de Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 31 Oct 2024 23:26:04 -0600 Subject: [PATCH 24/45] vim: o should scroll (#20054) Closes: #19684 Release Notes: - vim: Fixed `o` not scrolling new head into view --- crates/vim/src/visual.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index 72474d3ae4..71c8947f59 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -416,7 +416,7 @@ impl Vim { pub fn other_end(&mut self, _: &OtherEnd, cx: &mut ViewContext) { self.update_editor(cx, |_, editor, cx| { - editor.change_selections(None, cx, |s| { + editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.move_with(|_, selection| { selection.reversed = !selection.reversed; }) From ea08026cd0a68dae67d5fa16f47d430d515b200e Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 31 Oct 2024 23:49:43 -0600 Subject: [PATCH 25/45] vim: Make window shortcuts work in other contexts (#20058) Closes #18552 Release Notes: - vim: Extended `ctrl-w` to work in non-editor contexts (like markdown preview, or screen shares) --- assets/keymaps/vim.json | 97 ++++++++++++++++++++++------------------- 1 file changed, 51 insertions(+), 46 deletions(-) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 24ea1defb0..486292f20e 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -157,51 +157,6 @@ "7": ["vim::Number", 7], "8": ["vim::Number", 8], "9": ["vim::Number", 9], - // window related commands (ctrl-w X) - "ctrl-w": null, - "ctrl-w left": ["workspace::ActivatePaneInDirection", "Left"], - "ctrl-w right": ["workspace::ActivatePaneInDirection", "Right"], - "ctrl-w up": ["workspace::ActivatePaneInDirection", "Up"], - "ctrl-w down": ["workspace::ActivatePaneInDirection", "Down"], - "ctrl-w h": ["workspace::ActivatePaneInDirection", "Left"], - "ctrl-w l": ["workspace::ActivatePaneInDirection", "Right"], - "ctrl-w k": ["workspace::ActivatePaneInDirection", "Up"], - "ctrl-w j": ["workspace::ActivatePaneInDirection", "Down"], - "ctrl-w ctrl-h": ["workspace::ActivatePaneInDirection", "Left"], - "ctrl-w ctrl-l": ["workspace::ActivatePaneInDirection", "Right"], - "ctrl-w ctrl-k": ["workspace::ActivatePaneInDirection", "Up"], - "ctrl-w ctrl-j": ["workspace::ActivatePaneInDirection", "Down"], - "ctrl-w shift-left": ["workspace::SwapPaneInDirection", "Left"], - "ctrl-w shift-right": ["workspace::SwapPaneInDirection", "Right"], - "ctrl-w shift-up": ["workspace::SwapPaneInDirection", "Up"], - "ctrl-w shift-down": ["workspace::SwapPaneInDirection", "Down"], - "ctrl-w shift-h": ["workspace::SwapPaneInDirection", "Left"], - "ctrl-w shift-l": ["workspace::SwapPaneInDirection", "Right"], - "ctrl-w shift-k": ["workspace::SwapPaneInDirection", "Up"], - "ctrl-w shift-j": ["workspace::SwapPaneInDirection", "Down"], - "ctrl-w g t": "pane::ActivateNextItem", - "ctrl-w ctrl-g t": "pane::ActivateNextItem", - "ctrl-w g shift-t": "pane::ActivatePrevItem", - "ctrl-w ctrl-g shift-t": "pane::ActivatePrevItem", - "ctrl-w w": "workspace::ActivateNextPane", - "ctrl-w ctrl-w": "workspace::ActivateNextPane", - "ctrl-w p": "workspace::ActivatePreviousPane", - "ctrl-w ctrl-p": "workspace::ActivatePreviousPane", - "ctrl-w shift-w": "workspace::ActivatePreviousPane", - "ctrl-w ctrl-shift-w": "workspace::ActivatePreviousPane", - "ctrl-w v": "pane::SplitVertical", - "ctrl-w ctrl-v": "pane::SplitVertical", - "ctrl-w s": "pane::SplitHorizontal", - "ctrl-w shift-s": "pane::SplitHorizontal", - "ctrl-w ctrl-s": "pane::SplitHorizontal", - "ctrl-w c": "pane::CloseAllItems", - "ctrl-w ctrl-c": "pane::CloseAllItems", - "ctrl-w q": "pane::CloseAllItems", - "ctrl-w ctrl-q": "pane::CloseAllItems", - "ctrl-w o": "workspace::CloseInactiveTabsAndPanes", - "ctrl-w ctrl-o": "workspace::CloseInactiveTabsAndPanes", - "ctrl-w n": "workspace::NewFileSplitHorizontal", - "ctrl-w ctrl-n": "workspace::NewFileSplitHorizontal", "ctrl-w d": "editor::GoToDefinitionSplit", "ctrl-w g d": "editor::GoToDefinitionSplit", "ctrl-w shift-d": "editor::GoToTypeDefinitionSplit", @@ -546,7 +501,57 @@ } }, { - "context": "EmptyPane || SharedScreen", + "context": "ProjectPanel || CollabPanel || OutlinePanel || ChatPanel || VimControl || EmptyPane || SharedScreen || MarkdownPreview || KeyContextView", + "bindings": { + // window related commands (ctrl-w X) + "ctrl-w": null, + "ctrl-w left": ["workspace::ActivatePaneInDirection", "Left"], + "ctrl-w right": ["workspace::ActivatePaneInDirection", "Right"], + "ctrl-w up": ["workspace::ActivatePaneInDirection", "Up"], + "ctrl-w down": ["workspace::ActivatePaneInDirection", "Down"], + "ctrl-w h": ["workspace::ActivatePaneInDirection", "Left"], + "ctrl-w l": ["workspace::ActivatePaneInDirection", "Right"], + "ctrl-w k": ["workspace::ActivatePaneInDirection", "Up"], + "ctrl-w j": ["workspace::ActivatePaneInDirection", "Down"], + "ctrl-w ctrl-h": ["workspace::ActivatePaneInDirection", "Left"], + "ctrl-w ctrl-l": ["workspace::ActivatePaneInDirection", "Right"], + "ctrl-w ctrl-k": ["workspace::ActivatePaneInDirection", "Up"], + "ctrl-w ctrl-j": ["workspace::ActivatePaneInDirection", "Down"], + "ctrl-w shift-left": ["workspace::SwapPaneInDirection", "Left"], + "ctrl-w shift-right": ["workspace::SwapPaneInDirection", "Right"], + "ctrl-w shift-up": ["workspace::SwapPaneInDirection", "Up"], + "ctrl-w shift-down": ["workspace::SwapPaneInDirection", "Down"], + "ctrl-w shift-h": ["workspace::SwapPaneInDirection", "Left"], + "ctrl-w shift-l": ["workspace::SwapPaneInDirection", "Right"], + "ctrl-w shift-k": ["workspace::SwapPaneInDirection", "Up"], + "ctrl-w shift-j": ["workspace::SwapPaneInDirection", "Down"], + "ctrl-w g t": "pane::ActivateNextItem", + "ctrl-w ctrl-g t": "pane::ActivateNextItem", + "ctrl-w g shift-t": "pane::ActivatePrevItem", + "ctrl-w ctrl-g shift-t": "pane::ActivatePrevItem", + "ctrl-w w": "workspace::ActivateNextPane", + "ctrl-w ctrl-w": "workspace::ActivateNextPane", + "ctrl-w p": "workspace::ActivatePreviousPane", + "ctrl-w ctrl-p": "workspace::ActivatePreviousPane", + "ctrl-w shift-w": "workspace::ActivatePreviousPane", + "ctrl-w ctrl-shift-w": "workspace::ActivatePreviousPane", + "ctrl-w v": "pane::SplitVertical", + "ctrl-w ctrl-v": "pane::SplitVertical", + "ctrl-w s": "pane::SplitHorizontal", + "ctrl-w shift-s": "pane::SplitHorizontal", + "ctrl-w ctrl-s": "pane::SplitHorizontal", + "ctrl-w c": "pane::CloseAllItems", + "ctrl-w ctrl-c": "pane::CloseAllItems", + "ctrl-w q": "pane::CloseAllItems", + "ctrl-w ctrl-q": "pane::CloseAllItems", + "ctrl-w o": "workspace::CloseInactiveTabsAndPanes", + "ctrl-w ctrl-o": "workspace::CloseInactiveTabsAndPanes", + "ctrl-w n": "workspace::NewFileSplitHorizontal", + "ctrl-w ctrl-n": "workspace::NewFileSplitHorizontal" + } + }, + { + "context": "EmptyPane || SharedScreen || MarkdownPreview || KeyContextView", "bindings": { ":": "command_palette::Toggle", "g /": "pane::DeploySearch" From 08b124c8d44aa6a0a6f746dd148e5c1ccee7d17e Mon Sep 17 00:00:00 2001 From: Yury Zhuravlev Date: Fri, 1 Nov 2024 16:25:45 +0900 Subject: [PATCH 26/45] Add possibility to build without musl (#19813) Closes #19803 Ad Release Notes: - N/A --------- Co-authored-by: Kirill Bulatov --- script/bundle-linux | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/script/bundle-linux b/script/bundle-linux index 8edf1aaed8..2aa1dcab4a 100755 --- a/script/bundle-linux +++ b/script/bundle-linux @@ -38,27 +38,38 @@ version_info=$(rustc --version --verbose) host_line=$(echo "$version_info" | grep host) target_triple=${host_line#*: } musl_triple=${target_triple%-gnu}-musl +remote_server_triple=${REMOTE_SERVER_TARGET:-"${musl_triple}"} +rustup_installed=false +if command -v rustup >/dev/null 2>&1; then + rustup_installed=true +fi # Generate the licenses first, so they can be baked into the binaries script/generate-licenses -rustup target add "$musl_triple" + +if "$rustup_installed"; then + rustup target add "$remote_server_triple" +fi # Build binary in release mode export RUSTFLAGS="${RUSTFLAGS:-} -C link-args=-Wl,--disable-new-dtags,-rpath,\$ORIGIN/../lib" cargo build --release --target "${target_triple}" --package zed --package cli # Build remote_server in separate invocation to prevent feature unification from other crates # from influencing dynamic libraries required by it. -cargo build --release --target "${musl_triple}" --package remote_server +if [[ "$remote_server_triple" == "$musl_triple" ]]; then + export RUSTFLAGS="${RUSTFLAGS:-} -C target-feature=+crt-static" +fi +cargo build --release --target "${remote_server_triple}" --package remote_server # Strip the binary of all debug symbols # Later, we probably want to do something like this: https://github.com/GabrielMajeri/separate-symbols strip --strip-debug "${target_dir}/${target_triple}/release/zed" strip --strip-debug "${target_dir}/${target_triple}/release/cli" -strip --strip-debug "${target_dir}/${musl_triple}/release/remote_server" +strip --strip-debug "${target_dir}/${remote_server_triple}/release/remote_server" # Ensure that remote_server does not depend on libssl nor libcrypto, as we got rid of these deps. -! ldd "${target_dir}/${musl_triple}/release/remote_server" | grep -q 'libcrypto\|libssl' +! ldd "${target_dir}/${remote_server_triple}/release/remote_server" | grep -q 'libcrypto\|libssl' suffix="" if [ "$channel" != "stable" ]; then @@ -127,4 +138,4 @@ remove_match="zed(-[a-zA-Z0-9]+)?-linux-$(uname -m)\.tar\.gz" ls "${target_dir}/release" | grep -E ${remove_match} | xargs -d "\n" -I {} rm -f "${target_dir}/release/{}" || true tar -czvf "${target_dir}/release/$archive" -C ${temp_dir} "zed$suffix.app" -gzip --stdout --best "${target_dir}/${musl_triple}/release/remote_server" > "${target_dir}/zed-remote-server-linux-${arch}.gz" +gzip --stdout --best "${target_dir}/${remote_server_triple}/release/remote_server" > "${target_dir}/zed-remote-server-linux-${arch}.gz" From 183e3664cc3167f1e9e69bd5b94eb5fa71820217 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 1 Nov 2024 13:43:35 +0200 Subject: [PATCH 27/45] Mention spectre-mitigated libs component in the Windows docs (#20069) Closes https://github.com/zed-industries/zed/issues/20066 Release Notes: - N/A --- docs/src/development/windows.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/development/windows.md b/docs/src/development/windows.md index 03e8cae66b..fdb4f9500b 100644 --- a/docs/src/development/windows.md +++ b/docs/src/development/windows.md @@ -20,7 +20,7 @@ Clone down the [Zed repository](https://github.com/zed-industries/zed). rustup target add wasm32-wasip1 ``` -- Install [Visual Studio](https://visualstudio.microsoft.com/downloads/) with the optional component `MSVC v*** - VS YYYY C++ x64/x86 build tools` (`v***` is your VS version and `YYYY` is year when your VS was released) +- Install [Visual Studio](https://visualstudio.microsoft.com/downloads/) with the optional components `MSVC v*** - VS YYYY C++ x64/x86 build tools` and `MSVC v*** - VS YYYY C++ x64/x86 Spectre-mitigated libs` (`v***` is your VS version and `YYYY` is year when your VS was released. Pay attention to the architectyre and change it to yours if needed.) - Install Windows 11 or 10 SDK depending on your system, but ensure that at least `Windows 10 SDK version 2104 (10.0.20348.0)` is installed on your machine. You can download it from the [Windows SDK Archive](https://developer.microsoft.com/windows/downloads/windows-sdk/) - Install [CMake](https://cmake.org/download) From 95842c798708057a92e7a72221583c8aeeeb6994 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 1 Nov 2024 14:47:46 +0100 Subject: [PATCH 28/45] gpui: Add scroll anchors (#19894) ## Problem statement I want to add keyboard navigation support to SSH modal. Doing so is possible in current landscape, but not particularly ergonomic; `gpui::ScrollHandle` has `scroll_to_item` API that takes an index of the item you want to scroll to. The problem is, however, that it only works with it's immediate children - thus in order to support scrolling via keyboard you have to bend your UI to have a particular layout. Even when your list of items is perfectly flat, having decorations inbetween items is problematic as they are also children of the list, which means that you either have to maintain the mapping to devise a correct index of an item that you want to scroll to, or you have to make the decoration a part of the list item itself, which might render the scrolling imprecise (you might e.g. not want to scroll to a header, but to a button beneath it). ## The solution This PR adds `ScrollAnchor`, a new kind of handle to the gpui. It has a similar role to that of a ScrollHandle, but instead of tracking how far along an item has been scrolled, it tracks position of an element relative to the parent to which a given scroll handle belongs. In short, it allows us to persist the position of an element in a list of items and scroll to it even if it's not an immediate children of a container whose scroll position is tracked via an associated scroll handle. Additionally this PR adds a new kind of the container to the UI crate that serves as a convenience wrapper for using ScrollAnchors. This container provides handlers for `menu::SelectNext` and `menu::SelectPrev` and figures out which item should be focused next. Release Notes: - Improve keyboard navigation in ssh modal --- crates/gpui/src/elements/div.rs | 39 +- crates/recent_projects/src/remote_servers.rs | 900 ++++++++++-------- crates/recent_projects/src/ssh_connections.rs | 4 +- crates/ui/src/components.rs | 2 + crates/ui/src/components/navigable.rs | 98 ++ 5 files changed, 631 insertions(+), 412 deletions(-) create mode 100644 crates/ui/src/components/navigable.rs diff --git a/crates/gpui/src/elements/div.rs b/crates/gpui/src/elements/div.rs index 6c3b577e4c..e2015197e0 100644 --- a/crates/gpui/src/elements/div.rs +++ b/crates/gpui/src/elements/div.rs @@ -915,6 +915,12 @@ pub trait StatefulInteractiveElement: InteractiveElement { self } + /// Track the scroll state of this element with the given handle. + fn anchor_scroll(mut self, scroll_anchor: Option) -> Self { + self.interactivity().scroll_anchor = scroll_anchor; + self + } + /// Set the given styles to be applied when this element is active. fn active(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self where @@ -1156,6 +1162,9 @@ impl Element for Div { ) -> Option { let mut child_min = point(Pixels::MAX, Pixels::MAX); let mut child_max = Point::default(); + if let Some(handle) = self.interactivity.scroll_anchor.as_ref() { + *handle.last_origin.borrow_mut() = bounds.origin - cx.element_offset(); + } let content_size = if request_layout.child_layout_ids.is_empty() { bounds.size } else if let Some(scroll_handle) = self.interactivity.tracked_scroll_handle.as_ref() { @@ -1245,6 +1254,7 @@ pub struct Interactivity { pub(crate) focusable: bool, pub(crate) tracked_focus_handle: Option, pub(crate) tracked_scroll_handle: Option, + pub(crate) scroll_anchor: Option, pub(crate) scroll_offset: Option>>>, pub(crate) group: Option, /// The base style of the element, before any modifications are applied @@ -2091,7 +2101,6 @@ impl Interactivity { } scroll_offset.y += delta_y; scroll_offset.x += delta_x; - cx.stop_propagation(); if *scroll_offset != old_scroll_offset { cx.refresh(); @@ -2454,6 +2463,34 @@ where } } +/// Represents an element that can be scrolled *to* in its parent element. +/// +/// Contrary to [ScrollHandle::scroll_to_item], an anchored element does not have to be an immediate child of the parent. +#[derive(Clone)] +pub struct ScrollAnchor { + handle: ScrollHandle, + last_origin: Rc>>, +} + +impl ScrollAnchor { + /// Creates a [ScrollAnchor] associated with a given [ScrollHandle]. + pub fn for_handle(handle: ScrollHandle) -> Self { + Self { + handle, + last_origin: Default::default(), + } + } + /// Request scroll to this item on the next frame. + pub fn scroll_to(&self, cx: &mut WindowContext<'_>) { + let this = self.clone(); + + cx.on_next_frame(move |_| { + let viewport_bounds = this.handle.bounds(); + let self_bounds = *this.last_origin.borrow(); + this.handle.set_offset(viewport_bounds.origin - self_bounds); + }); + } +} #[derive(Default, Debug)] struct ScrollHandleState { offset: Rc>>, diff --git a/crates/recent_projects/src/remote_servers.rs b/crates/recent_projects/src/remote_servers.rs index 1b83120eb3..7806459ed1 100644 --- a/crates/recent_projects/src/remote_servers.rs +++ b/crates/recent_projects/src/remote_servers.rs @@ -20,6 +20,8 @@ use remote::SshConnectionOptions; use remote::SshRemoteClient; use settings::update_settings_file; use settings::Settings; +use ui::Navigable; +use ui::NavigableEntry; use ui::{ prelude::*, IconButtonShape, List, ListItem, ListSeparator, Modal, ModalHeader, Scrollbar, ScrollbarState, Section, Tooltip, @@ -41,12 +43,11 @@ use crate::ssh_connections::SshPrompt; use crate::ssh_connections::SshSettings; use crate::OpenRemote; +mod navigation_base {} pub struct RemoteServerProjects { mode: Mode, focus_handle: FocusHandle, - scroll_handle: ScrollHandle, workspace: WeakView, - selectable_items: SelectableItemList, retained_connections: Vec>, } @@ -79,16 +80,6 @@ struct ProjectPicker { _path_task: Shared>>, } -type SelectedItemCallback = - Box) + 'static>; - -/// Used to implement keyboard navigation for SSH modal. -#[derive(Default)] -struct SelectableItemList { - items: Vec, - active_item: Option, -} - struct EditNicknameState { index: usize, editor: View, @@ -116,60 +107,6 @@ impl EditNicknameState { } } -impl SelectableItemList { - fn reset(&mut self) { - self.items.clear(); - } - - fn reset_selection(&mut self) { - self.active_item.take(); - } - - fn prev(&mut self, _: &mut WindowContext<'_>) { - match self.active_item.as_mut() { - Some(active_index) => { - *active_index = active_index.checked_sub(1).unwrap_or(self.items.len() - 1) - } - None => { - self.active_item = Some(self.items.len() - 1); - } - } - } - - fn next(&mut self, _: &mut WindowContext<'_>) { - match self.active_item.as_mut() { - Some(active_index) => { - if *active_index + 1 < self.items.len() { - *active_index += 1; - } else { - *active_index = 0; - } - } - None => { - self.active_item = Some(0); - } - } - } - - fn add_item(&mut self, callback: SelectedItemCallback) { - self.items.push(callback) - } - - fn is_selected(&self) -> bool { - self.active_item == self.items.len().checked_sub(1) - } - - fn confirm( - &self, - remote_modal: &mut RemoteServerProjects, - cx: &mut ViewContext, - ) { - if let Some(active_item) = self.active_item.and_then(|ix| self.items.get(ix)) { - active_item(remote_modal, cx); - } - } -} - impl FocusableView for ProjectPicker { fn focus_handle(&self, cx: &AppContext) -> FocusHandle { self.picker.focus_handle(cx) @@ -309,18 +246,69 @@ impl gpui::Render for ProjectPicker { ) } } + +#[derive(Clone)] +struct ProjectEntry { + open_folder: NavigableEntry, + projects: Vec<(NavigableEntry, SshProject)>, + configure: NavigableEntry, + connection: SshConnection, +} + +#[derive(Clone)] +struct DefaultState { + scrollbar: ScrollbarState, + add_new_server: NavigableEntry, + servers: Vec, +} +impl DefaultState { + fn new(cx: &WindowContext<'_>) -> Self { + let handle = ScrollHandle::new(); + let scrollbar = ScrollbarState::new(handle.clone()); + let add_new_server = NavigableEntry::new(&handle, cx); + let servers = SshSettings::get_global(cx) + .ssh_connections() + .map(|connection| { + let open_folder = NavigableEntry::new(&handle, cx); + let configure = NavigableEntry::new(&handle, cx); + let projects = connection + .projects + .iter() + .map(|project| (NavigableEntry::new(&handle, cx), project.clone())) + .collect(); + ProjectEntry { + open_folder, + configure, + projects, + connection, + } + }) + .collect(); + Self { + scrollbar, + add_new_server, + servers, + } + } +} + +#[derive(Clone)] +struct ViewServerOptionsState { + server_index: usize, + connection: SshConnection, + entries: [NavigableEntry; 4], +} enum Mode { - Default(ScrollbarState), - ViewServerOptions(usize, SshConnection), + Default(DefaultState), + ViewServerOptions(ViewServerOptionsState), EditNickname(EditNicknameState), ProjectPicker(View), CreateRemoteServer(CreateRemoteServer), } impl Mode { - fn default_mode() -> Self { - let handle = ScrollHandle::new(); - Self::Default(ScrollbarState::new(handle)) + fn default_mode(cx: &WindowContext<'_>) -> Self { + Self::Default(DefaultState::new(cx)) } } impl RemoteServerProjects { @@ -348,30 +336,13 @@ impl RemoteServerProjects { }); Self { - mode: Mode::default_mode(), + mode: Mode::default_mode(cx), focus_handle, - scroll_handle: ScrollHandle::new(), workspace, - selectable_items: Default::default(), retained_connections: Vec::new(), } } - fn next_item(&mut self, _: &menu::SelectNext, cx: &mut ViewContext) { - if !matches!(self.mode, Mode::Default(_) | Mode::ViewServerOptions(_, _)) { - return; - } - - self.selectable_items.next(cx); - } - - fn prev_item(&mut self, _: &menu::SelectPrev, cx: &mut ViewContext) { - if !matches!(self.mode, Mode::Default(_) | Mode::ViewServerOptions(_, _)) { - return; - } - self.selectable_items.prev(cx); - } - pub fn project_picker( ix: usize, connection_options: remote::SshConnectionOptions, @@ -433,8 +404,7 @@ impl RemoteServerProjects { }); this.retained_connections.push(client); this.add_ssh_server(connection_options, cx); - this.mode = Mode::default_mode(); - this.selectable_items.reset_selection(); + this.mode = Mode::default_mode(cx); cx.notify() }) .log_err(), @@ -469,11 +439,15 @@ impl RemoteServerProjects { fn view_server_options( &mut self, - (index, connection): (usize, SshConnection), + (server_index, connection): (usize, SshConnection), cx: &mut ViewContext, ) { - self.selectable_items.reset_selection(); - self.mode = Mode::ViewServerOptions(index, connection); + self.mode = Mode::ViewServerOptions(ViewServerOptionsState { + server_index, + connection, + entries: std::array::from_fn(|_| NavigableEntry::focusable(cx)), + }); + self.focus_handle(cx).focus(cx); cx.notify(); } @@ -562,11 +536,7 @@ impl RemoteServerProjects { fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { match &self.mode { - Mode::Default(_) | Mode::ViewServerOptions(_, _) => { - let items = std::mem::take(&mut self.selectable_items); - items.confirm(self, cx); - self.selectable_items = items; - } + Mode::Default(_) | Mode::ViewServerOptions(_) => {} Mode::ProjectPicker(_) => {} Mode::CreateRemoteServer(state) => { if let Some(prompt) = state.ssh_prompt.as_ref() { @@ -588,8 +558,7 @@ impl RemoteServerProjects { } } }); - self.mode = Mode::default_mode(); - self.selectable_items.reset_selection(); + self.mode = Mode::default_mode(cx); self.focus_handle.focus(cx); } } @@ -606,12 +575,10 @@ impl RemoteServerProjects { }); self.mode = Mode::CreateRemoteServer(new_state); - self.selectable_items.reset_selection(); cx.notify(); } _ => { - self.mode = Mode::default_mode(); - self.selectable_items.reset_selection(); + self.mode = Mode::default_mode(cx); self.focus_handle(cx).focus(cx); cx.notify(); } @@ -621,14 +588,15 @@ impl RemoteServerProjects { fn render_ssh_connection( &mut self, ix: usize, - ssh_connection: SshConnection, + ssh_server: ProjectEntry, cx: &mut ViewContext, ) -> impl IntoElement { - let (main_label, aux_label) = if let Some(nickname) = ssh_connection.nickname.clone() { - let aux_label = SharedString::from(format!("({})", ssh_connection.host)); + let (main_label, aux_label) = if let Some(nickname) = ssh_server.connection.nickname.clone() + { + let aux_label = SharedString::from(format!("({})", ssh_server.connection.host)); (nickname.into(), Some(aux_label)) } else { - (ssh_connection.host.clone(), None) + (ssh_server.connection.host.clone(), None) }; v_flex() .w_full() @@ -657,75 +625,101 @@ impl RemoteServerProjects { .child( List::new() .empty_message("No projects.") - .children(ssh_connection.projects.iter().enumerate().map(|(pix, p)| { + .children(ssh_server.projects.iter().enumerate().map(|(pix, p)| { v_flex().gap_0p5().child(self.render_ssh_project( ix, - &ssh_connection, + &ssh_server, pix, p, cx, )) })) - .child(h_flex().map(|this| { - self.selectable_items.add_item(Box::new({ - let ssh_connection = ssh_connection.clone(); - move |this, cx| { - this.create_ssh_project(ix, ssh_connection.clone(), cx); - } - })); - let is_selected = self.selectable_items.is_selected(); - this.child( - ListItem::new(("new-remote-project", ix)) - .selected(is_selected) - .inset(true) - .spacing(ui::ListItemSpacing::Sparse) - .start_slot(Icon::new(IconName::Plus).color(Color::Muted)) - .child(Label::new("Open Folder")) - .on_click(cx.listener({ - let ssh_connection = ssh_connection.clone(); - move |this, _, cx| { - this.create_ssh_project(ix, ssh_connection.clone(), cx); - } - })), - ) - })) - .child(h_flex().map(|this| { - self.selectable_items.add_item(Box::new({ - let ssh_connection = ssh_connection.clone(); - move |this, cx| { - this.view_server_options((ix, ssh_connection.clone()), cx); - } - })); - let is_selected = self.selectable_items.is_selected(); - this.child( - ListItem::new(("server-options", ix)) - .selected(is_selected) - .inset(true) - .spacing(ui::ListItemSpacing::Sparse) - .start_slot(Icon::new(IconName::Settings).color(Color::Muted)) - .child(Label::new("View Server Options")) - .on_click(cx.listener({ - let ssh_connection = ssh_connection.clone(); - move |this, _, cx| { - this.view_server_options((ix, ssh_connection.clone()), cx); - } - })), - ) - })), + .child( + h_flex() + .id(("new-remote-project-container", ix)) + .track_focus(&ssh_server.open_folder.focus_handle) + .anchor_scroll(ssh_server.open_folder.scroll_anchor.clone()) + .on_action(cx.listener({ + let ssh_connection = ssh_server.clone(); + move |this, _: &menu::Confirm, cx| { + this.create_ssh_project( + ix, + ssh_connection.connection.clone(), + cx, + ); + } + })) + .child( + ListItem::new(("new-remote-project", ix)) + .selected( + ssh_server.open_folder.focus_handle.contains_focused(cx), + ) + .inset(true) + .spacing(ui::ListItemSpacing::Sparse) + .start_slot(Icon::new(IconName::Plus).color(Color::Muted)) + .child(Label::new("Open Folder")) + .on_click(cx.listener({ + let ssh_connection = ssh_server.clone(); + move |this, _, cx| { + this.create_ssh_project( + ix, + ssh_connection.connection.clone(), + cx, + ); + } + })), + ), + ) + .child( + h_flex() + .id(("server-options-container", ix)) + .track_focus(&ssh_server.configure.focus_handle) + .anchor_scroll(ssh_server.configure.scroll_anchor.clone()) + .on_action(cx.listener({ + let ssh_connection = ssh_server.clone(); + move |this, _: &menu::Confirm, cx| { + this.view_server_options( + (ix, ssh_connection.connection.clone()), + cx, + ); + } + })) + .child( + ListItem::new(("server-options", ix)) + .selected( + ssh_server.configure.focus_handle.contains_focused(cx), + ) + .inset(true) + .spacing(ui::ListItemSpacing::Sparse) + .start_slot(Icon::new(IconName::Settings).color(Color::Muted)) + .child(Label::new("View Server Options")) + .on_click(cx.listener({ + let ssh_connection = ssh_server.clone(); + move |this, _, cx| { + this.view_server_options( + (ix, ssh_connection.connection.clone()), + cx, + ); + } + })), + ), + ), ) } fn render_ssh_project( &mut self, server_ix: usize, - server: &SshConnection, + server: &ProjectEntry, ix: usize, - project: &SshProject, + (navigation, project): &(NavigableEntry, SshProject), cx: &ViewContext, ) -> impl IntoElement { let server = server.clone(); - let element_id_base = SharedString::from(format!("remote-project-{server_ix}")); + let container_element_id_base = + SharedString::from(format!("remote-project-container-{element_id_base}")); + let callback = Arc::new({ let project = project.clone(); move |this: &mut Self, cx: &mut ViewContext| { @@ -737,7 +731,7 @@ impl RemoteServerProjects { return; }; let project = project.clone(); - let server = server.clone(); + let server = server.connection.clone(); cx.emit(DismissEvent); cx.spawn(|_, mut cx| async move { let result = open_ssh_project( @@ -763,39 +757,46 @@ impl RemoteServerProjects { .detach(); } }); - self.selectable_items.add_item(Box::new({ - let callback = callback.clone(); - move |this, cx| callback(this, cx) - })); - let is_selected = self.selectable_items.is_selected(); - ListItem::new((element_id_base, ix)) - .inset(true) - .selected(is_selected) - .spacing(ui::ListItemSpacing::Sparse) - .start_slot( - Icon::new(IconName::Folder) - .color(Color::Muted) - .size(IconSize::Small), - ) - .child(Label::new(project.paths.join(", "))) - .on_click(cx.listener(move |this, _, cx| callback(this, cx))) - .end_hover_slot::(Some( - div() - .mr_2() - .child( - // Right-margin to offset it from the Scrollbar - IconButton::new("remove-remote-project", IconName::TrashAlt) - .icon_size(IconSize::Small) - .shape(IconButtonShape::Square) - .size(ButtonSize::Large) - .tooltip(|cx| Tooltip::text("Delete Remote Project", cx)) - .on_click(cx.listener(move |this, _, cx| { - this.delete_ssh_project(server_ix, ix, cx) - })), + div() + .id((container_element_id_base, ix)) + .track_focus(&navigation.focus_handle) + .anchor_scroll(navigation.scroll_anchor.clone()) + .on_action(cx.listener({ + let callback = callback.clone(); + move |this, _: &menu::Confirm, cx| { + callback(this, cx); + } + })) + .child( + ListItem::new((element_id_base, ix)) + .selected(navigation.focus_handle.contains_focused(cx)) + .inset(true) + .spacing(ui::ListItemSpacing::Sparse) + .start_slot( + Icon::new(IconName::Folder) + .color(Color::Muted) + .size(IconSize::Small), ) - .into_any_element(), - )) + .child(Label::new(project.paths.join(", "))) + .on_click(cx.listener(move |this, _, cx| callback(this, cx))) + .end_hover_slot::(Some( + div() + .mr_2() + .child( + // Right-margin to offset it from the Scrollbar + IconButton::new("remove-remote-project", IconName::TrashAlt) + .icon_size(IconSize::Small) + .shape(IconButtonShape::Square) + .size(ButtonSize::Large) + .tooltip(|cx| Tooltip::text("Delete Remote Project", cx)) + .on_click(cx.listener(move |this, _, cx| { + this.delete_ssh_project(server_ix, ix, cx) + })), + ) + .into_any_element(), + )), + ) } fn update_settings_file( @@ -870,6 +871,7 @@ impl RemoteServerProjects { let theme = cx.theme(); v_flex() + .track_focus(&self.focus_handle(cx)) .id("create-remote-server") .overflow_hidden() .size_full() @@ -930,185 +932,231 @@ impl RemoteServerProjects { fn render_view_options( &mut self, - index: usize, - connection: SshConnection, + ViewServerOptionsState { + server_index, + connection, + entries, + }: ViewServerOptionsState, cx: &mut ViewContext, ) -> impl IntoElement { let connection_string = connection.host.clone(); - div() - .size_full() - .child( - SshConnectionHeader { - connection_string: connection_string.clone(), - paths: Default::default(), - nickname: connection.nickname.clone().map(|s| s.into()), - } - .render(cx), - ) - .child( - v_flex() - .pb_1() - .child(ListSeparator) - .child({ - self.selectable_items.add_item(Box::new({ - move |this, cx| { - this.mode = Mode::EditNickname(EditNicknameState::new(index, cx)); - cx.notify(); - } - })); - let is_selected = self.selectable_items.is_selected(); - let label = if connection.nickname.is_some() { - "Edit Nickname" - } else { - "Add Nickname to Server" - }; - ListItem::new("add-nickname") - .selected(is_selected) - .inset(true) - .spacing(ui::ListItemSpacing::Sparse) - .start_slot(Icon::new(IconName::Pencil).color(Color::Muted)) - .child(Label::new(label)) - .on_click(cx.listener(move |this, _, cx| { - this.mode = Mode::EditNickname(EditNicknameState::new(index, cx)); - cx.notify(); - })) - }) - .child({ - let workspace = self.workspace.clone(); - fn callback( - workspace: WeakView, - connection_string: SharedString, - cx: &mut WindowContext<'_>, - ) { - cx.write_to_clipboard(ClipboardItem::new_string( - connection_string.to_string(), - )); - workspace - .update(cx, |this, cx| { - struct SshServerAddressCopiedToClipboard; - let notification = format!( - "Copied server address ({}) to clipboard", - connection_string - ); - - this.show_toast( - Toast::new( - NotificationId::composite::< - SshServerAddressCopiedToClipboard, - >( - connection_string.clone() - ), - notification, - ) - .autohide(), + let mut view = Navigable::new( + div() + .track_focus(&self.focus_handle(cx)) + .size_full() + .child( + SshConnectionHeader { + connection_string: connection_string.clone(), + paths: Default::default(), + nickname: connection.nickname.clone().map(|s| s.into()), + } + .render(cx), + ) + .child( + v_flex() + .pb_1() + .child(ListSeparator) + .child({ + let label = if connection.nickname.is_some() { + "Edit Nickname" + } else { + "Add Nickname to Server" + }; + div() + .id("ssh-options-add-nickname") + .track_focus(&entries[0].focus_handle) + .on_action(cx.listener(move |this, _: &menu::Confirm, cx| { + this.mode = Mode::EditNickname(EditNicknameState::new( + server_index, cx, - ); - }) - .ok(); - } - self.selectable_items.add_item(Box::new({ - let workspace = workspace.clone(); - let connection_string = connection_string.clone(); - move |_, cx| { - callback(workspace.clone(), connection_string.clone(), cx); - } - })); - let is_selected = self.selectable_items.is_selected(); - ListItem::new("copy-server-address") - .selected(is_selected) - .inset(true) - .spacing(ui::ListItemSpacing::Sparse) - .start_slot(Icon::new(IconName::Copy).color(Color::Muted)) - .child(Label::new("Copy Server Address")) - .end_hover_slot( - Label::new(connection_string.clone()).color(Color::Muted), - ) - .on_click({ - let connection_string = connection_string.clone(); - move |_, cx| { - callback(workspace.clone(), connection_string.clone(), cx); - } - }) - }) - .child({ - fn remove_ssh_server( - remote_servers: View, - index: usize, - connection_string: SharedString, - cx: &mut WindowContext<'_>, - ) { - let prompt_message = format!("Remove server `{}`?", connection_string); - - let confirmation = cx.prompt( - PromptLevel::Warning, - &prompt_message, - None, - &["Yes, remove it", "No, keep it"], - ); - - cx.spawn(|mut cx| async move { - if confirmation.await.ok() == Some(0) { - remote_servers - .update(&mut cx, |this, cx| { - this.delete_ssh_server(index, cx); - this.mode = Mode::default_mode(); + )); + cx.notify(); + })) + .child( + ListItem::new("add-nickname") + .selected(entries[0].focus_handle.contains_focused(cx)) + .inset(true) + .spacing(ui::ListItemSpacing::Sparse) + .start_slot(Icon::new(IconName::Pencil).color(Color::Muted)) + .child(Label::new(label)) + .on_click(cx.listener(move |this, _, cx| { + this.mode = Mode::EditNickname(EditNicknameState::new( + server_index, + cx, + )); cx.notify(); - }) - .ok(); - } - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - } - self.selectable_items.add_item(Box::new({ - let connection_string = connection_string.clone(); - move |_, cx| { - remove_ssh_server( - cx.view().clone(), - index, - connection_string.clone(), - cx, - ); + })), + ) + }) + .child({ + let workspace = self.workspace.clone(); + fn callback( + workspace: WeakView, + connection_string: SharedString, + cx: &mut WindowContext<'_>, + ) { + cx.write_to_clipboard(ClipboardItem::new_string( + connection_string.to_string(), + )); + workspace + .update(cx, |this, cx| { + struct SshServerAddressCopiedToClipboard; + let notification = format!( + "Copied server address ({}) to clipboard", + connection_string + ); + + this.show_toast( + Toast::new( + NotificationId::composite::< + SshServerAddressCopiedToClipboard, + >( + connection_string.clone() + ), + notification, + ) + .autohide(), + cx, + ); + }) + .ok(); } - })); - let is_selected = self.selectable_items.is_selected(); - ListItem::new("remove-server") - .selected(is_selected) - .inset(true) - .spacing(ui::ListItemSpacing::Sparse) - .start_slot(Icon::new(IconName::Trash).color(Color::Error)) - .child(Label::new("Remove Server").color(Color::Error)) - .on_click(cx.listener(move |_, _, cx| { - remove_ssh_server( - cx.view().clone(), - index, - connection_string.clone(), - cx, + div() + .id("ssh-options-copy-server-address") + .track_focus(&entries[1].focus_handle) + .on_action({ + let connection_string = connection_string.clone(); + let workspace = self.workspace.clone(); + move |_: &menu::Confirm, cx| { + callback(workspace.clone(), connection_string.clone(), cx); + } + }) + .child( + ListItem::new("copy-server-address") + .selected(entries[1].focus_handle.contains_focused(cx)) + .inset(true) + .spacing(ui::ListItemSpacing::Sparse) + .start_slot(Icon::new(IconName::Copy).color(Color::Muted)) + .child(Label::new("Copy Server Address")) + .end_hover_slot( + Label::new(connection_string.clone()) + .color(Color::Muted), + ) + .on_click({ + let connection_string = connection_string.clone(); + move |_, cx| { + callback( + workspace.clone(), + connection_string.clone(), + cx, + ); + } + }), + ) + }) + .child({ + fn remove_ssh_server( + remote_servers: View, + index: usize, + connection_string: SharedString, + cx: &mut WindowContext<'_>, + ) { + let prompt_message = + format!("Remove server `{}`?", connection_string); + + let confirmation = cx.prompt( + PromptLevel::Warning, + &prompt_message, + None, + &["Yes, remove it", "No, keep it"], ); - })) - }) - .child(ListSeparator) - .child({ - self.selectable_items.add_item(Box::new({ - move |this, cx| { - this.mode = Mode::default_mode(); - cx.notify(); + + cx.spawn(|mut cx| async move { + if confirmation.await.ok() == Some(0) { + remote_servers + .update(&mut cx, |this, cx| { + this.delete_ssh_server(index, cx); + }) + .ok(); + remote_servers + .update(&mut cx, |this, cx| { + this.mode = Mode::default_mode(cx); + cx.notify(); + }) + .ok(); + } + anyhow::Ok(()) + }) + .detach_and_log_err(cx); } - })); - let is_selected = self.selectable_items.is_selected(); - ListItem::new("go-back") - .selected(is_selected) - .inset(true) - .spacing(ui::ListItemSpacing::Sparse) - .start_slot(Icon::new(IconName::ArrowLeft).color(Color::Muted)) - .child(Label::new("Go Back")) - .on_click(cx.listener(|this, _, cx| { - this.mode = Mode::default_mode(); - cx.notify() - })) - }), - ) + div() + .id("ssh-options-copy-server-address") + .track_focus(&entries[2].focus_handle) + .on_action(cx.listener({ + let connection_string = connection_string.clone(); + move |_, _: &menu::Confirm, cx| { + remove_ssh_server( + cx.view().clone(), + server_index, + connection_string.clone(), + cx, + ); + cx.focus_self(); + } + })) + .child( + ListItem::new("remove-server") + .selected(entries[2].focus_handle.contains_focused(cx)) + .inset(true) + .spacing(ui::ListItemSpacing::Sparse) + .start_slot(Icon::new(IconName::Trash).color(Color::Error)) + .child(Label::new("Remove Server").color(Color::Error)) + .on_click(cx.listener(move |_, _, cx| { + remove_ssh_server( + cx.view().clone(), + server_index, + connection_string.clone(), + cx, + ); + cx.focus_self(); + })), + ) + }) + .child(ListSeparator) + .child({ + div() + .id("ssh-options-copy-server-address") + .track_focus(&entries[3].focus_handle) + .on_action(cx.listener(|this, _: &menu::Confirm, cx| { + this.mode = Mode::default_mode(cx); + cx.focus_self(); + cx.notify(); + })) + .child( + ListItem::new("go-back") + .selected(entries[3].focus_handle.contains_focused(cx)) + .inset(true) + .spacing(ui::ListItemSpacing::Sparse) + .start_slot( + Icon::new(IconName::ArrowLeft).color(Color::Muted), + ) + .child(Label::new("Go Back")) + .on_click(cx.listener(|this, _, cx| { + this.mode = Mode::default_mode(cx); + cx.focus_self(); + cx.notify() + })), + ) + }), + ) + .into_any_element(), + ); + for entry in entries { + view = view.entry(entry); + } + + view.render(cx).into_any_element() } fn render_edit_nickname( @@ -1120,13 +1168,17 @@ impl RemoteServerProjects { .ssh_connections() .nth(state.index) else { - return v_flex(); + return v_flex() + .id("ssh-edit-nickname") + .track_focus(&self.focus_handle(cx)); }; let connection_string = connection.host.clone(); let nickname = connection.nickname.clone().map(|s| s.into()); v_flex() + .id("ssh-edit-nickname") + .track_focus(&self.focus_handle(cx)) .child( SshConnectionHeader { connection_string, @@ -1146,27 +1198,45 @@ impl RemoteServerProjects { fn render_default( &mut self, - scroll_state: ScrollbarState, + mut state: DefaultState, cx: &mut ViewContext, ) -> impl IntoElement { - let scroll_state = scroll_state.parent_view(cx.view()); - let ssh_connections = SshSettings::get_global(cx) - .ssh_connections() - .collect::>(); - self.selectable_items.add_item(Box::new(|this, cx| { - this.mode = Mode::CreateRemoteServer(CreateRemoteServer::new(cx)); - cx.notify(); - })); + if SshSettings::get_global(cx) + .ssh_connections + .as_ref() + .map_or(false, |connections| { + state + .servers + .iter() + .map(|server| &server.connection) + .ne(connections.iter()) + }) + { + self.mode = Mode::default_mode(cx); + if let Mode::Default(new_state) = &self.mode { + state = new_state.clone(); + } + } + let scroll_state = state.scrollbar.parent_view(cx.view()); + let connect_button = div() + .id("ssh-connect-new-server-container") + .track_focus(&state.add_new_server.focus_handle) + .anchor_scroll(state.add_new_server.scroll_anchor.clone()) + .child( + ListItem::new("register-remove-server-button") + .selected(state.add_new_server.focus_handle.contains_focused(cx)) + .inset(true) + .spacing(ui::ListItemSpacing::Sparse) + .start_slot(Icon::new(IconName::Plus).color(Color::Muted)) + .child(Label::new("Connect New Server")) + .on_click(cx.listener(|this, _, cx| { + let state = CreateRemoteServer::new(cx); + this.mode = Mode::CreateRemoteServer(state); - let is_selected = self.selectable_items.is_selected(); - - let connect_button = ListItem::new("register-remove-server-button") - .selected(is_selected) - .inset(true) - .spacing(ui::ListItemSpacing::Sparse) - .start_slot(Icon::new(IconName::Plus).color(Color::Muted)) - .child(Label::new("Connect New Server")) - .on_click(cx.listener(|this, _, cx| { + cx.notify(); + })), + ) + .on_action(cx.listener(|this, _: &menu::Confirm, cx| { let state = CreateRemoteServer::new(cx); this.mode = Mode::CreateRemoteServer(state); @@ -1177,31 +1247,46 @@ impl RemoteServerProjects { unreachable!() }; - let mut modal_section = v_flex() - .id("ssh-server-list") - .overflow_y_scroll() - .track_scroll(&scroll_handle) - .size_full() - .child(connect_button) - .child( - List::new() - .empty_message( - v_flex() - .child(div().px_3().child( - Label::new("No remote servers registered yet.").color(Color::Muted), - )) - .into_any_element(), - ) - .children(ssh_connections.iter().cloned().enumerate().map( - |(ix, connection)| { - self.render_ssh_connection(ix, connection, cx) + let mut modal_section = Navigable::new( + v_flex() + .track_focus(&self.focus_handle(cx)) + .id("ssh-server-list") + .overflow_y_scroll() + .track_scroll(&scroll_handle) + .size_full() + .child(connect_button) + .child( + List::new() + .empty_message( + v_flex() + .child( + div().px_3().child( + Label::new("No remote servers registered yet.") + .color(Color::Muted), + ), + ) + .into_any_element(), + ) + .children(state.servers.iter().enumerate().map(|(ix, connection)| { + self.render_ssh_connection(ix, connection.clone(), cx) .into_any_element() - }, - )), - ) - .into_any_element(); + })), + ) + .into_any_element(), + ) + .entry(state.add_new_server.clone()); - Modal::new("remote-projects", Some(self.scroll_handle.clone())) + for server in &state.servers { + for (navigation_state, _) in &server.projects { + modal_section = modal_section.entry(navigation_state.clone()); + } + modal_section = modal_section + .entry(server.open_folder.clone()) + .entry(server.configure.clone()); + } + let mut modal_section = modal_section.render(cx).into_any_element(); + + Modal::new("remote-projects", None) .header( ModalHeader::new() .child(Headline::new("Remote Projects (beta)").size(HeadlineSize::XSmall)), @@ -1242,6 +1327,7 @@ impl RemoteServerProjects { ), ), ) + .into_any_element() } } @@ -1264,16 +1350,12 @@ impl EventEmitter for RemoteServerProjects {} impl Render for RemoteServerProjects { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - self.selectable_items.reset(); div() - .track_focus(&self.focus_handle(cx)) .elevation_3(cx) .w(rems(34.)) .key_context("RemoteServerModal") .on_action(cx.listener(Self::cancel)) .on_action(cx.listener(Self::confirm)) - .on_action(cx.listener(Self::prev_item)) - .on_action(cx.listener(Self::next_item)) .capture_any_mouse_down(cx.listener(|this, _, cx| { this.focus_handle(cx).focus(cx); })) @@ -1284,8 +1366,8 @@ impl Render for RemoteServerProjects { })) .child(match &self.mode { Mode::Default(state) => self.render_default(state.clone(), cx).into_any_element(), - Mode::ViewServerOptions(index, connection) => self - .render_view_options(*index, connection.clone(), cx) + Mode::ViewServerOptions(state) => self + .render_view_options(state.clone(), cx) .into_any_element(), Mode::ProjectPicker(element) => element.clone().into_any_element(), Mode::CreateRemoteServer(state) => self diff --git a/crates/recent_projects/src/ssh_connections.rs b/crates/recent_projects/src/ssh_connections.rs index 0d40da375b..f84576a1d9 100644 --- a/crates/recent_projects/src/ssh_connections.rs +++ b/crates/recent_projects/src/ssh_connections.rs @@ -63,7 +63,7 @@ impl SshSettings { } } -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] +#[derive(Clone, Default, Serialize, Deserialize, PartialEq, JsonSchema)] pub struct SshConnection { pub host: SharedString, #[serde(skip_serializing_if = "Option::is_none")] @@ -100,7 +100,7 @@ impl From for SshConnectionOptions { } } -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] +#[derive(Clone, Default, Serialize, PartialEq, Deserialize, JsonSchema)] pub struct SshProject { pub paths: Vec, } diff --git a/crates/ui/src/components.rs b/crates/ui/src/components.rs index 7a13ff6917..08ebe1a771 100644 --- a/crates/ui/src/components.rs +++ b/crates/ui/src/components.rs @@ -14,6 +14,7 @@ mod keybinding; mod label; mod list; mod modal; +mod navigable; mod numeric_stepper; mod popover; mod popover_menu; @@ -47,6 +48,7 @@ pub use keybinding::*; pub use label::*; pub use list::*; pub use modal::*; +pub use navigable::*; pub use numeric_stepper::*; pub use popover::*; pub use popover_menu::*; diff --git a/crates/ui/src/components/navigable.rs b/crates/ui/src/components/navigable.rs new file mode 100644 index 0000000000..fadd6d597e --- /dev/null +++ b/crates/ui/src/components/navigable.rs @@ -0,0 +1,98 @@ +use crate::prelude::*; +use gpui::{AnyElement, FocusHandle, ScrollAnchor, ScrollHandle}; + +/// An element that can be navigated through via keyboard. Intended for use with scrollable views that want to use +pub struct Navigable { + child: AnyElement, + selectable_children: Vec, +} + +/// An entry of [Navigable] that can be navigated to. +#[derive(Clone)] +pub struct NavigableEntry { + #[allow(missing_docs)] + pub focus_handle: FocusHandle, + #[allow(missing_docs)] + pub scroll_anchor: Option, +} + +impl NavigableEntry { + /// Creates a new [NavigableEntry] for a given scroll handle. + pub fn new(scroll_handle: &ScrollHandle, cx: &WindowContext<'_>) -> Self { + Self { + focus_handle: cx.focus_handle(), + scroll_anchor: Some(ScrollAnchor::for_handle(scroll_handle.clone())), + } + } + /// Create a new [NavigableEntry] that cannot be scrolled to. + pub fn focusable(cx: &WindowContext<'_>) -> Self { + Self { + focus_handle: cx.focus_handle(), + scroll_anchor: None, + } + } +} +impl Navigable { + /// Creates new empty [Navigable] wrapper. + pub fn new(child: AnyElement) -> Self { + Self { + child, + selectable_children: vec![], + } + } + + /// Add a new entry that can be navigated to via keyboard. + /// The order of calls to [Navigable::entry] determines the order of traversal of elements via successive + /// uses of [menu:::SelectNext]/[menu::SelectPrev] + pub fn entry(mut self, child: NavigableEntry) -> Self { + self.selectable_children.push(child); + self + } + + fn find_focused( + selectable_children: &[NavigableEntry], + cx: &mut WindowContext<'_>, + ) -> Option { + selectable_children + .iter() + .position(|entry| entry.focus_handle.contains_focused(cx)) + } +} +impl RenderOnce for Navigable { + fn render(self, _: &mut WindowContext<'_>) -> impl crate::IntoElement { + div() + .on_action({ + let children = self.selectable_children.clone(); + + move |_: &menu::SelectNext, cx| { + let target = Self::find_focused(&children, cx) + .and_then(|index| { + index.checked_add(1).filter(|index| *index < children.len()) + }) + .unwrap_or(0); + if let Some(entry) = children.get(target) { + entry.focus_handle.focus(cx); + if let Some(anchor) = &entry.scroll_anchor { + anchor.scroll_to(cx); + } + } + } + }) + .on_action({ + let children = self.selectable_children; + move |_: &menu::SelectPrev, cx| { + let target = Self::find_focused(&children, cx) + .and_then(|index| index.checked_sub(1)) + .or(children.len().checked_sub(1)); + if let Some(entry) = target.and_then(|target| children.get(target)) { + entry.focus_handle.focus(cx); + if let Some(anchor) = &entry.scroll_anchor { + anchor.scroll_to(cx); + } + } + } + }) + .size_full() + .child(self.child) + } +} From cec72b837eb5c950b6c1453cb45989df85ad7747 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 1 Nov 2024 16:06:04 +0200 Subject: [PATCH 29/45] Update Rust crate linkme to v0.3.29 (#19657) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [linkme](https://redirect.github.com/dtolnay/linkme) | dependencies | patch | `0.3.28` -> `0.3.29` | --- ### Release Notes
dtolnay/linkme (linkme) ### [`v0.3.29`](https://redirect.github.com/dtolnay/linkme/releases/tag/0.3.29) [Compare Source](https://redirect.github.com/dtolnay/linkme/compare/0.3.28...0.3.29) - Add UEFI target support ([#​100](https://redirect.github.com/dtolnay/linkme/issues/100), thanks [@​Javagedes](https://redirect.github.com/Javagedes))
--- ### Configuration 📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone America/New_York, Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- Release Notes: - N/A Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a14680f981..b56d4861d3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6556,18 +6556,18 @@ dependencies = [ [[package]] name = "linkme" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c943daedff228392b791b33bba32e75737756e80a613e32e246c6ce9cbab20a" +checksum = "70fe496a7af8c406f877635cbf3cd6a9fac9d6f443f58691cd8afe6ce0971af4" dependencies = [ "linkme-impl", ] [[package]] name = "linkme-impl" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb26336e6dc7cc76e7927d2c9e7e3bb376d7af65a6f56a0b16c47d18a9b1abc5" +checksum = "b01f197a15988fb5b2ec0a5a9800c97e70771499c456ad757d63b3c5e9b96e75" dependencies = [ "proc-macro2", "quote", From dde692eb88a592c49c472e78c0d90add1f2d7647 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 1 Nov 2024 16:06:16 +0200 Subject: [PATCH 30/45] Update Rust crate libc to v0.2.161 (#19650) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [libc](https://redirect.github.com/rust-lang/libc) | workspace.dependencies | patch | `0.2.159` -> `0.2.161` | --- ### Release Notes
rust-lang/libc (libc) ### [`v0.2.161`](https://redirect.github.com/rust-lang/libc/releases/tag/0.2.161) [Compare Source](https://redirect.github.com/rust-lang/libc/compare/0.2.160...0.2.161) ##### Fixed - OpenBSD: fix `FNM_PATHNAME` and `FNM_NOESCAPE` values [#​3983](https://redirect.github.com/rust-lang/libc/pull/3983) ### [`v0.2.160`](https://redirect.github.com/rust-lang/libc/releases/tag/0.2.160) [Compare Source](https://redirect.github.com/rust-lang/libc/compare/0.2.159...0.2.160) ##### Added - Android: add `PR_GET_NAME` and `PR_SET_NAME` [#​3941](https://redirect.github.com/rust-lang/libc/pull/3941) - Apple: add `F_TRANSFEREXTENTS` [#​3925](https://redirect.github.com/rust-lang/libc/pull/3925) - Apple: add `mach_error_string` [#​3913](https://redirect.github.com/rust-lang/libc/pull/3913) - Apple: add additional `pthread` APIs [#​3846](https://redirect.github.com/rust-lang/libc/pull/3846) - Apple: add the `LOCAL_PEERTOKEN` socket option [#​3929](https://redirect.github.com/rust-lang/libc/pull/3929) - BSD: add `RTF_*`, `RTA_*`, `RTAX_*`, and `RTM_*` definitions [#​3714](https://redirect.github.com/rust-lang/libc/pull/3714) - Emscripten: add `AT_EACCESS` [#​3911](https://redirect.github.com/rust-lang/libc/pull/3911) - Emscripten: add `getgrgid`, `getgrnam`, `getgrnam_r` and `getgrgid_r` [#​3912](https://redirect.github.com/rust-lang/libc/pull/3912) - Emscripten: add `getpwnam_r` and `getpwuid_r` [#​3906](https://redirect.github.com/rust-lang/libc/pull/3906) - FreeBSD: add `POLLRDHUP` [#​3936](https://redirect.github.com/rust-lang/libc/pull/3936) - Haiku: add `arc4random` [#​3945](https://redirect.github.com/rust-lang/libc/pull/3945) - Illumos: add `ptsname_r` [#​3867](https://redirect.github.com/rust-lang/libc/pull/3867) - Linux: add `fanotify` interfaces [#​3695](https://redirect.github.com/rust-lang/libc/pull/3695) - Linux: add `tcp_info` [#​3480](https://redirect.github.com/rust-lang/libc/pull/3480) - Linux: add additional AF_PACKET options [#​3540](https://redirect.github.com/rust-lang/libc/pull/3540) - Linux: make Elf constants always available [#​3938](https://redirect.github.com/rust-lang/libc/pull/3938) - Musl x86: add `iopl` and `ioperm` [#​3720](https://redirect.github.com/rust-lang/libc/pull/3720) - Musl: add `posix_spawn` chdir functions [#​3949](https://redirect.github.com/rust-lang/libc/pull/3949) - Musl: add `utmpx.h` constants [#​3908](https://redirect.github.com/rust-lang/libc/pull/3908) - NetBSD: add `sysctlnametomib`, `CLOCK_THREAD_CPUTIME_ID` and `CLOCK_PROCESS_CPUTIME_ID` [#​3927](https://redirect.github.com/rust-lang/libc/pull/3927) - Nuttx: initial support [#​3909](https://redirect.github.com/rust-lang/libc/pull/3909) - RTEMS: add `getentropy` [#​3973](https://redirect.github.com/rust-lang/libc/pull/3973) - RTEMS: initial support [#​3866](https://redirect.github.com/rust-lang/libc/pull/3866) - Solarish: add `POLLRDHUP`, `POSIX_FADV_*`, `O_RSYNC`, and `posix_fallocate` [#​3936](https://redirect.github.com/rust-lang/libc/pull/3936) - Unix: add `fnmatch.h` [#​3937](https://redirect.github.com/rust-lang/libc/pull/3937) - VxWorks: add riscv64 support [#​3935](https://redirect.github.com/rust-lang/libc/pull/3935) - VxWorks: update constants related to the scheduler [#​3963](https://redirect.github.com/rust-lang/libc/pull/3963) ##### Changed - Redox: change `ino_t` to be `c_ulonglong` [#​3919](https://redirect.github.com/rust-lang/libc/pull/3919) ##### Fixed - ESP-IDF: fix mismatched constants and structs [#​3920](https://redirect.github.com/rust-lang/libc/pull/3920) - FreeBSD: fix `struct stat` on FreeBSD 12+ [#​3946](https://redirect.github.com/rust-lang/libc/pull/3946) ##### Other - CI: Fix CI for FreeBSD 15 [#​3950](https://redirect.github.com/rust-lang/libc/pull/3950) - Docs: link to `windows-sys` [#​3915](https://redirect.github.com/rust-lang/libc/pull/3915)
--- ### Configuration 📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone America/New_York, Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- Release Notes: - N/A Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b56d4861d3..e933bb8a03 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6448,9 +6448,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" [[package]] name = "libc" -version = "0.2.159" +version = "0.2.161" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" +checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" [[package]] name = "libdbus-sys" From 1af53040742a94569a7e0c4580ea136b889a6b24 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 1 Nov 2024 16:06:24 +0200 Subject: [PATCH 31/45] Update Rust crate flume to v0.11.1 (#19641) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [flume](https://redirect.github.com/zesterer/flume) | dependencies | patch | `0.11.0` -> `0.11.1` | --- ### Release Notes
zesterer/flume (flume) ### [`v0.11.1`](https://redirect.github.com/zesterer/flume/blob/HEAD/CHANGELOG.md#0111---2024-10-19) ##### Added - `SendSink::sender` - `SendFut`, `SendSink`, `RecvFut`, `RecvStream`, `WeakSender`, `Iter`, `TryIter`, and `IntoIter` now implement `Debug` - Docs now show required features ##### Removed ##### Changed - `WeakSender` is now `Clone` - `spin` feature no longer uses `std::thread::sleep` for locking except on Unix-like operating systems and Windows - Flume is now in [casual maintenance mode](https://casuallymaintained.tech/). ##### Fixed
--- ### Configuration 📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone America/New_York, Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- Release Notes: - N/A Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e933bb8a03..e46bcfbfa4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4388,9 +4388,9 @@ checksum = "8ce81f49ae8a0482e4c55ea62ebbd7e5a686af544c00b9d090bba3ff9be97b3d" [[package]] name = "flume" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" dependencies = [ "futures-core", "futures-sink", From 0e264b5a68ede6311abe03949e669188b96eff4c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 1 Nov 2024 16:07:36 +0200 Subject: [PATCH 32/45] Update cloudflare/wrangler-action digest to b2a0191 (#19645) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [cloudflare/wrangler-action](https://redirect.github.com/cloudflare/wrangler-action) | action | digest | `9681c29` -> `b2a0191` | --- ### Configuration 📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone America/New_York, Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- Release Notes: - N/A Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/deploy_cloudflare.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/deploy_cloudflare.yml b/.github/workflows/deploy_cloudflare.yml index e948eb64c3..d91eeb0f57 100644 --- a/.github/workflows/deploy_cloudflare.yml +++ b/.github/workflows/deploy_cloudflare.yml @@ -37,28 +37,28 @@ jobs: mdbook build ./docs --dest-dir=../target/deploy/docs/ - name: Deploy Docs - uses: cloudflare/wrangler-action@9681c2997648301493e78cacbfb790a9f19c833f # v3 + uses: cloudflare/wrangler-action@b2a0191ce60d21388e1a8dcc968b4e9966f938e1 # v3 with: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} command: pages deploy target/deploy --project-name=docs - name: Deploy Install - uses: cloudflare/wrangler-action@9681c2997648301493e78cacbfb790a9f19c833f # v3 + uses: cloudflare/wrangler-action@b2a0191ce60d21388e1a8dcc968b4e9966f938e1 # v3 with: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} command: r2 object put -f script/install.sh zed-open-source-website-assets/install.sh - name: Deploy Docs Workers - uses: cloudflare/wrangler-action@9681c2997648301493e78cacbfb790a9f19c833f # v3 + uses: cloudflare/wrangler-action@b2a0191ce60d21388e1a8dcc968b4e9966f938e1 # v3 with: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} command: deploy .cloudflare/docs-proxy/src/worker.js - name: Deploy Install Workers - uses: cloudflare/wrangler-action@9681c2997648301493e78cacbfb790a9f19c833f # v3 + uses: cloudflare/wrangler-action@b2a0191ce60d21388e1a8dcc968b4e9966f938e1 # v3 with: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} From 6de23302538d09f1a62ef37bdd91c954f0c0b4d0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 1 Nov 2024 16:10:09 +0200 Subject: [PATCH 33/45] Update Rust crate pathdiff to v0.2.2 (#19325) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [pathdiff](https://redirect.github.com/Manishearth/pathdiff) | workspace.dependencies | patch | `0.2.1` -> `0.2.2` | --- ### Configuration 📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone America/New_York, Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- Release Notes: - N/A Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e46bcfbfa4..4e789dec7e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7932,9 +7932,9 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "pathdiff" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" +checksum = "d61c5ce1153ab5b689d0c074c4e7fc613e942dfb7dd9eea5ab202d2ad91fe361" [[package]] name = "pathfinder_geometry" From cd8d776fe1527fe268061540eb5a17f7e25821ed Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 1 Nov 2024 16:10:21 +0200 Subject: [PATCH 34/45] Update Rust crate profiling to v1.0.16 (#19334) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [profiling](https://redirect.github.com/aclysma/profiling) | workspace.dependencies | patch | `1.0.15` -> `1.0.16` | --- ### Release Notes
aclysma/profiling (profiling) ### [`v1.0.16`](https://redirect.github.com/aclysma/profiling/blob/HEAD/CHANGELOG.md#1016) [Compare Source](https://redirect.github.com/aclysma/profiling/compare/v1.0.15...v1.0.16) - Address warnings from upstream rustc changes - Update puffin to 0.19.1 - Update tracing-tracy to 0.11.3 and tracing-subscriber to 0.3 - Implement finish_frame! for tracing - Add fuction_scope!() as an alternative to the function proc macro - Avoid local variable names that don't start with an underscore introduced into a function's namespace
--- ### Configuration 📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone America/New_York, Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- Release Notes: - N/A Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4e789dec7e..a5644e6813 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8818,18 +8818,18 @@ dependencies = [ [[package]] name = "profiling" -version = "1.0.15" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d84d1d7a6ac92673717f9f6d1518374ef257669c24ebc5ac25d5033828be58" +checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d" dependencies = [ "profiling-procmacros", ] [[package]] name = "profiling-procmacros" -version = "1.0.15" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd" +checksum = "a65f2e60fbf1063868558d69c6beacf412dc755f9fc020f514b7955fc914fe30" dependencies = [ "quote", "syn 2.0.76", From d3cd8f8f1484eb5e7ab5ec05e59a0ac936e9b6f5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 1 Nov 2024 16:10:31 +0200 Subject: [PATCH 35/45] Update Rust crate proc-macro2 to v1.0.89 (#19326) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [proc-macro2](https://redirect.github.com/dtolnay/proc-macro2) | dependencies | patch | `1.0.87` -> `1.0.89` | --- ### Release Notes
dtolnay/proc-macro2 (proc-macro2) ### [`v1.0.89`](https://redirect.github.com/dtolnay/proc-macro2/releases/tag/1.0.89) [Compare Source](https://redirect.github.com/dtolnay/proc-macro2/compare/1.0.88...1.0.89) - Ensure OUT_DIR is left with deterministic contents after build script execution ([#​474](https://redirect.github.com/dtolnay/proc-macro2/issues/474)) ### [`v1.0.88`](https://redirect.github.com/dtolnay/proc-macro2/releases/tag/1.0.88) [Compare Source](https://redirect.github.com/dtolnay/proc-macro2/compare/1.0.87...1.0.88) - Return accurate line and column from `Span::start` and `Span::end` inside proc macros on nightly ([#​472](https://redirect.github.com/dtolnay/proc-macro2/issues/472))
--- ### Configuration 📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone America/New_York, Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- Release Notes: - N/A Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a5644e6813..a3fcebfdb2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8809,9 +8809,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.87" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a" +checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" dependencies = [ "unicode-ident", ] From c04c439d231369a5aee3b96e6590ad52a71ef0b4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 1 Nov 2024 16:11:16 +0200 Subject: [PATCH 36/45] Update Rust crate async-compression to v0.4.17 (#19319) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [async-compression](https://redirect.github.com/Nullus157/async-compression) | workspace.dependencies | patch | `0.4.13` -> `0.4.17` | --- ### Release Notes
Nullus157/async-compression (async-compression) ### [`v0.4.17`](https://redirect.github.com/Nullus157/async-compression/blob/HEAD/CHANGELOG.md#0417---2024-10-20) [Compare Source](https://redirect.github.com/Nullus157/async-compression/compare/v0.4.16...v0.4.17) ##### Fixed - Fix occasional panics when consuming from pending buffers. ### [`v0.4.16`](https://redirect.github.com/Nullus157/async-compression/blob/HEAD/CHANGELOG.md#0416---2024-10-16) [Compare Source](https://redirect.github.com/Nullus157/async-compression/compare/v0.4.15...v0.4.16) ##### Other - Implement pass-through `AsyncBufRead` on write-based encoders & decoders. ### [`v0.4.15`](https://redirect.github.com/Nullus157/async-compression/blob/HEAD/CHANGELOG.md#0415---2024-10-13) [Compare Source](https://redirect.github.com/Nullus157/async-compression/compare/v0.4.14...v0.4.15) ##### Feature - Implement pass-through `AsyncRead` or `AsyncWrite` where appropriate. - Relax `AsyncRead`/`AsyncWrite` bounds on `*::{get_ref, get_mut, get_pin_mut, into_inner}()` methods. ### [`v0.4.14`](https://redirect.github.com/Nullus157/async-compression/blob/HEAD/CHANGELOG.md#0414---2024-10-10) [Compare Source](https://redirect.github.com/Nullus157/async-compression/compare/v0.4.13...v0.4.14) ##### Fixed - In Tokio-based decoders, attempt to decode from internal state even if nothing was read.
--- ### Configuration 📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone America/New_York, Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- Release Notes: - N/A Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a3fcebfdb2..e8a9a3d0ed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -544,9 +544,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.13" +version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e614738943d3f68c628ae3dbce7c3daffb196665f82f8c8ea6b65de73c79429" +checksum = "0cb8f1d480b0ea3783ab015936d2a55c87e219676f0c0b7dec61494043f21857" dependencies = [ "deflate64", "flate2", From af9e7f1f96b33011e3fbb865b2e55868346e2071 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 1 Nov 2024 10:19:09 -0400 Subject: [PATCH 37/45] theme: Turn `ThemeRegistry` into a trait (#20076) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR converts the `ThemeRegistry` type into a trait instead of a concrete implementation. This allows for the extension store to depend on an abstraction rather than the concrete theme registry implementation. We currently have two `ThemeRegistry` implementations: - `RealThemeRegistry` — this was previously the `ThemeRegistry` and contains the real implementation of the registry. - `VoidThemeRegistry` — a null object that doesn't have any behavior. Release Notes: - N/A --- Cargo.lock | 1 + crates/extension/src/extension_store.rs | 8 +- crates/extension/src/extension_store_test.rs | 6 +- crates/extension_cli/src/main.rs | 3 +- .../src/appearance_settings_controls.rs | 2 +- crates/storybook/src/storybook.rs | 2 +- crates/theme/Cargo.toml | 1 + crates/theme/src/registry.rs | 205 +++++++++++------- crates/theme/src/settings.rs | 8 +- crates/theme/src/theme.rs | 46 +++- crates/theme_selector/src/theme_selector.rs | 4 +- crates/zed/src/main.rs | 12 +- crates/zed/src/zed.rs | 4 +- 13 files changed, 184 insertions(+), 118 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e8a9a3d0ed..e268bd1174 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12078,6 +12078,7 @@ name = "theme" version = "0.1.0" dependencies = [ "anyhow", + "async-trait", "collections", "derive_more", "fs", diff --git a/crates/extension/src/extension_store.rs b/crates/extension/src/extension_store.rs index 0a9299a8be..f9b4523cac 100644 --- a/crates/extension/src/extension_store.rs +++ b/crates/extension/src/extension_store.rs @@ -114,7 +114,7 @@ pub struct ExtensionStore { outstanding_operations: BTreeMap, ExtensionOperation>, index_path: PathBuf, language_registry: Arc, - theme_registry: Arc, + theme_registry: Arc, slash_command_registry: Arc, indexed_docs_registry: Arc, snippet_registry: Arc, @@ -179,7 +179,7 @@ pub fn init( client: Arc, node_runtime: NodeRuntime, language_registry: Arc, - theme_registry: Arc, + theme_registry: Arc, cx: &mut AppContext, ) { ExtensionSettings::register(cx); @@ -230,7 +230,7 @@ impl ExtensionStore { telemetry: Option>, node_runtime: NodeRuntime, language_registry: Arc, - theme_registry: Arc, + theme_registry: Arc, slash_command_registry: Arc, indexed_docs_registry: Arc, snippet_registry: Arc, @@ -1358,7 +1358,7 @@ impl ExtensionStore { continue; }; - let Some(theme_family) = ThemeRegistry::read_user_theme(&theme_path, fs.clone()) + let Some(theme_family) = theme::read_user_theme(&theme_path, fs.clone()) .await .log_err() else { diff --git a/crates/extension/src/extension_store_test.rs b/crates/extension/src/extension_store_test.rs index 1274fafc3c..08dce1a98e 100644 --- a/crates/extension/src/extension_store_test.rs +++ b/crates/extension/src/extension_store_test.rs @@ -27,7 +27,7 @@ use std::{ path::{Path, PathBuf}, sync::Arc, }; -use theme::ThemeRegistry; +use theme::{RealThemeRegistry, ThemeRegistry}; use util::test::temp_tree; #[cfg(test)] @@ -260,7 +260,7 @@ async fn test_extension_store(cx: &mut TestAppContext) { }; let language_registry = Arc::new(LanguageRegistry::test(cx.executor())); - let theme_registry = Arc::new(ThemeRegistry::new(Box::new(()))); + let theme_registry = Arc::new(RealThemeRegistry::new(Box::new(()))); let slash_command_registry = SlashCommandRegistry::new(); let indexed_docs_registry = Arc::new(IndexedDocsRegistry::new(cx.executor())); let snippet_registry = Arc::new(SnippetRegistry::new()); @@ -486,7 +486,7 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) { let project = Project::test(fs.clone(), [project_dir.as_path()], cx).await; let language_registry = project.read_with(cx, |project, _cx| project.languages().clone()); - let theme_registry = Arc::new(ThemeRegistry::new(Box::new(()))); + let theme_registry = Arc::new(RealThemeRegistry::new(Box::new(()))); let slash_command_registry = SlashCommandRegistry::new(); let indexed_docs_registry = Arc::new(IndexedDocsRegistry::new(cx.executor())); let snippet_registry = Arc::new(SnippetRegistry::new()); diff --git a/crates/extension_cli/src/main.rs b/crates/extension_cli/src/main.rs index dd6f221378..ffa9555c21 100644 --- a/crates/extension_cli/src/main.rs +++ b/crates/extension_cli/src/main.rs @@ -15,7 +15,6 @@ use extension::{ }; use language::LanguageConfig; use reqwest_client::ReqwestClient; -use theme::ThemeRegistry; use tree_sitter::{Language, Query, WasmStore}; #[derive(Parser, Debug)] @@ -267,7 +266,7 @@ async fn test_themes( ) -> Result<()> { for relative_theme_path in &manifest.themes { let theme_path = extension_path.join(relative_theme_path); - let theme_family = ThemeRegistry::read_user_theme(&theme_path, fs.clone()).await?; + let theme_family = theme::read_user_theme(&theme_path, fs.clone()).await?; log::info!("loaded theme family {}", theme_family.name); } diff --git a/crates/settings_ui/src/appearance_settings_controls.rs b/crates/settings_ui/src/appearance_settings_controls.rs index c7686761c4..00dd8a4c01 100644 --- a/crates/settings_ui/src/appearance_settings_controls.rs +++ b/crates/settings_ui/src/appearance_settings_controls.rs @@ -83,7 +83,7 @@ impl RenderOnce for ThemeControl { "theme", value.clone(), ContextMenu::build(cx, |mut menu, cx| { - let theme_registry = ThemeRegistry::global(cx); + let theme_registry = ::global(cx); for theme in theme_registry.list_names(false) { menu = menu.custom_entry( diff --git a/crates/storybook/src/storybook.rs b/crates/storybook/src/storybook.rs index 9fe61a70c4..d751a5ed03 100644 --- a/crates/storybook/src/storybook.rs +++ b/crates/storybook/src/storybook.rs @@ -76,7 +76,7 @@ fn main() { let selector = story_selector; - let theme_registry = ThemeRegistry::global(cx); + let theme_registry = ::global(cx); let mut theme_settings = ThemeSettings::get_global(cx).clone(); theme_settings.active_theme = theme_registry.get(&theme_name).unwrap(); ThemeSettings::override_global(theme_settings, cx); diff --git a/crates/theme/Cargo.toml b/crates/theme/Cargo.toml index c3e3a197cb..b4fe396793 100644 --- a/crates/theme/Cargo.toml +++ b/crates/theme/Cargo.toml @@ -18,6 +18,7 @@ doctest = false [dependencies] anyhow.workspace = true +async-trait.workspace = true collections.workspace = true derive_more.workspace = true fs.workspace = true diff --git a/crates/theme/src/registry.rs b/crates/theme/src/registry.rs index 73e8fe8c66..38aadf6ece 100644 --- a/crates/theme/src/registry.rs +++ b/crates/theme/src/registry.rs @@ -1,7 +1,8 @@ use std::sync::Arc; use std::{fmt::Debug, path::Path}; -use anyhow::{anyhow, Context, Result}; +use anyhow::{anyhow, bail, Context, Result}; +use async_trait::async_trait; use collections::HashMap; use derive_more::{Deref, DerefMut}; use fs::Fs; @@ -10,7 +11,9 @@ use gpui::{AppContext, AssetSource, Global, SharedString}; use parking_lot::RwLock; use util::ResultExt; -use crate::{refine_theme_family, Appearance, Theme, ThemeFamily, ThemeFamilyContent}; +use crate::{ + read_user_theme, refine_theme_family, Appearance, Theme, ThemeFamily, ThemeFamilyContent, +}; /// The metadata for a theme. #[derive(Debug, Clone)] @@ -27,22 +30,34 @@ pub struct ThemeMeta { /// inserting the [`ThemeRegistry`] into the context as a global. /// /// This should not be exposed outside of this module. -#[derive(Default, Deref, DerefMut)] -struct GlobalThemeRegistry(Arc); +#[derive(Deref, DerefMut)] +struct GlobalThemeRegistry(Arc); impl Global for GlobalThemeRegistry {} -struct ThemeRegistryState { - themes: HashMap>, +/// A registry for themes. +#[async_trait] +pub trait ThemeRegistry: Send + Sync + 'static { + /// Returns the names of all themes in the registry. + fn list_names(&self, _staff: bool) -> Vec; + + /// Returns the metadata of all themes in the registry. + fn list(&self, _staff: bool) -> Vec; + + /// Returns the theme with the given name. + fn get(&self, name: &str) -> Result>; + + /// Loads the user theme from the specified path and adds it to the registry. + async fn load_user_theme(&self, theme_path: &Path, fs: Arc) -> Result<()>; + + /// Loads the user themes from the specified directory and adds them to the registry. + async fn load_user_themes(&self, themes_path: &Path, fs: Arc) -> Result<()>; + + /// Removes the themes with the given names from the registry. + fn remove_user_themes(&self, themes_to_remove: &[SharedString]); } -/// The registry for themes. -pub struct ThemeRegistry { - state: RwLock, - assets: Box, -} - -impl ThemeRegistry { +impl dyn ThemeRegistry { /// Returns the global [`ThemeRegistry`]. pub fn global(cx: &AppContext) -> Arc { cx.global::().0.clone() @@ -52,18 +67,37 @@ impl ThemeRegistry { /// /// Inserts a default [`ThemeRegistry`] if one does not yet exist. pub fn default_global(cx: &mut AppContext) -> Arc { - cx.default_global::().0.clone() - } + if let Some(registry) = cx.try_global::() { + return registry.0.clone(); + } + let registry = Arc::new(RealThemeRegistry::default()); + cx.set_global(GlobalThemeRegistry(registry.clone())); + + registry + } +} + +struct RealThemeRegistryState { + themes: HashMap>, +} + +/// The registry for themes. +pub struct RealThemeRegistry { + state: RwLock, + assets: Box, +} + +impl RealThemeRegistry { /// Sets the global [`ThemeRegistry`]. - pub(crate) fn set_global(assets: Box, cx: &mut AppContext) { - cx.set_global(GlobalThemeRegistry(Arc::new(ThemeRegistry::new(assets)))); + pub(crate) fn set_global(self: Arc, cx: &mut AppContext) { + cx.set_global(GlobalThemeRegistry(self)); } /// Creates a new [`ThemeRegistry`] with the given [`AssetSource`]. pub fn new(assets: Box) -> Self { let registry = Self { - state: RwLock::new(ThemeRegistryState { + state: RwLock::new(RealThemeRegistryState { themes: HashMap::default(), }), assets, @@ -98,49 +132,11 @@ impl ThemeRegistry { } } - /// Removes the themes with the given names from the registry. - pub fn remove_user_themes(&self, themes_to_remove: &[SharedString]) { - self.state - .write() - .themes - .retain(|name, _| !themes_to_remove.contains(name)) - } - /// Removes all themes from the registry. pub fn clear(&self) { self.state.write().themes.clear(); } - /// Returns the names of all themes in the registry. - pub fn list_names(&self, _staff: bool) -> Vec { - let mut names = self.state.read().themes.keys().cloned().collect::>(); - names.sort(); - names - } - - /// Returns the metadata of all themes in the registry. - pub fn list(&self, _staff: bool) -> Vec { - self.state - .read() - .themes - .values() - .map(|theme| ThemeMeta { - name: theme.name.clone(), - appearance: theme.appearance(), - }) - .collect() - } - - /// Returns the theme with the given name. - pub fn get(&self, name: &str) -> Result> { - self.state - .read() - .themes - .get(name) - .ok_or_else(|| anyhow!("theme not found: {}", name)) - .cloned() - } - /// Loads the themes bundled with the Zed binary and adds them to the registry. pub fn load_bundled_themes(&self) { let theme_paths = self @@ -165,9 +161,52 @@ impl ThemeRegistry { self.insert_user_theme_families([theme_family]); } } +} - /// Loads the user themes from the specified directory and adds them to the registry. - pub async fn load_user_themes(&self, themes_path: &Path, fs: Arc) -> Result<()> { +impl Default for RealThemeRegistry { + fn default() -> Self { + Self::new(Box::new(())) + } +} + +#[async_trait] +impl ThemeRegistry for RealThemeRegistry { + fn list_names(&self, _staff: bool) -> Vec { + let mut names = self.state.read().themes.keys().cloned().collect::>(); + names.sort(); + names + } + + fn list(&self, _staff: bool) -> Vec { + self.state + .read() + .themes + .values() + .map(|theme| ThemeMeta { + name: theme.name.clone(), + appearance: theme.appearance(), + }) + .collect() + } + + fn get(&self, name: &str) -> Result> { + self.state + .read() + .themes + .get(name) + .ok_or_else(|| anyhow!("theme not found: {}", name)) + .cloned() + } + + async fn load_user_theme(&self, theme_path: &Path, fs: Arc) -> Result<()> { + let theme = read_user_theme(theme_path, fs).await?; + + self.insert_user_theme_families([theme]); + + Ok(()) + } + + async fn load_user_themes(&self, themes_path: &Path, fs: Arc) -> Result<()> { let mut theme_paths = fs .read_dir(themes_path) .await @@ -186,40 +225,38 @@ impl ThemeRegistry { Ok(()) } - /// Asynchronously reads the user theme from the specified path. - pub async fn read_user_theme(theme_path: &Path, fs: Arc) -> Result { - let reader = fs.open_sync(theme_path).await?; - let theme_family: ThemeFamilyContent = serde_json_lenient::from_reader(reader)?; + fn remove_user_themes(&self, themes_to_remove: &[SharedString]) { + self.state + .write() + .themes + .retain(|name, _| !themes_to_remove.contains(name)) + } +} - for theme in &theme_family.themes { - if theme - .style - .colors - .deprecated_scrollbar_thumb_background - .is_some() - { - log::warn!( - r#"Theme "{theme_name}" is using a deprecated style property: scrollbar_thumb.background. Use `scrollbar.thumb.background` instead."#, - theme_name = theme.name - ) - } - } +/// A theme registry that doesn't have any behavior. +pub struct VoidThemeRegistry; - Ok(theme_family) +#[async_trait] +impl ThemeRegistry for VoidThemeRegistry { + fn list_names(&self, _staff: bool) -> Vec { + Vec::new() } - /// Loads the user theme from the specified path and adds it to the registry. - pub async fn load_user_theme(&self, theme_path: &Path, fs: Arc) -> Result<()> { - let theme = Self::read_user_theme(theme_path, fs).await?; + fn list(&self, _staff: bool) -> Vec { + Vec::new() + } - self.insert_user_theme_families([theme]); + fn get(&self, name: &str) -> Result> { + bail!("cannot retrieve theme {name:?} from a void theme registry") + } + async fn load_user_theme(&self, _theme_path: &Path, _fs: Arc) -> Result<()> { Ok(()) } -} -impl Default for ThemeRegistry { - fn default() -> Self { - Self::new(Box::new(())) + async fn load_user_themes(&self, _themes_path: &Path, _fs: Arc) -> Result<()> { + Ok(()) } + + fn remove_user_themes(&self, _themes_to_remove: &[SharedString]) {} } diff --git a/crates/theme/src/settings.rs b/crates/theme/src/settings.rs index 4d8158388c..fdae092c22 100644 --- a/crates/theme/src/settings.rs +++ b/crates/theme/src/settings.rs @@ -150,7 +150,7 @@ impl ThemeSettings { // If the selected theme doesn't exist, fall back to a default theme // based on the system appearance. - let theme_registry = ThemeRegistry::global(cx); + let theme_registry = ::global(cx); if theme_registry.get(theme_name).ok().is_none() { theme_name = Self::default_theme(*system_appearance); }; @@ -446,7 +446,7 @@ impl ThemeSettings { /// Returns a `Some` containing the new theme if it was successful. /// Returns `None` otherwise. pub fn switch_theme(&mut self, theme: &str, cx: &mut AppContext) -> Option> { - let themes = ThemeRegistry::default_global(cx); + let themes = ::default_global(cx); let mut new_theme = None; @@ -598,7 +598,7 @@ impl settings::Settings for ThemeSettings { type FileContent = ThemeSettingsContent; fn load(sources: SettingsSources, cx: &mut AppContext) -> Result { - let themes = ThemeRegistry::default_global(cx); + let themes = ::default_global(cx); let system_appearance = SystemAppearance::default_global(cx); let defaults = sources.default; @@ -710,7 +710,7 @@ impl settings::Settings for ThemeSettings { cx: &AppContext, ) -> schemars::schema::RootSchema { let mut root_schema = generator.root_schema_for::(); - let theme_names = ThemeRegistry::global(cx) + let theme_names = ::global(cx) .list_names(params.staff_mode) .into_iter() .map(|theme_name| Value::String(theme_name.to_string())) diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 307ea6b287..103ad68486 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -17,17 +17,12 @@ mod schema; mod settings; mod styles; +use std::path::Path; use std::sync::Arc; use ::settings::{Settings, SettingsStore}; -pub use default_colors::*; -pub use font_family_cache::*; -pub use registry::*; -pub use scale::*; -pub use schema::*; -pub use settings::*; -pub use styles::*; - +use anyhow::Result; +use fs::Fs; use gpui::{ px, AppContext, AssetSource, HighlightStyle, Hsla, Pixels, Refineable, SharedString, WindowAppearance, WindowBackgroundAppearance, @@ -35,6 +30,14 @@ use gpui::{ use serde::Deserialize; use uuid::Uuid; +pub use crate::default_colors::*; +pub use crate::font_family_cache::*; +pub use crate::registry::*; +pub use crate::scale::*; +pub use crate::schema::*; +pub use crate::settings::*; +pub use crate::styles::*; + /// Defines window border radius for platforms that use client side decorations. pub const CLIENT_SIDE_DECORATION_ROUNDING: Pixels = px(10.0); /// Defines window shadow size for platforms that use client side decorations. @@ -85,10 +88,11 @@ pub fn init(themes_to_load: LoadThemes, cx: &mut AppContext) { LoadThemes::JustBase => (Box::new(()) as Box, false), LoadThemes::All(assets) => (assets, true), }; - ThemeRegistry::set_global(assets, cx); + let registry = Arc::new(RealThemeRegistry::new(assets)); + registry.clone().set_global(cx); if load_user_themes { - ThemeRegistry::global(cx).load_bundled_themes(); + registry.load_bundled_themes(); } ThemeSettings::register(cx); @@ -321,3 +325,25 @@ pub fn color_alpha(color: Hsla, alpha: f32) -> Hsla { color.a = alpha; color } + +/// Asynchronously reads the user theme from the specified path. +pub async fn read_user_theme(theme_path: &Path, fs: Arc) -> Result { + let reader = fs.open_sync(theme_path).await?; + let theme_family: ThemeFamilyContent = serde_json_lenient::from_reader(reader)?; + + for theme in &theme_family.themes { + if theme + .style + .colors + .deprecated_scrollbar_thumb_background + .is_some() + { + log::warn!( + r#"Theme "{theme_name}" is using a deprecated style property: scrollbar_thumb.background. Use `scrollbar.thumb.background` instead."#, + theme_name = theme.name + ) + } + } + + Ok(theme_family) +} diff --git a/crates/theme_selector/src/theme_selector.rs b/crates/theme_selector/src/theme_selector.rs index 2ce2b9ba89..47750c0ea0 100644 --- a/crates/theme_selector/src/theme_selector.rs +++ b/crates/theme_selector/src/theme_selector.rs @@ -97,7 +97,7 @@ impl ThemeSelectorDelegate { let original_theme = cx.theme().clone(); let staff_mode = cx.is_staff(); - let registry = ThemeRegistry::global(cx); + let registry = ::global(cx); let mut themes = registry .list(staff_mode) .into_iter() @@ -142,7 +142,7 @@ impl ThemeSelectorDelegate { fn show_selected_theme(&mut self, cx: &mut ViewContext>) { if let Some(mat) = self.matches.get(self.selected_index) { - let registry = ThemeRegistry::global(cx); + let registry = ::global(cx); match registry.get(&mat.string) { Ok(theme) => { Self::set_theme(theme, cx); diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index f366323ff5..688e217fb4 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -407,7 +407,7 @@ fn main() { app_state.client.clone(), app_state.node_runtime.clone(), app_state.languages.clone(), - ThemeRegistry::global(cx), + ::global(cx), cx, ); recent_projects::init(cx); @@ -1160,8 +1160,9 @@ fn load_user_themes_in_background(fs: Arc, cx: &mut AppContext) { cx.spawn({ let fs = fs.clone(); |cx| async move { - if let Some(theme_registry) = - cx.update(|cx| ThemeRegistry::global(cx).clone()).log_err() + if let Some(theme_registry) = cx + .update(|cx| ::global(cx).clone()) + .log_err() { let themes_dir = paths::themes_dir().as_ref(); match fs @@ -1200,8 +1201,9 @@ fn watch_themes(fs: Arc, cx: &mut AppContext) { while let Some(paths) = events.next().await { for event in paths { if fs.metadata(&event.path).await.ok().flatten().is_some() { - if let Some(theme_registry) = - cx.update(|cx| ThemeRegistry::global(cx).clone()).log_err() + if let Some(theme_registry) = cx + .update(|cx| ::global(cx).clone()) + .log_err() { if let Some(()) = theme_registry .load_user_theme(&event.path, fs.clone()) diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index bbe24bdaaf..c3d23572e2 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -1139,7 +1139,7 @@ mod tests { path::{Path, PathBuf}, time::Duration, }; - use theme::{ThemeRegistry, ThemeSettings}; + use theme::{RealThemeRegistry, ThemeRegistry, ThemeSettings}; use workspace::{ item::{Item, ItemHandle}, open_new, open_paths, pane, NewFile, OpenVisible, SaveIntent, SplitDirection, @@ -3419,7 +3419,7 @@ mod tests { .unwrap(), ]) .unwrap(); - let themes = ThemeRegistry::default(); + let themes = RealThemeRegistry::default(); settings::init(cx); theme::init(theme::LoadThemes::JustBase, cx); From a9603443019cfdcee9ad061f66104235b24f0560 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 1 Nov 2024 10:54:21 -0400 Subject: [PATCH 38/45] theme: Remove unused `staff` parameter for listing themes (#20077) This PR removes the `staff` parameter for listing themes, as it was not used. Release Notes: - N/A --- Cargo.lock | 2 -- crates/extension/src/extension_store_test.rs | 6 +++--- crates/languages/Cargo.toml | 1 - crates/languages/src/json.rs | 3 --- crates/settings/src/json_schema.rs | 1 - .../settings_ui/src/appearance_settings_controls.rs | 2 +- crates/theme/src/registry.rs | 12 ++++++------ crates/theme/src/settings.rs | 2 +- crates/theme_selector/Cargo.toml | 1 - crates/theme_selector/src/theme_selector.rs | 4 +--- crates/zed/src/zed.rs | 2 +- 11 files changed, 13 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e268bd1174..8e0cffd249 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6371,7 +6371,6 @@ dependencies = [ "async-tar", "async-trait", "collections", - "feature_flags", "futures 0.3.30", "gpui", "http_client", @@ -12127,7 +12126,6 @@ name = "theme_selector" version = "0.1.0" dependencies = [ "client", - "feature_flags", "fs", "fuzzy", "gpui", diff --git a/crates/extension/src/extension_store_test.rs b/crates/extension/src/extension_store_test.rs index 08dce1a98e..f14707c3ba 100644 --- a/crates/extension/src/extension_store_test.rs +++ b/crates/extension/src/extension_store_test.rs @@ -296,7 +296,7 @@ async fn test_extension_store(cx: &mut TestAppContext) { ["ERB", "Plain Text", "Ruby"] ); assert_eq!( - theme_registry.list_names(false), + theme_registry.list_names(), [ "Monokai Dark", "Monokai Light", @@ -377,7 +377,7 @@ async fn test_extension_store(cx: &mut TestAppContext) { assert_eq!(index.themes, expected_index.themes); assert_eq!( - theme_registry.list_names(false), + theme_registry.list_names(), [ "Gruvbox", "Monokai Dark", @@ -424,7 +424,7 @@ async fn test_extension_store(cx: &mut TestAppContext) { ["embedded_template".into(), "ruby".into()] ); assert_eq!( - theme_registry.list_names(false), + theme_registry.list_names(), [ "Gruvbox", "Monokai Dark", diff --git a/crates/languages/Cargo.toml b/crates/languages/Cargo.toml index 29c52ba301..d76dd5a327 100644 --- a/crates/languages/Cargo.toml +++ b/crates/languages/Cargo.toml @@ -38,7 +38,6 @@ async-compression.workspace = true async-tar.workspace = true async-trait.workspace = true collections.workspace = true -feature_flags.workspace = true futures.workspace = true gpui.workspace = true http_client.workspace = true diff --git a/crates/languages/src/json.rs b/crates/languages/src/json.rs index 28ee884307..d65b30ac79 100644 --- a/crates/languages/src/json.rs +++ b/crates/languages/src/json.rs @@ -3,7 +3,6 @@ use async_compression::futures::bufread::GzipDecoder; use async_tar::Archive; use async_trait::async_trait; use collections::HashMap; -use feature_flags::FeatureFlagAppExt; use futures::StreamExt; use gpui::{AppContext, AsyncAppContext}; use http_client::github::{latest_github_release, GitHubLspBinaryVersion}; @@ -77,13 +76,11 @@ impl JsonLspAdapter { fn get_workspace_config(language_names: Vec, cx: &mut AppContext) -> Value { let action_names = cx.all_action_names(); - let staff_mode = cx.is_staff(); let font_names = &cx.text_system().all_font_names(); let settings_schema = cx.global::().json_schema( &SettingsJsonSchemaParams { language_names: &language_names, - staff_mode, font_names, }, cx, diff --git a/crates/settings/src/json_schema.rs b/crates/settings/src/json_schema.rs index 6ee634a3b5..dd01a96c31 100644 --- a/crates/settings/src/json_schema.rs +++ b/crates/settings/src/json_schema.rs @@ -2,7 +2,6 @@ use schemars::schema::{ArrayValidation, InstanceType, RootSchema, Schema, Schema use serde_json::Value; pub struct SettingsJsonSchemaParams<'a> { - pub staff_mode: bool, pub language_names: &'a [String], pub font_names: &'a [String], } diff --git a/crates/settings_ui/src/appearance_settings_controls.rs b/crates/settings_ui/src/appearance_settings_controls.rs index 00dd8a4c01..70e8e78cb4 100644 --- a/crates/settings_ui/src/appearance_settings_controls.rs +++ b/crates/settings_ui/src/appearance_settings_controls.rs @@ -85,7 +85,7 @@ impl RenderOnce for ThemeControl { ContextMenu::build(cx, |mut menu, cx| { let theme_registry = ::global(cx); - for theme in theme_registry.list_names(false) { + for theme in theme_registry.list_names() { menu = menu.custom_entry( { let theme = theme.clone(); diff --git a/crates/theme/src/registry.rs b/crates/theme/src/registry.rs index 38aadf6ece..a78a22b08f 100644 --- a/crates/theme/src/registry.rs +++ b/crates/theme/src/registry.rs @@ -39,10 +39,10 @@ impl Global for GlobalThemeRegistry {} #[async_trait] pub trait ThemeRegistry: Send + Sync + 'static { /// Returns the names of all themes in the registry. - fn list_names(&self, _staff: bool) -> Vec; + fn list_names(&self) -> Vec; /// Returns the metadata of all themes in the registry. - fn list(&self, _staff: bool) -> Vec; + fn list(&self) -> Vec; /// Returns the theme with the given name. fn get(&self, name: &str) -> Result>; @@ -171,13 +171,13 @@ impl Default for RealThemeRegistry { #[async_trait] impl ThemeRegistry for RealThemeRegistry { - fn list_names(&self, _staff: bool) -> Vec { + fn list_names(&self) -> Vec { let mut names = self.state.read().themes.keys().cloned().collect::>(); names.sort(); names } - fn list(&self, _staff: bool) -> Vec { + fn list(&self) -> Vec { self.state .read() .themes @@ -238,11 +238,11 @@ pub struct VoidThemeRegistry; #[async_trait] impl ThemeRegistry for VoidThemeRegistry { - fn list_names(&self, _staff: bool) -> Vec { + fn list_names(&self) -> Vec { Vec::new() } - fn list(&self, _staff: bool) -> Vec { + fn list(&self) -> Vec { Vec::new() } diff --git a/crates/theme/src/settings.rs b/crates/theme/src/settings.rs index fdae092c22..81e1958029 100644 --- a/crates/theme/src/settings.rs +++ b/crates/theme/src/settings.rs @@ -711,7 +711,7 @@ impl settings::Settings for ThemeSettings { ) -> schemars::schema::RootSchema { let mut root_schema = generator.root_schema_for::(); let theme_names = ::global(cx) - .list_names(params.staff_mode) + .list_names() .into_iter() .map(|theme_name| Value::String(theme_name.to_string())) .collect(); diff --git a/crates/theme_selector/Cargo.toml b/crates/theme_selector/Cargo.toml index 42087f2704..ec7e9aa877 100644 --- a/crates/theme_selector/Cargo.toml +++ b/crates/theme_selector/Cargo.toml @@ -14,7 +14,6 @@ doctest = false [dependencies] client.workspace = true -feature_flags.workspace = true fs.workspace = true fuzzy.workspace = true gpui.workspace = true diff --git a/crates/theme_selector/src/theme_selector.rs b/crates/theme_selector/src/theme_selector.rs index 47750c0ea0..781a930607 100644 --- a/crates/theme_selector/src/theme_selector.rs +++ b/crates/theme_selector/src/theme_selector.rs @@ -1,5 +1,4 @@ use client::telemetry::Telemetry; -use feature_flags::FeatureFlagAppExt; use fs::Fs; use fuzzy::{match_strings, StringMatch, StringMatchCandidate}; use gpui::{ @@ -96,10 +95,9 @@ impl ThemeSelectorDelegate { ) -> Self { let original_theme = cx.theme().clone(); - let staff_mode = cx.is_staff(); let registry = ::global(cx); let mut themes = registry - .list(staff_mode) + .list() .into_iter() .filter(|meta| { if let Some(theme_filter) = themes_filter { diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index c3d23572e2..175497ffd7 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -3424,7 +3424,7 @@ mod tests { theme::init(theme::LoadThemes::JustBase, cx); let mut has_default_theme = false; - for theme_name in themes.list(false).into_iter().map(|meta| meta.name) { + for theme_name in themes.list().into_iter().map(|meta| meta.name) { let theme = themes.get(&theme_name).unwrap(); assert_eq!(theme.name, theme_name); if theme.name == ThemeSettings::get(None, cx).active_theme.name { From c8f1969916ad3f437a41b65e7e0ea52596821d2a Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 1 Nov 2024 18:01:10 +0200 Subject: [PATCH 39/45] Fix the outline panel's focus tracking (#20083) Closes https://github.com/zed-industries/zed/issues/20073 Release Notes: - Fixed outline panel navigation ([https://github.com/zed-industries/zed/issues/20073](#20073)) Co-authored-by: Mikayla Maki --- crates/outline_panel/src/outline_panel.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index 7de8872c64..a7708ec08f 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -4261,7 +4261,7 @@ impl Render for OutlinePanel { } }), ) - .track_focus(&self.focus_handle(cx)) + .track_focus(&self.focus_handle) .when_some(search_query, |outline_panel, search_state| { outline_panel.child( v_flex() From ea44c510a3d696aaba0189b55efde81514b02ed9 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 1 Nov 2024 12:53:02 -0400 Subject: [PATCH 40/45] Rename `extension` crate to `extension_host` (#20081) This PR renames the `extension` crate to `extension_host`. This is to free up the name so that we can create a smaller-scoped `extension` crate. Release Notes: - N/A --- Cargo.lock | 52 +++++++++---------- Cargo.toml | 4 +- crates/activity_indicator/Cargo.toml | 2 +- .../src/activity_indicator.rs | 2 +- crates/extension_cli/Cargo.toml | 2 +- crates/extension_cli/src/main.rs | 2 +- .../{extension => extension_host}/Cargo.toml | 4 +- .../{extension => extension_host}/LICENSE-GPL | 0 crates/{extension => extension_host}/build.rs | 0 .../src/extension_builder.rs | 0 .../src/extension_host.rs} | 0 .../src/extension_indexed_docs_provider.rs | 0 .../src/extension_lsp_adapter.rs | 0 .../src/extension_manifest.rs | 0 .../src/extension_settings.rs | 0 .../src/extension_slash_command.rs | 0 .../src/extension_store_test.rs | 0 .../src/wasm_host.rs | 0 .../src/wasm_host/wit.rs | 0 .../src/wasm_host/wit/since_v0_0_1.rs | 0 .../src/wasm_host/wit/since_v0_0_4.rs | 0 .../src/wasm_host/wit/since_v0_0_6.rs | 0 .../src/wasm_host/wit/since_v0_1_0.rs | 0 .../src/wasm_host/wit/since_v0_2_0.rs | 0 crates/extensions_ui/Cargo.toml | 2 +- crates/extensions_ui/src/extension_suggest.rs | 2 +- .../src/extension_version_selector.rs | 6 +-- crates/extensions_ui/src/extensions_ui.rs | 9 ++-- crates/zed/Cargo.toml | 2 +- crates/zed/src/main.rs | 2 +- 30 files changed, 46 insertions(+), 45 deletions(-) rename crates/{extension => extension_host}/Cargo.toml (96%) rename crates/{extension => extension_host}/LICENSE-GPL (100%) rename crates/{extension => extension_host}/build.rs (100%) rename crates/{extension => extension_host}/src/extension_builder.rs (100%) rename crates/{extension/src/extension_store.rs => extension_host/src/extension_host.rs} (100%) rename crates/{extension => extension_host}/src/extension_indexed_docs_provider.rs (100%) rename crates/{extension => extension_host}/src/extension_lsp_adapter.rs (100%) rename crates/{extension => extension_host}/src/extension_manifest.rs (100%) rename crates/{extension => extension_host}/src/extension_settings.rs (100%) rename crates/{extension => extension_host}/src/extension_slash_command.rs (100%) rename crates/{extension => extension_host}/src/extension_store_test.rs (100%) rename crates/{extension => extension_host}/src/wasm_host.rs (100%) rename crates/{extension => extension_host}/src/wasm_host/wit.rs (100%) rename crates/{extension => extension_host}/src/wasm_host/wit/since_v0_0_1.rs (100%) rename crates/{extension => extension_host}/src/wasm_host/wit/since_v0_0_4.rs (100%) rename crates/{extension => extension_host}/src/wasm_host/wit/since_v0_0_6.rs (100%) rename crates/{extension => extension_host}/src/wasm_host/wit/since_v0_1_0.rs (100%) rename crates/{extension => extension_host}/src/wasm_host/wit/since_v0_2_0.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index 8e0cffd249..c720155429 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9,7 +9,7 @@ dependencies = [ "anyhow", "auto_update", "editor", - "extension", + "extension_host", "futures 0.3.30", "gpui", "language", @@ -4090,7 +4090,29 @@ dependencies = [ ] [[package]] -name = "extension" +name = "extension_cli" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "env_logger 0.11.5", + "extension_host", + "fs", + "language", + "log", + "reqwest_client", + "rpc", + "serde", + "serde_json", + "theme", + "tokio", + "toml 0.8.19", + "tree-sitter", + "wasmtime", +] + +[[package]] +name = "extension_host" version = "0.1.0" dependencies = [ "anyhow", @@ -4137,28 +4159,6 @@ dependencies = [ "workspace", ] -[[package]] -name = "extension_cli" -version = "0.1.0" -dependencies = [ - "anyhow", - "clap", - "env_logger 0.11.5", - "extension", - "fs", - "language", - "log", - "reqwest_client", - "rpc", - "serde", - "serde_json", - "theme", - "tokio", - "toml 0.8.19", - "tree-sitter", - "wasmtime", -] - [[package]] name = "extensions_ui" version = "0.1.0" @@ -4168,7 +4168,7 @@ dependencies = [ "collections", "db", "editor", - "extension", + "extension_host", "fs", "fuzzy", "gpui", @@ -15065,7 +15065,7 @@ dependencies = [ "diagnostics", "editor", "env_logger 0.11.5", - "extension", + "extension_host", "extensions_ui", "feature_flags", "feedback", diff --git a/Cargo.toml b/Cargo.toml index 18dc85994a..853372dd3c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,9 +27,9 @@ members = [ "crates/docs_preprocessor", "crates/editor", "crates/evals", - "crates/extension", "crates/extension_api", "crates/extension_cli", + "crates/extension_host", "crates/extensions_ui", "crates/feature_flags", "crates/feedback", @@ -201,7 +201,7 @@ copilot = { path = "crates/copilot" } db = { path = "crates/db" } diagnostics = { path = "crates/diagnostics" } editor = { path = "crates/editor" } -extension = { path = "crates/extension" } +extension_host = { path = "crates/extension_host" } extensions_ui = { path = "crates/extensions_ui" } feature_flags = { path = "crates/feature_flags" } feedback = { path = "crates/feedback" } diff --git a/crates/activity_indicator/Cargo.toml b/crates/activity_indicator/Cargo.toml index b4fb2ec5b0..6f026d7662 100644 --- a/crates/activity_indicator/Cargo.toml +++ b/crates/activity_indicator/Cargo.toml @@ -16,7 +16,7 @@ doctest = false anyhow.workspace = true auto_update.workspace = true editor.workspace = true -extension.workspace = true +extension_host.workspace = true futures.workspace = true gpui.workspace = true language.workspace = true diff --git a/crates/activity_indicator/src/activity_indicator.rs b/crates/activity_indicator/src/activity_indicator.rs index f06ebe4b23..c9fc51ff1e 100644 --- a/crates/activity_indicator/src/activity_indicator.rs +++ b/crates/activity_indicator/src/activity_indicator.rs @@ -1,6 +1,6 @@ use auto_update::{AutoUpdateStatus, AutoUpdater, DismissErrorMessage}; use editor::Editor; -use extension::ExtensionStore; +use extension_host::ExtensionStore; use futures::StreamExt; use gpui::{ actions, percentage, Animation, AnimationExt as _, AppContext, CursorStyle, EventEmitter, diff --git a/crates/extension_cli/Cargo.toml b/crates/extension_cli/Cargo.toml index 6c7e3bdc62..8cf3e9b6be 100644 --- a/crates/extension_cli/Cargo.toml +++ b/crates/extension_cli/Cargo.toml @@ -16,7 +16,7 @@ path = "src/main.rs" anyhow.workspace = true clap = { workspace = true, features = ["derive"] } env_logger.workspace = true -extension = { workspace = true, features = ["no-webrtc"] } +extension_host = { workspace = true, features = ["no-webrtc"] } fs.workspace = true language.workspace = true log.workspace = true diff --git a/crates/extension_cli/src/main.rs b/crates/extension_cli/src/main.rs index ffa9555c21..58bee34923 100644 --- a/crates/extension_cli/src/main.rs +++ b/crates/extension_cli/src/main.rs @@ -9,7 +9,7 @@ use std::{ use ::fs::{copy_recursive, CopyOptions, Fs, RealFs}; use anyhow::{anyhow, bail, Context, Result}; use clap::Parser; -use extension::{ +use extension_host::{ extension_builder::{CompileExtensionOptions, ExtensionBuilder}, ExtensionManifest, }; diff --git a/crates/extension/Cargo.toml b/crates/extension_host/Cargo.toml similarity index 96% rename from crates/extension/Cargo.toml rename to crates/extension_host/Cargo.toml index 26b8610e76..dd31c2af86 100644 --- a/crates/extension/Cargo.toml +++ b/crates/extension_host/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "extension" +name = "extension_host" version = "0.1.0" edition = "2021" publish = false @@ -9,7 +9,7 @@ license = "GPL-3.0-or-later" workspace = true [lib] -path = "src/extension_store.rs" +path = "src/extension_host.rs" doctest = false [features] diff --git a/crates/extension/LICENSE-GPL b/crates/extension_host/LICENSE-GPL similarity index 100% rename from crates/extension/LICENSE-GPL rename to crates/extension_host/LICENSE-GPL diff --git a/crates/extension/build.rs b/crates/extension_host/build.rs similarity index 100% rename from crates/extension/build.rs rename to crates/extension_host/build.rs diff --git a/crates/extension/src/extension_builder.rs b/crates/extension_host/src/extension_builder.rs similarity index 100% rename from crates/extension/src/extension_builder.rs rename to crates/extension_host/src/extension_builder.rs diff --git a/crates/extension/src/extension_store.rs b/crates/extension_host/src/extension_host.rs similarity index 100% rename from crates/extension/src/extension_store.rs rename to crates/extension_host/src/extension_host.rs diff --git a/crates/extension/src/extension_indexed_docs_provider.rs b/crates/extension_host/src/extension_indexed_docs_provider.rs similarity index 100% rename from crates/extension/src/extension_indexed_docs_provider.rs rename to crates/extension_host/src/extension_indexed_docs_provider.rs diff --git a/crates/extension/src/extension_lsp_adapter.rs b/crates/extension_host/src/extension_lsp_adapter.rs similarity index 100% rename from crates/extension/src/extension_lsp_adapter.rs rename to crates/extension_host/src/extension_lsp_adapter.rs diff --git a/crates/extension/src/extension_manifest.rs b/crates/extension_host/src/extension_manifest.rs similarity index 100% rename from crates/extension/src/extension_manifest.rs rename to crates/extension_host/src/extension_manifest.rs diff --git a/crates/extension/src/extension_settings.rs b/crates/extension_host/src/extension_settings.rs similarity index 100% rename from crates/extension/src/extension_settings.rs rename to crates/extension_host/src/extension_settings.rs diff --git a/crates/extension/src/extension_slash_command.rs b/crates/extension_host/src/extension_slash_command.rs similarity index 100% rename from crates/extension/src/extension_slash_command.rs rename to crates/extension_host/src/extension_slash_command.rs diff --git a/crates/extension/src/extension_store_test.rs b/crates/extension_host/src/extension_store_test.rs similarity index 100% rename from crates/extension/src/extension_store_test.rs rename to crates/extension_host/src/extension_store_test.rs diff --git a/crates/extension/src/wasm_host.rs b/crates/extension_host/src/wasm_host.rs similarity index 100% rename from crates/extension/src/wasm_host.rs rename to crates/extension_host/src/wasm_host.rs diff --git a/crates/extension/src/wasm_host/wit.rs b/crates/extension_host/src/wasm_host/wit.rs similarity index 100% rename from crates/extension/src/wasm_host/wit.rs rename to crates/extension_host/src/wasm_host/wit.rs diff --git a/crates/extension/src/wasm_host/wit/since_v0_0_1.rs b/crates/extension_host/src/wasm_host/wit/since_v0_0_1.rs similarity index 100% rename from crates/extension/src/wasm_host/wit/since_v0_0_1.rs rename to crates/extension_host/src/wasm_host/wit/since_v0_0_1.rs diff --git a/crates/extension/src/wasm_host/wit/since_v0_0_4.rs b/crates/extension_host/src/wasm_host/wit/since_v0_0_4.rs similarity index 100% rename from crates/extension/src/wasm_host/wit/since_v0_0_4.rs rename to crates/extension_host/src/wasm_host/wit/since_v0_0_4.rs diff --git a/crates/extension/src/wasm_host/wit/since_v0_0_6.rs b/crates/extension_host/src/wasm_host/wit/since_v0_0_6.rs similarity index 100% rename from crates/extension/src/wasm_host/wit/since_v0_0_6.rs rename to crates/extension_host/src/wasm_host/wit/since_v0_0_6.rs diff --git a/crates/extension/src/wasm_host/wit/since_v0_1_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_1_0.rs similarity index 100% rename from crates/extension/src/wasm_host/wit/since_v0_1_0.rs rename to crates/extension_host/src/wasm_host/wit/since_v0_1_0.rs diff --git a/crates/extension/src/wasm_host/wit/since_v0_2_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_2_0.rs similarity index 100% rename from crates/extension/src/wasm_host/wit/since_v0_2_0.rs rename to crates/extension_host/src/wasm_host/wit/since_v0_2_0.rs diff --git a/crates/extensions_ui/Cargo.toml b/crates/extensions_ui/Cargo.toml index 28f0fcb7ad..0de1fd947a 100644 --- a/crates/extensions_ui/Cargo.toml +++ b/crates/extensions_ui/Cargo.toml @@ -20,7 +20,7 @@ client.workspace = true collections.workspace = true db.workspace = true editor.workspace = true -extension.workspace = true +extension_host.workspace = true fs.workspace = true fuzzy.workspace = true gpui.workspace = true diff --git a/crates/extensions_ui/src/extension_suggest.rs b/crates/extensions_ui/src/extension_suggest.rs index b21621537f..223a36699e 100644 --- a/crates/extensions_ui/src/extension_suggest.rs +++ b/crates/extensions_ui/src/extension_suggest.rs @@ -4,7 +4,7 @@ use std::sync::{Arc, OnceLock}; use db::kvp::KEY_VALUE_STORE; use editor::Editor; -use extension::ExtensionStore; +use extension_host::ExtensionStore; use gpui::{Model, VisualContext}; use language::Buffer; use ui::{SharedString, ViewContext}; diff --git a/crates/extensions_ui/src/extension_version_selector.rs b/crates/extensions_ui/src/extension_version_selector.rs index 23208bc710..1041e9524f 100644 --- a/crates/extensions_ui/src/extension_version_selector.rs +++ b/crates/extensions_ui/src/extension_version_selector.rs @@ -2,7 +2,7 @@ use std::str::FromStr; use std::sync::Arc; use client::ExtensionMetadata; -use extension::{ExtensionSettings, ExtensionStore}; +use extension_host::{ExtensionSettings, ExtensionStore}; use fs::Fs; use fuzzy::{match_strings, StringMatch, StringMatchCandidate}; use gpui::{ @@ -167,7 +167,7 @@ impl PickerDelegate for ExtensionVersionSelectorDelegate { let candidate_id = self.matches[self.selected_index].candidate_id; let extension_version = &self.extension_versions[candidate_id]; - if !extension::is_version_compatible(ReleaseChannel::global(cx), extension_version) { + if !extension_host::is_version_compatible(ReleaseChannel::global(cx), extension_version) { return; } @@ -203,7 +203,7 @@ impl PickerDelegate for ExtensionVersionSelectorDelegate { let extension_version = &self.extension_versions[version_match.candidate_id]; let is_version_compatible = - extension::is_version_compatible(ReleaseChannel::global(cx), extension_version); + extension_host::is_version_compatible(ReleaseChannel::global(cx), extension_version); let disabled = !is_version_compatible; Some( diff --git a/crates/extensions_ui/src/extensions_ui.rs b/crates/extensions_ui/src/extensions_ui.rs index f246e3cf4f..e6386ffe4a 100644 --- a/crates/extensions_ui/src/extensions_ui.rs +++ b/crates/extensions_ui/src/extensions_ui.rs @@ -11,7 +11,7 @@ use client::telemetry::Telemetry; use client::ExtensionMetadata; use collections::{BTreeMap, BTreeSet}; use editor::{Editor, EditorElement, EditorStyle}; -use extension::{ExtensionManifest, ExtensionOperation, ExtensionStore}; +use extension_host::{ExtensionManifest, ExtensionOperation, ExtensionStore}; use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ actions, uniform_list, AppContext, EventEmitter, Flatten, FocusableView, InteractiveElement, @@ -203,8 +203,8 @@ impl ExtensionsPage { let subscriptions = [ cx.observe(&store, |_, _, cx| cx.notify()), cx.subscribe(&store, move |this, _, event, cx| match event { - extension::Event::ExtensionsUpdated => this.fetch_extensions_debounced(cx), - extension::Event::ExtensionInstalled(extension_id) => { + extension_host::Event::ExtensionsUpdated => this.fetch_extensions_debounced(cx), + extension_host::Event::ExtensionInstalled(extension_id) => { this.on_extension_installed(workspace_handle.clone(), extension_id, cx) } _ => {} @@ -691,7 +691,8 @@ impl ExtensionsPage { has_dev_extension: bool, cx: &mut ViewContext, ) -> (Button, Option