diff --git a/Cargo.lock b/Cargo.lock index 9078b32f7a..3b1337eece 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11152,12 +11152,10 @@ dependencies = [ "ai_onboarding", "anyhow", "client", - "command_palette_hooks", "component", "db", "documented", "editor", - "feature_flags", "fs", "fuzzy", "git", @@ -11172,6 +11170,7 @@ dependencies = [ "schemars", "serde", "settings", + "telemetry", "theme", "ui", "util", @@ -18887,33 +18886,6 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" -[[package]] -name = "welcome" -version = "0.1.0" -dependencies = [ - "anyhow", - "client", - "component", - "db", - "documented", - "editor", - "fuzzy", - "gpui", - "install_cli", - "language", - "picker", - "project", - "serde", - "settings", - "telemetry", - "ui", - "util", - "vim_mode_setting", - "workspace", - "workspace-hack", - "zed_actions", -] - [[package]] name = "which" version = "4.4.2" @@ -20528,7 +20500,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.200.0" +version = "0.201.0" dependencies = [ "activity_indicator", "agent", @@ -20668,7 +20640,6 @@ dependencies = [ "watch", "web_search", "web_search_providers", - "welcome", "windows 0.61.1", "winresource", "workspace", @@ -20692,7 +20663,7 @@ dependencies = [ [[package]] name = "zed_emmet" -version = "0.0.5" +version = "0.0.6" dependencies = [ "zed_extension_api 0.1.0", ] diff --git a/Cargo.toml b/Cargo.toml index 8cb3c34a8a..1baa6d3d74 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -185,7 +185,6 @@ members = [ "crates/watch", "crates/web_search", "crates/web_search_providers", - "crates/welcome", "crates/workspace", "crates/worktree", "crates/x_ai", @@ -412,7 +411,6 @@ vim_mode_setting = { path = "crates/vim_mode_setting" } watch = { path = "crates/watch" } web_search = { path = "crates/web_search" } web_search_providers = { path = "crates/web_search_providers" } -welcome = { path = "crates/welcome" } workspace = { path = "crates/workspace" } worktree = { path = "crates/worktree" } x_ai = { path = "crates/x_ai" } diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 708432393c..dda26f406b 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -239,6 +239,7 @@ "ctrl-shift-a": "agent::ToggleContextPicker", "ctrl-shift-j": "agent::ToggleNavigationMenu", "ctrl-shift-i": "agent::ToggleOptionsMenu", + "ctrl-alt-shift-n": "agent::ToggleNewThreadMenu", "shift-alt-escape": "agent::ExpandMessageEditor", "ctrl->": "assistant::QuoteSelection", "ctrl-alt-e": "agent::RemoveAllContext", diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index abb741af29..3966efd8df 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -279,6 +279,7 @@ "cmd-shift-a": "agent::ToggleContextPicker", "cmd-shift-j": "agent::ToggleNavigationMenu", "cmd-shift-i": "agent::ToggleOptionsMenu", + "cmd-alt-shift-n": "agent::ToggleNewThreadMenu", "shift-alt-escape": "agent::ExpandMessageEditor", "cmd->": "assistant::QuoteSelection", "cmd-alt-e": "agent::RemoveAllContext", diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index bfd2ea5e06..ccb5119e14 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -11,12 +11,12 @@ use serde::{Deserialize, Serialize}; use crate::NewExternalAgentThread; use crate::agent_diff::AgentDiffThread; -use crate::ui::NewThreadButton; use crate::{ AddContextServer, AgentDiffPane, ContinueThread, ContinueWithBurnMode, DeleteRecentlyOpenThread, ExpandMessageEditor, Follow, InlineAssistant, NewTextThread, NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff, OpenHistory, ResetTrialEndUpsell, - ResetTrialUpsell, ToggleBurnMode, ToggleContextPicker, ToggleNavigationMenu, ToggleOptionsMenu, + ResetTrialUpsell, ToggleBurnMode, ToggleContextPicker, ToggleNavigationMenu, + ToggleNewThreadMenu, ToggleOptionsMenu, acp::AcpThreadView, active_thread::{self, ActiveThread, ActiveThreadEvent}, agent_configuration::{AgentConfiguration, AssistantConfigurationEvent}, @@ -66,8 +66,8 @@ use theme::ThemeSettings; use time::UtcOffset; use ui::utils::WithRemSize; use ui::{ - Banner, Callout, ContextMenu, ContextMenuEntry, ElevationIndex, KeyBinding, PopoverMenu, - PopoverMenuHandle, ProgressBar, Tab, Tooltip, prelude::*, + Banner, ButtonLike, Callout, ContextMenu, ContextMenuEntry, ElevationIndex, KeyBinding, + PopoverMenu, PopoverMenuHandle, ProgressBar, Tab, Tooltip, prelude::*, }; use util::ResultExt as _; use workspace::{ @@ -85,6 +85,7 @@ const AGENT_PANEL_KEY: &str = "agent_panel"; #[derive(Serialize, Deserialize)] struct SerializedAgentPanel { width: Option, + selected_agent: Option, } pub fn init(cx: &mut App) { @@ -178,6 +179,14 @@ pub fn init(cx: &mut App) { }); } }) + .register_action(|workspace, _: &ToggleNewThreadMenu, window, cx| { + if let Some(panel) = workspace.panel::(cx) { + workspace.focus_panel::(window, cx); + panel.update(cx, |panel, cx| { + panel.toggle_new_thread_menu(&ToggleNewThreadMenu, window, cx); + }); + } + }) .register_action(|workspace, _: &OpenOnboardingModal, window, cx| { AgentOnboardingModal::toggle(workspace, window, cx) }) @@ -222,6 +231,36 @@ enum WhichFontSize { None, } +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum AgentType { + #[default] + Zed, + TextThread, + Gemini, + ClaudeCode, + NativeAgent, +} + +impl AgentType { + fn label(self) -> impl Into { + match self { + Self::Zed | Self::TextThread => "Zed", + Self::NativeAgent => "Agent 2", + Self::Gemini => "Gemini", + Self::ClaudeCode => "Claude Code", + } + } + + fn icon(self) -> IconName { + match self { + Self::Zed | Self::TextThread => IconName::AiZed, + Self::NativeAgent => IconName::ZedAssistant, + Self::Gemini => IconName::AiGemini, + Self::ClaudeCode => IconName::AiClaude, + } + } +} + impl ActiveView { pub fn which_font_size_used(&self) -> WhichFontSize { match self { @@ -452,16 +491,21 @@ pub struct AgentPanel { zoomed: bool, pending_serialization: Option>>, onboarding: Entity, + selected_agent: AgentType, } impl AgentPanel { fn serialize(&mut self, cx: &mut Context) { let width = self.width; + let selected_agent = self.selected_agent; self.pending_serialization = Some(cx.background_spawn(async move { KEY_VALUE_STORE .write_kvp( AGENT_PANEL_KEY.into(), - serde_json::to_string(&SerializedAgentPanel { width })?, + serde_json::to_string(&SerializedAgentPanel { + width, + selected_agent: Some(selected_agent), + })?, ) .await?; anyhow::Ok(()) @@ -530,6 +574,9 @@ impl AgentPanel { if let Some(serialized_panel) = serialized_panel { panel.update(cx, |panel, cx| { panel.width = serialized_panel.width.map(|w| w.round()); + if let Some(selected_agent) = serialized_panel.selected_agent { + panel.selected_agent = selected_agent; + } cx.notify(); }); } @@ -731,6 +778,7 @@ impl AgentPanel { zoomed: false, pending_serialization: None, onboarding, + selected_agent: AgentType::default(), } } @@ -1171,6 +1219,15 @@ impl AgentPanel { self.agent_panel_menu_handle.toggle(window, cx); } + pub fn toggle_new_thread_menu( + &mut self, + _: &ToggleNewThreadMenu, + window: &mut Window, + cx: &mut Context, + ) { + self.new_thread_menu_handle.toggle(window, cx); + } + pub fn increase_font_size( &mut self, action: &IncreaseBufferFontSize, @@ -1578,6 +1635,17 @@ impl AgentPanel { menu } + + pub fn set_selected_agent(&mut self, agent: AgentType, cx: &mut Context) { + if self.selected_agent != agent { + self.selected_agent = agent; + self.serialize(cx); + } + } + + pub fn selected_agent(&self) -> AgentType { + self.selected_agent + } } impl Focusable for AgentPanel { @@ -1808,200 +1876,24 @@ impl AgentPanel { .into_any() } - fn render_toolbar(&self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + fn render_panel_options_menu( + &self, + window: &mut Window, + cx: &mut Context, + ) -> impl IntoElement { let user_store = self.user_store.read(cx); let usage = user_store.model_request_usage(); - let account_url = zed_urls::account_url(cx); let focus_handle = self.focus_handle(cx); - let go_back_button = div().child( - IconButton::new("go-back", IconName::ArrowLeft) - .icon_size(IconSize::Small) - .on_click(cx.listener(|this, _, window, cx| { - this.go_back(&workspace::GoBack, window, cx); - })) - .tooltip({ - let focus_handle = focus_handle.clone(); - move |window, cx| { - Tooltip::for_action_in( - "Go Back", - &workspace::GoBack, - &focus_handle, - window, - cx, - ) - } - }), - ); - - let recent_entries_menu = div().child( - PopoverMenu::new("agent-nav-menu") - .trigger_with_tooltip( - IconButton::new("agent-nav-menu", IconName::MenuAlt) - .icon_size(IconSize::Small) - .style(ui::ButtonStyle::Subtle), - { - let focus_handle = focus_handle.clone(); - move |window, cx| { - Tooltip::for_action_in( - "Toggle Panel Menu", - &ToggleNavigationMenu, - &focus_handle, - window, - cx, - ) - } - }, - ) - .anchor(Corner::TopLeft) - .with_handle(self.assistant_navigation_menu_handle.clone()) - .menu({ - let menu = self.assistant_navigation_menu.clone(); - move |window, cx| { - if let Some(menu) = menu.as_ref() { - menu.update(cx, |_, cx| { - cx.defer_in(window, |menu, window, cx| { - menu.rebuild(window, cx); - }); - }) - } - menu.clone() - } - }), - ); - let full_screen_label = if self.is_zoomed(window, cx) { "Disable Full Screen" } else { "Enable Full Screen" }; - let active_thread = match &self.active_view { - ActiveView::Thread { thread, .. } => Some(thread.read(cx).thread().clone()), - ActiveView::ExternalAgentThread { .. } - | ActiveView::TextThread { .. } - | ActiveView::History - | ActiveView::Configuration => None, - }; - - let new_thread_menu = PopoverMenu::new("new_thread_menu") - .trigger_with_tooltip( - IconButton::new("new_thread_menu_btn", IconName::Plus).icon_size(IconSize::Small), - Tooltip::text("New Thread…"), - ) - .anchor(Corner::TopRight) - .with_handle(self.new_thread_menu_handle.clone()) - .menu({ - let focus_handle = focus_handle.clone(); - move |window, cx| { - let active_thread = active_thread.clone(); - Some(ContextMenu::build(window, cx, |mut menu, _window, cx| { - menu = menu - .context(focus_handle.clone()) - .when(cx.has_flag::(), |this| { - this.header("Zed Agent") - }) - .when_some(active_thread, |this, active_thread| { - let thread = active_thread.read(cx); - - if !thread.is_empty() { - let thread_id = thread.id().clone(); - this.item( - ContextMenuEntry::new("New From Summary") - .icon(IconName::ThreadFromSummary) - .icon_color(Color::Muted) - .handler(move |window, cx| { - window.dispatch_action( - Box::new(NewThread { - from_thread_id: Some(thread_id.clone()), - }), - cx, - ); - }), - ) - } else { - this - } - }) - .item( - ContextMenuEntry::new("New Thread") - .icon(IconName::Thread) - .icon_color(Color::Muted) - .action(NewThread::default().boxed_clone()) - .handler(move |window, cx| { - window.dispatch_action( - NewThread::default().boxed_clone(), - cx, - ); - }), - ) - .item( - ContextMenuEntry::new("New Text Thread") - .icon(IconName::TextThread) - .icon_color(Color::Muted) - .action(NewTextThread.boxed_clone()) - .handler(move |window, cx| { - window.dispatch_action(NewTextThread.boxed_clone(), cx); - }), - ) - .when(cx.has_flag::(), |this| { - this.separator() - .header("External Agents") - .item( - ContextMenuEntry::new("New Gemini Thread") - .icon(IconName::AiGemini) - .icon_color(Color::Muted) - .handler(move |window, cx| { - window.dispatch_action( - NewExternalAgentThread { - agent: Some(crate::ExternalAgent::Gemini), - } - .boxed_clone(), - cx, - ); - }), - ) - .item( - ContextMenuEntry::new("New Claude Code Thread") - .icon(IconName::AiClaude) - .icon_color(Color::Muted) - .handler(move |window, cx| { - window.dispatch_action( - NewExternalAgentThread { - agent: Some( - crate::ExternalAgent::ClaudeCode, - ), - } - .boxed_clone(), - cx, - ); - }), - ) - .item( - ContextMenuEntry::new("New Native Agent Thread") - .icon(IconName::ZedAssistant) - .icon_color(Color::Muted) - .handler(move |window, cx| { - window.dispatch_action( - NewExternalAgentThread { - agent: Some( - crate::ExternalAgent::NativeAgent, - ), - } - .boxed_clone(), - cx, - ); - }), - ) - }); - menu - })) - } - }); - - let agent_panel_menu = PopoverMenu::new("agent-options-menu") + PopoverMenu::new("agent-options-menu") .trigger_with_tooltip( IconButton::new("agent-options-menu", IconName::Ellipsis) .icon_size(IconSize::Small), @@ -2084,6 +1976,139 @@ impl AgentPanel { menu })) } + }) + } + + fn render_recent_entries_menu( + &self, + icon: IconName, + cx: &mut Context, + ) -> impl IntoElement { + let focus_handle = self.focus_handle(cx); + + PopoverMenu::new("agent-nav-menu") + .trigger_with_tooltip( + IconButton::new("agent-nav-menu", icon) + .icon_size(IconSize::Small) + .style(ui::ButtonStyle::Subtle), + { + let focus_handle = focus_handle.clone(); + move |window, cx| { + Tooltip::for_action_in( + "Toggle Panel Menu", + &ToggleNavigationMenu, + &focus_handle, + window, + cx, + ) + } + }, + ) + .anchor(Corner::TopLeft) + .with_handle(self.assistant_navigation_menu_handle.clone()) + .menu({ + let menu = self.assistant_navigation_menu.clone(); + move |window, cx| { + if let Some(menu) = menu.as_ref() { + menu.update(cx, |_, cx| { + cx.defer_in(window, |menu, window, cx| { + menu.rebuild(window, cx); + }); + }) + } + menu.clone() + } + }) + } + + fn render_toolbar_back_button(&self, cx: &mut Context) -> impl IntoElement { + let focus_handle = self.focus_handle(cx); + + IconButton::new("go-back", IconName::ArrowLeft) + .icon_size(IconSize::Small) + .on_click(cx.listener(|this, _, window, cx| { + this.go_back(&workspace::GoBack, window, cx); + })) + .tooltip({ + let focus_handle = focus_handle.clone(); + + move |window, cx| { + Tooltip::for_action_in("Go Back", &workspace::GoBack, &focus_handle, window, cx) + } + }) + } + + fn render_toolbar_old(&self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let focus_handle = self.focus_handle(cx); + + let active_thread = match &self.active_view { + ActiveView::Thread { thread, .. } => Some(thread.read(cx).thread().clone()), + ActiveView::ExternalAgentThread { .. } + | ActiveView::TextThread { .. } + | ActiveView::History + | ActiveView::Configuration => None, + }; + + let new_thread_menu = PopoverMenu::new("new_thread_menu") + .trigger_with_tooltip( + IconButton::new("new_thread_menu_btn", IconName::Plus).icon_size(IconSize::Small), + Tooltip::text("New Thread…"), + ) + .anchor(Corner::TopRight) + .with_handle(self.new_thread_menu_handle.clone()) + .menu({ + let focus_handle = focus_handle.clone(); + move |window, cx| { + let active_thread = active_thread.clone(); + Some(ContextMenu::build(window, cx, |mut menu, _window, cx| { + menu = menu + .context(focus_handle.clone()) + .when_some(active_thread, |this, active_thread| { + let thread = active_thread.read(cx); + + if !thread.is_empty() { + let thread_id = thread.id().clone(); + this.item( + ContextMenuEntry::new("New From Summary") + .icon(IconName::ThreadFromSummary) + .icon_color(Color::Muted) + .handler(move |window, cx| { + window.dispatch_action( + Box::new(NewThread { + from_thread_id: Some(thread_id.clone()), + }), + cx, + ); + }), + ) + } else { + this + } + }) + .item( + ContextMenuEntry::new("New Thread") + .icon(IconName::Thread) + .icon_color(Color::Muted) + .action(NewThread::default().boxed_clone()) + .handler(move |window, cx| { + window.dispatch_action( + NewThread::default().boxed_clone(), + cx, + ); + }), + ) + .item( + ContextMenuEntry::new("New Text Thread") + .icon(IconName::TextThread) + .icon_color(Color::Muted) + .action(NewTextThread.boxed_clone()) + .handler(move |window, cx| { + window.dispatch_action(NewTextThread.boxed_clone(), cx); + }), + ); + menu + })) + } }); h_flex() @@ -2102,8 +2127,12 @@ impl AgentPanel { .pl_1() .gap_1() .child(match &self.active_view { - ActiveView::History | ActiveView::Configuration => go_back_button, - _ => recent_entries_menu, + ActiveView::History | ActiveView::Configuration => { + self.render_toolbar_back_button(cx).into_any_element() + } + _ => self + .render_recent_entries_menu(IconName::MenuAlt, cx) + .into_any_element(), }) .child(self.render_title_view(window, cx)), ) @@ -2120,11 +2149,308 @@ impl AgentPanel { .border_l_1() .border_color(cx.theme().colors().border) .child(new_thread_menu) - .child(agent_panel_menu), + .child(self.render_panel_options_menu(window, cx)), ), ) } + fn render_toolbar_new(&self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let focus_handle = self.focus_handle(cx); + + let active_thread = match &self.active_view { + ActiveView::Thread { thread, .. } => Some(thread.read(cx).thread().clone()), + ActiveView::ExternalAgentThread { .. } + | ActiveView::TextThread { .. } + | ActiveView::History + | ActiveView::Configuration => None, + }; + + let new_thread_menu = PopoverMenu::new("new_thread_menu") + .trigger_with_tooltip( + ButtonLike::new("new_thread_menu_btn").child( + h_flex() + .group("agent-selector") + .gap_1p5() + .child( + h_flex() + .relative() + .size_4() + .justify_center() + .child( + h_flex() + .group_hover("agent-selector", |s| s.invisible()) + .child( + Icon::new(self.selected_agent.icon()) + .color(Color::Muted), + ), + ) + .child( + h_flex() + .absolute() + .invisible() + .group_hover("agent-selector", |s| s.visible()) + .child(Icon::new(IconName::Plus)), + ), + ) + .child(Label::new(self.selected_agent.label())), + ), + { + let focus_handle = focus_handle.clone(); + move |window, cx| { + Tooltip::for_action_in( + "New…", + &ToggleNewThreadMenu, + &focus_handle, + window, + cx, + ) + } + }, + ) + .anchor(Corner::TopLeft) + .with_handle(self.new_thread_menu_handle.clone()) + .menu({ + let focus_handle = focus_handle.clone(); + let workspace = self.workspace.clone(); + + move |window, cx| { + let active_thread = active_thread.clone(); + Some(ContextMenu::build(window, cx, |mut menu, _window, cx| { + menu = menu + .context(focus_handle.clone()) + .header("Zed Agent") + .when_some(active_thread, |this, active_thread| { + let thread = active_thread.read(cx); + + if !thread.is_empty() { + let thread_id = thread.id().clone(); + this.item( + ContextMenuEntry::new("New From Summary") + .icon(IconName::ThreadFromSummary) + .icon_color(Color::Muted) + .handler(move |window, cx| { + window.dispatch_action( + Box::new(NewThread { + from_thread_id: Some(thread_id.clone()), + }), + cx, + ); + }), + ) + } else { + this + } + }) + .item( + ContextMenuEntry::new("New Thread") + .icon(IconName::Thread) + .icon_color(Color::Muted) + .action(NewThread::default().boxed_clone()) + .handler({ + let workspace = workspace.clone(); + move |window, cx| { + if let Some(workspace) = workspace.upgrade() { + workspace.update(cx, |workspace, cx| { + if let Some(panel) = + workspace.panel::(cx) + { + panel.update(cx, |panel, cx| { + panel.set_selected_agent( + AgentType::Zed, + cx, + ); + }); + } + }); + } + window.dispatch_action( + NewThread::default().boxed_clone(), + cx, + ); + } + }), + ) + .item( + ContextMenuEntry::new("New Text Thread") + .icon(IconName::TextThread) + .icon_color(Color::Muted) + .action(NewTextThread.boxed_clone()) + .handler({ + let workspace = workspace.clone(); + move |window, cx| { + if let Some(workspace) = workspace.upgrade() { + workspace.update(cx, |workspace, cx| { + if let Some(panel) = + workspace.panel::(cx) + { + panel.update(cx, |panel, cx| { + panel.set_selected_agent( + AgentType::TextThread, + cx, + ); + }); + } + }); + } + window.dispatch_action(NewTextThread.boxed_clone(), cx); + } + }), + ) + .item( + ContextMenuEntry::new("New Native Agent Thread") + .icon(IconName::ZedAssistant) + .icon_color(Color::Muted) + .handler({ + let workspace = workspace.clone(); + move |window, cx| { + if let Some(workspace) = workspace.upgrade() { + workspace.update(cx, |workspace, cx| { + if let Some(panel) = + workspace.panel::(cx) + { + panel.update(cx, |panel, cx| { + panel.set_selected_agent( + AgentType::NativeAgent, + cx, + ); + }); + } + }); + } + window.dispatch_action( + NewExternalAgentThread { + agent: Some(crate::ExternalAgent::NativeAgent), + } + .boxed_clone(), + cx, + ); + } + }), + ) + .separator() + .header("External Agents") + .item( + ContextMenuEntry::new("New Gemini Thread") + .icon(IconName::AiGemini) + .icon_color(Color::Muted) + .handler({ + let workspace = workspace.clone(); + move |window, cx| { + if let Some(workspace) = workspace.upgrade() { + workspace.update(cx, |workspace, cx| { + if let Some(panel) = + workspace.panel::(cx) + { + panel.update(cx, |panel, cx| { + panel.set_selected_agent( + AgentType::Gemini, + cx, + ); + }); + } + }); + } + window.dispatch_action( + NewExternalAgentThread { + agent: Some(crate::ExternalAgent::Gemini), + } + .boxed_clone(), + cx, + ); + } + }), + ) + .item( + ContextMenuEntry::new("New Claude Code Thread") + .icon(IconName::AiClaude) + .icon_color(Color::Muted) + .handler({ + let workspace = workspace.clone(); + move |window, cx| { + if let Some(workspace) = workspace.upgrade() { + workspace.update(cx, |workspace, cx| { + if let Some(panel) = + workspace.panel::(cx) + { + panel.update(cx, |panel, cx| { + panel.set_selected_agent( + AgentType::ClaudeCode, + cx, + ); + }); + } + }); + } + window.dispatch_action( + NewExternalAgentThread { + agent: Some(crate::ExternalAgent::ClaudeCode), + } + .boxed_clone(), + cx, + ); + } + }), + ); + menu + })) + } + }); + + h_flex() + .id("agent-panel-toolbar") + .h(Tab::container_height(cx)) + .max_w_full() + .flex_none() + .justify_between() + .gap_2() + .bg(cx.theme().colors().tab_bar_background) + .border_b_1() + .border_color(cx.theme().colors().border) + .child( + h_flex() + .size_full() + .gap(DynamicSpacing::Base08.rems(cx)) + .child(match &self.active_view { + ActiveView::History | ActiveView::Configuration => { + self.render_toolbar_back_button(cx).into_any_element() + } + _ => h_flex() + .h_full() + .px(DynamicSpacing::Base04.rems(cx)) + .border_r_1() + .border_color(cx.theme().colors().border) + .child(new_thread_menu) + .into_any_element(), + }) + .child(self.render_title_view(window, cx)), + ) + .child( + h_flex() + .h_full() + .gap_2() + .children(self.render_token_count(cx)) + .child( + h_flex() + .h_full() + .gap(DynamicSpacing::Base02.rems(cx)) + .pl(DynamicSpacing::Base04.rems(cx)) + .pr(DynamicSpacing::Base06.rems(cx)) + .border_l_1() + .border_color(cx.theme().colors().border) + .child(self.render_recent_entries_menu(IconName::HistoryRerun, cx)) + .child(self.render_panel_options_menu(window, cx)), + ), + ) + } + + fn render_toolbar(&self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + if cx.has_flag::() { + self.render_toolbar_new(window, cx).into_any_element() + } else { + self.render_toolbar_old(window, cx).into_any_element() + } + } + fn render_token_count(&self, cx: &App) -> Option { match &self.active_view { ActiveView::Thread { @@ -2573,138 +2899,6 @@ impl AgentPanel { }, )), ) - .child(self.render_empty_state_section_header("Start", None, cx)) - .child( - v_flex() - .p_1() - .gap_2() - .child( - h_flex() - .w_full() - .gap_2() - .child( - NewThreadButton::new( - "new-thread-btn", - "New Thread", - IconName::Thread, - ) - .keybinding(KeyBinding::for_action_in( - &NewThread::default(), - &self.focus_handle(cx), - window, - cx, - )) - .on_click( - |window, cx| { - window.dispatch_action( - NewThread::default().boxed_clone(), - cx, - ) - }, - ), - ) - .child( - NewThreadButton::new( - "new-text-thread-btn", - "New Text Thread", - IconName::TextThread, - ) - .keybinding(KeyBinding::for_action_in( - &NewTextThread, - &self.focus_handle(cx), - window, - cx, - )) - .on_click( - |window, cx| { - window.dispatch_action(Box::new(NewTextThread), cx) - }, - ), - ), - ) - .when(cx.has_flag::(), |this| { - this.child( - h_flex() - .w_full() - .gap_2() - .child( - NewThreadButton::new( - "new-gemini-thread-btn", - "New Gemini Thread", - IconName::AiGemini, - ) - // .keybinding(KeyBinding::for_action_in( - // &OpenHistory, - // &self.focus_handle(cx), - // window, - // cx, - // )) - .on_click( - |window, cx| { - window.dispatch_action( - Box::new(NewExternalAgentThread { - agent: Some( - crate::ExternalAgent::Gemini, - ), - }), - cx, - ) - }, - ), - ) - .child( - NewThreadButton::new( - "new-claude-thread-btn", - "New Claude Code Thread", - IconName::AiClaude, - ) - // .keybinding(KeyBinding::for_action_in( - // &OpenHistory, - // &self.focus_handle(cx), - // window, - // cx, - // )) - .on_click( - |window, cx| { - window.dispatch_action( - Box::new(NewExternalAgentThread { - agent: Some( - crate::ExternalAgent::ClaudeCode, - ), - }), - cx, - ) - }, - ), - ) - .child( - NewThreadButton::new( - "new-native-agent-thread-btn", - "New Native Agent Thread", - IconName::ZedAssistant, - ) - // .keybinding(KeyBinding::for_action_in( - // &OpenHistory, - // &self.focus_handle(cx), - // window, - // cx, - // )) - .on_click( - |window, cx| { - window.dispatch_action( - Box::new(NewExternalAgentThread { - agent: Some( - crate::ExternalAgent::NativeAgent, - ), - }), - cx, - ) - }, - ), - ), - ) - }), - ) .when_some(configuration_error.as_ref(), |this, err| { this.child(self.render_configuration_error(err, &focus_handle, window, cx)) }) diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index b776c0830b..231b9cfb38 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -64,6 +64,8 @@ actions!( NewTextThread, /// Toggles the context picker interface for adding files, symbols, or other context. ToggleContextPicker, + /// Toggles the menu to create new agent threads. + ToggleNewThreadMenu, /// Toggles the navigation menu for switching between threads and views. ToggleNavigationMenu, /// Toggles the options menu for agent settings and preferences. diff --git a/crates/agent_ui/src/ui.rs b/crates/agent_ui/src/ui.rs index b477a8c385..beeaf0c43b 100644 --- a/crates/agent_ui/src/ui.rs +++ b/crates/agent_ui/src/ui.rs @@ -2,7 +2,7 @@ mod agent_notification; mod burn_mode_tooltip; mod context_pill; mod end_trial_upsell; -mod new_thread_button; +// mod new_thread_button; mod onboarding_modal; pub mod preview; @@ -10,5 +10,5 @@ pub use agent_notification::*; pub use burn_mode_tooltip::*; pub use context_pill::*; pub use end_trial_upsell::*; -pub use new_thread_button::*; +// pub use new_thread_button::*; pub use onboarding_modal::*; diff --git a/crates/agent_ui/src/ui/new_thread_button.rs b/crates/agent_ui/src/ui/new_thread_button.rs index 7764144150..347d6adcaf 100644 --- a/crates/agent_ui/src/ui/new_thread_button.rs +++ b/crates/agent_ui/src/ui/new_thread_button.rs @@ -11,7 +11,7 @@ pub struct NewThreadButton { } impl NewThreadButton { - pub fn new(id: impl Into, label: impl Into, icon: IconName) -> Self { + fn new(id: impl Into, label: impl Into, icon: IconName) -> Self { Self { id: id.into(), label: label.into(), @@ -21,12 +21,12 @@ impl NewThreadButton { } } - pub fn keybinding(mut self, keybinding: Option) -> Self { + fn keybinding(mut self, keybinding: Option) -> Self { self.keybinding = keybinding; self } - pub fn on_click(mut self, handler: F) -> Self + fn on_click(mut self, handler: F) -> Self where F: Fn(&mut Window, &mut App) + 'static, { diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 49ae2b9d9c..166a582c70 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -21,7 +21,7 @@ use language::{ point_from_lsp, point_to_lsp, }; use lsp::{LanguageServer, LanguageServerBinary, LanguageServerId, LanguageServerName}; -use node_runtime::NodeRuntime; +use node_runtime::{NodeRuntime, VersionCheck}; use parking_lot::Mutex; use project::DisableAiSettings; use request::StatusNotification; @@ -1169,9 +1169,8 @@ async fn get_copilot_lsp(fs: Arc, node_runtime: NodeRuntime) -> anyhow:: const SERVER_PATH: &str = "node_modules/@github/copilot-language-server/dist/language-server.js"; - let latest_version = node_runtime - .npm_package_latest_version(PACKAGE_NAME) - .await?; + // pinning it: https://github.com/zed-industries/zed/issues/36093 + const PINNED_VERSION: &str = "1.354"; let server_path = paths::copilot_dir().join(SERVER_PATH); fs.create_dir(paths::copilot_dir()).await?; @@ -1181,12 +1180,13 @@ async fn get_copilot_lsp(fs: Arc, node_runtime: NodeRuntime) -> anyhow:: PACKAGE_NAME, &server_path, paths::copilot_dir(), - &latest_version, + &PINNED_VERSION, + VersionCheck::VersionMismatch, ) .await; if should_install { node_runtime - .npm_install_packages(paths::copilot_dir(), &[(PACKAGE_NAME, &latest_version)]) + .npm_install_packages(paths::copilot_dir(), &[(PACKAGE_NAME, &PINNED_VERSION)]) .await?; } diff --git a/crates/file_icons/src/file_icons.rs b/crates/file_icons/src/file_icons.rs index 2f159771b1..82a8e05d85 100644 --- a/crates/file_icons/src/file_icons.rs +++ b/crates/file_icons/src/file_icons.rs @@ -33,13 +33,23 @@ impl FileIcons { // TODO: Associate a type with the languages and have the file's language // override these associations - // check if file name is in suffixes - // e.g. catch file named `eslint.config.js` instead of `.eslint.config.js` - if let Some(typ) = path.file_name().and_then(|typ| typ.to_str()) { + if let Some(mut typ) = path.file_name().and_then(|typ| typ.to_str()) { + // check if file name is in suffixes + // e.g. catch file named `eslint.config.js` instead of `.eslint.config.js` let maybe_path = get_icon_from_suffix(typ); if maybe_path.is_some() { return maybe_path; } + + // check if suffix based on first dot is in suffixes + // e.g. consider `module.js` as suffix to angular's module file named `auth.module.js` + while let Some((_, suffix)) = typ.split_once('.') { + let maybe_path = get_icon_from_suffix(suffix); + if maybe_path.is_some() { + return maybe_path; + } + typ = suffix; + } } // primary case: check if the files extension or the hidden file name diff --git a/crates/languages/src/css.rs b/crates/languages/src/css.rs index 7725e079be..19329fcc6e 100644 --- a/crates/languages/src/css.rs +++ b/crates/languages/src/css.rs @@ -103,7 +103,13 @@ impl LspAdapter for CssLspAdapter { let should_install_language_server = self .node - .should_install_npm_package(Self::PACKAGE_NAME, &server_path, &container_dir, &version) + .should_install_npm_package( + Self::PACKAGE_NAME, + &server_path, + &container_dir, + &version, + Default::default(), + ) .await; if should_install_language_server { diff --git a/crates/languages/src/json.rs b/crates/languages/src/json.rs index ca82bb2431..019b45d396 100644 --- a/crates/languages/src/json.rs +++ b/crates/languages/src/json.rs @@ -340,7 +340,13 @@ impl LspAdapter for JsonLspAdapter { let should_install_language_server = self .node - .should_install_npm_package(Self::PACKAGE_NAME, &server_path, &container_dir, &version) + .should_install_npm_package( + Self::PACKAGE_NAME, + &server_path, + &container_dir, + &version, + Default::default(), + ) .await; if should_install_language_server { diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index 0524c02fd5..5513324487 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -206,6 +206,7 @@ impl LspAdapter for PythonLspAdapter { &server_path, &container_dir, &version, + Default::default(), ) .await; diff --git a/crates/languages/src/tailwind.rs b/crates/languages/src/tailwind.rs index a7edbb148c..6f03eeda8d 100644 --- a/crates/languages/src/tailwind.rs +++ b/crates/languages/src/tailwind.rs @@ -108,7 +108,13 @@ impl LspAdapter for TailwindLspAdapter { let should_install_language_server = self .node - .should_install_npm_package(Self::PACKAGE_NAME, &server_path, &container_dir, &version) + .should_install_npm_package( + Self::PACKAGE_NAME, + &server_path, + &container_dir, + &version, + Default::default(), + ) .await; if should_install_language_server { diff --git a/crates/languages/src/typescript.rs b/crates/languages/src/typescript.rs index f976b62614..a8ba880889 100644 --- a/crates/languages/src/typescript.rs +++ b/crates/languages/src/typescript.rs @@ -589,6 +589,7 @@ impl LspAdapter for TypeScriptLspAdapter { &server_path, &container_dir, version.typescript_version.as_str(), + Default::default(), ) .await; diff --git a/crates/languages/src/vtsls.rs b/crates/languages/src/vtsls.rs index 33751f733e..73498fc579 100644 --- a/crates/languages/src/vtsls.rs +++ b/crates/languages/src/vtsls.rs @@ -116,6 +116,7 @@ impl LspAdapter for VtslsLspAdapter { &server_path, &container_dir, &latest_version.server_version, + Default::default(), ) .await { @@ -129,6 +130,7 @@ impl LspAdapter for VtslsLspAdapter { &container_dir.join(Self::TYPESCRIPT_TSDK_PATH), &container_dir, &latest_version.typescript_version, + Default::default(), ) .await { diff --git a/crates/languages/src/yaml.rs b/crates/languages/src/yaml.rs index 815605d524..28be2cc1a4 100644 --- a/crates/languages/src/yaml.rs +++ b/crates/languages/src/yaml.rs @@ -104,7 +104,13 @@ impl LspAdapter for YamlLspAdapter { let should_install_language_server = self .node - .should_install_npm_package(Self::PACKAGE_NAME, &server_path, &container_dir, &version) + .should_install_npm_package( + Self::PACKAGE_NAME, + &server_path, + &container_dir, + &version, + Default::default(), + ) .await; if should_install_language_server { diff --git a/crates/node_runtime/src/node_runtime.rs b/crates/node_runtime/src/node_runtime.rs index 08698a1d6c..6fcc3a728a 100644 --- a/crates/node_runtime/src/node_runtime.rs +++ b/crates/node_runtime/src/node_runtime.rs @@ -29,6 +29,15 @@ pub struct NodeBinaryOptions { pub use_paths: Option<(PathBuf, PathBuf)>, } +#[derive(Default)] +pub enum VersionCheck { + /// Check whether the installed and requested version have a mismatch + VersionMismatch, + /// Only check whether the currently installed version is older than the newest one + #[default] + OlderVersion, +} + #[derive(Clone)] pub struct NodeRuntime(Arc>); @@ -287,6 +296,7 @@ impl NodeRuntime { local_executable_path: &Path, local_package_directory: &Path, latest_version: &str, + version_check: VersionCheck, ) -> bool { // In the case of the local system not having the package installed, // or in the instances where we fail to parse package.json data, @@ -311,7 +321,10 @@ impl NodeRuntime { return true; }; - installed_version < latest_version + match version_check { + VersionCheck::VersionMismatch => installed_version != latest_version, + VersionCheck::OlderVersion => installed_version < latest_version, + } } } diff --git a/crates/onboarding/Cargo.toml b/crates/onboarding/Cargo.toml index cb07bb5dab..4157be3172 100644 --- a/crates/onboarding/Cargo.toml +++ b/crates/onboarding/Cargo.toml @@ -18,12 +18,10 @@ default = [] ai_onboarding.workspace = true anyhow.workspace = true client.workspace = true -command_palette_hooks.workspace = true component.workspace = true db.workspace = true documented.workspace = true editor.workspace = true -feature_flags.workspace = true fs.workspace = true fuzzy.workspace = true git.workspace = true @@ -38,6 +36,7 @@ project.workspace = true schemars.workspace = true serde.workspace = true settings.workspace = true +telemetry.workspace = true theme.workspace = true ui.workspace = true util.workspace = true diff --git a/crates/onboarding/src/ai_setup_page.rs b/crates/onboarding/src/ai_setup_page.rs index 8203f96479..bb1932bdf2 100644 --- a/crates/onboarding/src/ai_setup_page.rs +++ b/crates/onboarding/src/ai_setup_page.rs @@ -188,6 +188,11 @@ fn render_llm_provider_card( workspace .update(cx, |workspace, cx| { workspace.toggle_modal(window, cx, |window, cx| { + telemetry::event!( + "Welcome AI Modal Opened", + provider = provider.name().0, + ); + let modal = AiConfigurationModal::new( provider.clone(), window, @@ -245,16 +250,25 @@ pub(crate) fn render_ai_setup_page( ToggleState::Selected }, |&toggle_state, _, cx| { + let enabled = match toggle_state { + ToggleState::Indeterminate => { + return; + } + ToggleState::Unselected => true, + ToggleState::Selected => false, + }; + + telemetry::event!( + "Welcome AI Enabled", + toggle = if enabled { "on" } else { "off" }, + ); + let fs = ::global(cx); update_settings_file::( fs, cx, move |ai_settings: &mut Option, _| { - *ai_settings = match toggle_state { - ToggleState::Indeterminate => None, - ToggleState::Unselected => Some(true), - ToggleState::Selected => Some(false), - }; + *ai_settings = Some(enabled); }, ); }, diff --git a/crates/welcome/src/base_keymap_picker.rs b/crates/onboarding/src/base_keymap_picker.rs similarity index 99% rename from crates/welcome/src/base_keymap_picker.rs rename to crates/onboarding/src/base_keymap_picker.rs index 92317ca711..0ac07d9a9d 100644 --- a/crates/welcome/src/base_keymap_picker.rs +++ b/crates/onboarding/src/base_keymap_picker.rs @@ -12,7 +12,7 @@ use util::ResultExt; use workspace::{ModalView, Workspace, ui::HighlightedLabel}; actions!( - welcome, + zed, [ /// Toggles the base keymap selector modal. ToggleBaseKeymapSelector diff --git a/crates/onboarding/src/editing_page.rs b/crates/onboarding/src/editing_page.rs index c69bd3852e..aa7f4eee74 100644 --- a/crates/onboarding/src/editing_page.rs +++ b/crates/onboarding/src/editing_page.rs @@ -35,6 +35,11 @@ fn write_show_mini_map(show: ShowMinimap, cx: &mut App) { EditorSettings::override_global(curr_settings, cx); update_settings_file::(fs, cx, move |editor_settings, _| { + telemetry::event!( + "Welcome Minimap Clicked", + from = editor_settings.minimap.unwrap_or_default(), + to = show + ); editor_settings.minimap.get_or_insert_default().show = Some(show); }); } @@ -71,7 +76,7 @@ fn read_git_blame(cx: &App) -> bool { ProjectSettings::get_global(cx).git.inline_blame_enabled() } -fn set_git_blame(enabled: bool, cx: &mut App) { +fn write_git_blame(enabled: bool, cx: &mut App) { let fs = ::global(cx); let mut curr_settings = ProjectSettings::get_global(cx).clone(); @@ -95,6 +100,12 @@ fn write_ui_font_family(font: SharedString, cx: &mut App) { let fs = ::global(cx); update_settings_file::(fs, cx, move |theme_settings, _| { + telemetry::event!( + "Welcome Font Changed", + type = "ui font", + old = theme_settings.ui_font_family, + new = font.clone() + ); theme_settings.ui_font_family = Some(FontFamilyName(font.into())); }); } @@ -119,6 +130,13 @@ fn write_buffer_font_family(font_family: SharedString, cx: &mut App) { let fs = ::global(cx); update_settings_file::(fs, cx, move |theme_settings, _| { + telemetry::event!( + "Welcome Font Changed", + type = "editor font", + old = theme_settings.buffer_font_family, + new = font_family.clone() + ); + theme_settings.buffer_font_family = Some(FontFamilyName(font_family.into())); }); } @@ -197,7 +215,7 @@ fn render_setting_import_button( .color(Color::Muted) .size(IconSize::XSmall), ) - .child(Label::new(label)), + .child(Label::new(label.clone())), ) .when(imported, |this| { this.child( @@ -212,7 +230,10 @@ fn render_setting_import_button( ) }), ) - .on_click(move |_, window, cx| window.dispatch_action(action.boxed_clone(), cx)), + .on_click(move |_, window, cx| { + telemetry::event!("Welcome Import Settings", import_source = label,); + window.dispatch_action(action.boxed_clone(), cx); + }), ) } @@ -605,7 +626,13 @@ fn render_popular_settings_section( ui::ToggleState::Unselected }, |toggle_state, _, cx| { - write_font_ligatures(toggle_state == &ToggleState::Selected, cx); + let enabled = toggle_state == &ToggleState::Selected; + telemetry::event!( + "Welcome Font Ligature", + options = if enabled { "on" } else { "off" }, + ); + + write_font_ligatures(enabled, cx); }, ) .tab_index({ @@ -625,7 +652,13 @@ fn render_popular_settings_section( ui::ToggleState::Unselected }, |toggle_state, _, cx| { - write_format_on_save(toggle_state == &ToggleState::Selected, cx); + let enabled = toggle_state == &ToggleState::Selected; + telemetry::event!( + "Welcome Format On Save Changed", + options = if enabled { "on" } else { "off" }, + ); + + write_format_on_save(enabled, cx); }, ) .tab_index({ @@ -644,7 +677,13 @@ fn render_popular_settings_section( ui::ToggleState::Unselected }, |toggle_state, _, cx| { - write_inlay_hints(toggle_state == &ToggleState::Selected, cx); + let enabled = toggle_state == &ToggleState::Selected; + telemetry::event!( + "Welcome Inlay Hints Changed", + options = if enabled { "on" } else { "off" }, + ); + + write_inlay_hints(enabled, cx); }, ) .tab_index({ @@ -663,7 +702,13 @@ fn render_popular_settings_section( ui::ToggleState::Unselected }, |toggle_state, _, cx| { - set_git_blame(toggle_state == &ToggleState::Selected, cx); + let enabled = toggle_state == &ToggleState::Selected; + telemetry::event!( + "Welcome Git Blame Changed", + options = if enabled { "on" } else { "off" }, + ); + + write_git_blame(enabled, cx); }, ) .tab_index({ diff --git a/crates/welcome/src/multibuffer_hint.rs b/crates/onboarding/src/multibuffer_hint.rs similarity index 100% rename from crates/welcome/src/multibuffer_hint.rs rename to crates/onboarding/src/multibuffer_hint.rs diff --git a/crates/onboarding/src/onboarding.rs b/crates/onboarding/src/onboarding.rs index c86871c919..e07a8dc9fb 100644 --- a/crates/onboarding/src/onboarding.rs +++ b/crates/onboarding/src/onboarding.rs @@ -1,8 +1,7 @@ -use crate::welcome::{ShowWelcome, WelcomePage}; +pub use crate::welcome::ShowWelcome; +use crate::{multibuffer_hint::MultibufferHint, welcome::WelcomePage}; use client::{Client, UserStore, zed_urls}; -use command_palette_hooks::CommandPaletteFilter; use db::kvp::KEY_VALUE_STORE; -use feature_flags::{FeatureFlag, FeatureFlagViewExt as _}; use fs::Fs; use gpui::{ Action, AnyElement, App, AppContext, AsyncWindowContext, Context, Entity, EventEmitter, @@ -27,17 +26,13 @@ use workspace::{ }; mod ai_setup_page; +mod base_keymap_picker; mod basics_page; mod editing_page; +pub mod multibuffer_hint; mod theme_preview; mod welcome; -pub struct OnBoardingFeatureFlag {} - -impl FeatureFlag for OnBoardingFeatureFlag { - const NAME: &'static str = "onboarding"; -} - /// Imports settings from Visual Studio Code. #[derive(Copy, Clone, Debug, Default, PartialEq, Deserialize, JsonSchema, Action)] #[action(namespace = zed)] @@ -57,6 +52,7 @@ pub struct ImportCursorSettings { } pub const FIRST_OPEN: &str = "first_open"; +pub const DOCS_URL: &str = "https://zed.dev/docs/"; actions!( zed, @@ -80,11 +76,19 @@ actions!( /// Sign in while in the onboarding flow. SignIn, /// Open the user account in zed.dev while in the onboarding flow. - OpenAccount + OpenAccount, + /// Resets the welcome screen hints to their initial state. + ResetHints ] ); pub fn init(cx: &mut App) { + cx.observe_new(|workspace: &mut Workspace, _, _cx| { + workspace + .register_action(|_workspace, _: &ResetHints, _, cx| MultibufferHint::set_count(0, cx)); + }) + .detach(); + cx.on_action(|_: &OpenOnboarding, cx| { with_active_or_new_workspace(cx, |workspace, window, cx| { workspace @@ -182,38 +186,14 @@ pub fn init(cx: &mut App) { }) .detach(); - cx.observe_new::(|_, window, cx| { - let Some(window) = window else { - return; - }; + base_keymap_picker::init(cx); - let onboarding_actions = [ - std::any::TypeId::of::(), - std::any::TypeId::of::(), - ]; - - CommandPaletteFilter::update_global(cx, |filter, _cx| { - filter.hide_action_types(&onboarding_actions); - }); - - cx.observe_flag::(window, move |is_enabled, _, _, cx| { - if is_enabled { - CommandPaletteFilter::update_global(cx, |filter, _cx| { - filter.show_action_types(onboarding_actions.iter()); - }); - } else { - CommandPaletteFilter::update_global(cx, |filter, _cx| { - filter.hide_action_types(&onboarding_actions); - }); - } - }) - .detach(); - }) - .detach(); register_serializable_item::(cx); + register_serializable_item::(cx); } pub fn show_onboarding_view(app_state: Arc, cx: &mut App) -> Task> { + telemetry::event!("Onboarding Page Opened"); open_new( Default::default(), app_state, @@ -242,6 +222,16 @@ enum SelectedPage { AiSetup, } +impl SelectedPage { + fn name(&self) -> &'static str { + match self { + SelectedPage::Basics => "Basics", + SelectedPage::Editing => "Editing", + SelectedPage::AiSetup => "AI Setup", + } + } +} + struct Onboarding { workspace: WeakEntity, focus_handle: FocusHandle, @@ -261,7 +251,21 @@ impl Onboarding { }) } - fn set_page(&mut self, page: SelectedPage, cx: &mut Context) { + fn set_page( + &mut self, + page: SelectedPage, + clicked: Option<&'static str>, + cx: &mut Context, + ) { + if let Some(click) = clicked { + telemetry::event!( + "Welcome Tab Clicked", + from = self.selected_page.name(), + to = page.name(), + clicked = click, + ); + } + self.selected_page = page; cx.notify(); cx.emit(ItemEvent::UpdateTab); @@ -325,8 +329,13 @@ impl Onboarding { gpui::Empty.into_any_element(), IntoElement::into_any_element, )) - .on_click(cx.listener(move |this, _, _, cx| { - this.set_page(page, cx); + .on_click(cx.listener(move |this, click_event, _, cx| { + let click = match click_event { + gpui::ClickEvent::Mouse(_) => "mouse", + gpui::ClickEvent::Keyboard(_) => "keyboard", + }; + + this.set_page(page, Some(click), cx); })) }) } @@ -475,6 +484,7 @@ impl Onboarding { } fn on_finish(_: &Finish, _: &mut Window, cx: &mut App) { + telemetry::event!("Welcome Skip Clicked"); go_to_welcome_page(cx); } @@ -532,13 +542,13 @@ impl Render for Onboarding { .on_action(Self::handle_sign_in) .on_action(Self::handle_open_account) .on_action(cx.listener(|this, _: &ActivateBasicsPage, _, cx| { - this.set_page(SelectedPage::Basics, cx); + this.set_page(SelectedPage::Basics, Some("action"), cx); })) .on_action(cx.listener(|this, _: &ActivateEditingPage, _, cx| { - this.set_page(SelectedPage::Editing, cx); + this.set_page(SelectedPage::Editing, Some("action"), cx); })) .on_action(cx.listener(|this, _: &ActivateAISetupPage, _, cx| { - this.set_page(SelectedPage::AiSetup, cx); + this.set_page(SelectedPage::AiSetup, Some("action"), cx); })) .on_action(cx.listener(|_, _: &menu::SelectNext, window, cx| { window.focus_next(); @@ -806,7 +816,7 @@ impl workspace::SerializableItem for Onboarding { if let Some(page) = page { zlog::info!("Onboarding page {page:?} loaded"); onboarding_page.update(cx, |onboarding_page, cx| { - onboarding_page.set_page(page, cx); + onboarding_page.set_page(page, None, cx); }) } onboarding_page diff --git a/crates/onboarding/src/welcome.rs b/crates/onboarding/src/welcome.rs index 65baad03a0..ba0053a3b6 100644 --- a/crates/onboarding/src/welcome.rs +++ b/crates/onboarding/src/welcome.rs @@ -1,6 +1,6 @@ use gpui::{ Action, App, Context, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, - ParentElement, Render, Styled, Window, actions, + ParentElement, Render, Styled, Task, Window, actions, }; use menu::{SelectNext, SelectPrevious}; use ui::{ButtonLike, Divider, DividerColor, KeyBinding, Vector, VectorName, prelude::*}; @@ -352,3 +352,109 @@ impl Item for WelcomePage { f(*event) } } + +impl workspace::SerializableItem for WelcomePage { + fn serialized_item_kind() -> &'static str { + "WelcomePage" + } + + fn cleanup( + workspace_id: workspace::WorkspaceId, + alive_items: Vec, + _window: &mut Window, + cx: &mut App, + ) -> Task> { + workspace::delete_unloaded_items( + alive_items, + workspace_id, + "welcome_pages", + &persistence::WELCOME_PAGES, + cx, + ) + } + + fn deserialize( + _project: Entity, + _workspace: gpui::WeakEntity, + workspace_id: workspace::WorkspaceId, + item_id: workspace::ItemId, + window: &mut Window, + cx: &mut App, + ) -> Task>> { + if persistence::WELCOME_PAGES + .get_welcome_page(item_id, workspace_id) + .ok() + .is_some_and(|is_open| is_open) + { + window.spawn(cx, async move |cx| cx.update(WelcomePage::new)) + } else { + Task::ready(Err(anyhow::anyhow!("No welcome page to deserialize"))) + } + } + + fn serialize( + &mut self, + workspace: &mut workspace::Workspace, + item_id: workspace::ItemId, + _closing: bool, + _window: &mut Window, + cx: &mut Context, + ) -> Option>> { + let workspace_id = workspace.database_id()?; + Some(cx.background_spawn(async move { + persistence::WELCOME_PAGES + .save_welcome_page(item_id, workspace_id, true) + .await + })) + } + + fn should_serialize(&self, event: &Self::Event) -> bool { + event == &ItemEvent::UpdateTab + } +} + +mod persistence { + use db::{define_connection, query, sqlez_macros::sql}; + use workspace::WorkspaceDb; + + define_connection! { + pub static ref WELCOME_PAGES: WelcomePagesDb = + &[ + sql!( + CREATE TABLE welcome_pages ( + workspace_id INTEGER, + item_id INTEGER UNIQUE, + is_open INTEGER DEFAULT FALSE, + + PRIMARY KEY(workspace_id, item_id), + FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) + ON DELETE CASCADE + ) STRICT; + ), + ]; + } + + impl WelcomePagesDb { + query! { + pub async fn save_welcome_page( + item_id: workspace::ItemId, + workspace_id: workspace::WorkspaceId, + is_open: bool + ) -> Result<()> { + INSERT OR REPLACE INTO welcome_pages(item_id, workspace_id, is_open) + VALUES (?, ?, ?) + } + } + + query! { + pub fn get_welcome_page( + item_id: workspace::ItemId, + workspace_id: workspace::WorkspaceId + ) -> Result { + SELECT is_open + FROM welcome_pages + WHERE item_id = ? AND workspace_id = ? + } + } + } +} diff --git a/crates/welcome/Cargo.toml b/crates/welcome/Cargo.toml deleted file mode 100644 index acb3fe0f84..0000000000 --- a/crates/welcome/Cargo.toml +++ /dev/null @@ -1,40 +0,0 @@ -[package] -name = "welcome" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/welcome.rs" - -[features] -test-support = [] - -[dependencies] -anyhow.workspace = true -client.workspace = true -component.workspace = true -db.workspace = true -documented.workspace = true -fuzzy.workspace = true -gpui.workspace = true -install_cli.workspace = true -language.workspace = true -picker.workspace = true -project.workspace = true -serde.workspace = true -settings.workspace = true -telemetry.workspace = true -ui.workspace = true -util.workspace = true -vim_mode_setting.workspace = true -workspace-hack.workspace = true -workspace.workspace = true -zed_actions.workspace = true - -[dev-dependencies] -editor = { workspace = true, features = ["test-support"] } diff --git a/crates/welcome/LICENSE-GPL b/crates/welcome/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/welcome/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/welcome/src/welcome.rs b/crates/welcome/src/welcome.rs deleted file mode 100644 index b0a1c316f4..0000000000 --- a/crates/welcome/src/welcome.rs +++ /dev/null @@ -1,446 +0,0 @@ -use client::{TelemetrySettings, telemetry::Telemetry}; -use db::kvp::KEY_VALUE_STORE; -use gpui::{ - Action, App, Context, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, - ParentElement, Render, Styled, Subscription, Task, WeakEntity, Window, actions, svg, -}; -use language::language_settings::{EditPredictionProvider, all_language_settings}; -use project::DisableAiSettings; -use settings::{Settings, SettingsStore}; -use std::sync::Arc; -use ui::{CheckboxWithLabel, ElevationIndex, Tooltip, prelude::*}; -use util::ResultExt; -use vim_mode_setting::VimModeSetting; -use workspace::{ - AppState, Welcome, Workspace, WorkspaceId, - dock::DockPosition, - item::{Item, ItemEvent}, - open_new, -}; - -pub use multibuffer_hint::*; - -mod base_keymap_picker; -mod multibuffer_hint; - -actions!( - welcome, - [ - /// Resets the welcome screen hints to their initial state. - ResetHints - ] -); - -pub const FIRST_OPEN: &str = "first_open"; -pub const DOCS_URL: &str = "https://zed.dev/docs/"; - -pub fn init(cx: &mut App) { - cx.observe_new(|workspace: &mut Workspace, _, _cx| { - workspace.register_action(|workspace, _: &Welcome, window, cx| { - let welcome_page = WelcomePage::new(workspace, cx); - workspace.add_item_to_active_pane(Box::new(welcome_page), None, true, window, cx) - }); - workspace - .register_action(|_workspace, _: &ResetHints, _, cx| MultibufferHint::set_count(0, cx)); - }) - .detach(); - - base_keymap_picker::init(cx); -} - -pub fn show_welcome_view(app_state: Arc, cx: &mut App) -> Task> { - open_new( - Default::default(), - app_state, - cx, - |workspace, window, cx| { - workspace.toggle_dock(DockPosition::Left, window, cx); - let welcome_page = WelcomePage::new(workspace, cx); - workspace.add_item_to_center(Box::new(welcome_page.clone()), window, cx); - - window.focus(&welcome_page.focus_handle(cx)); - - cx.notify(); - - db::write_and_log(cx, || { - KEY_VALUE_STORE.write_kvp(FIRST_OPEN.to_string(), "false".to_string()) - }); - }, - ) -} - -pub struct WelcomePage { - workspace: WeakEntity, - focus_handle: FocusHandle, - telemetry: Arc, - _settings_subscription: Subscription, -} - -impl Render for WelcomePage { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - let edit_prediction_provider_is_zed = - all_language_settings(None, cx).edit_predictions.provider - == EditPredictionProvider::Zed; - - let edit_prediction_label = if edit_prediction_provider_is_zed { - "Edit Prediction Enabled" - } else { - "Try Edit Prediction" - }; - - h_flex() - .size_full() - .bg(cx.theme().colors().editor_background) - .key_context("Welcome") - .track_focus(&self.focus_handle(cx)) - .child( - v_flex() - .gap_8() - .mx_auto() - .child( - v_flex() - .w_full() - .child( - svg() - .path("icons/logo_96.svg") - .text_color(cx.theme().colors().icon_disabled) - .w(px(40.)) - .h(px(40.)) - .mx_auto() - .mb_4(), - ) - .child( - h_flex() - .w_full() - .justify_center() - .child(Headline::new("Welcome to Zed")), - ) - .child( - h_flex().w_full().justify_center().child( - Label::new("The editor for what's next") - .color(Color::Muted) - .italic(), - ), - ), - ) - .child( - h_flex() - .items_start() - .gap_8() - .child( - v_flex() - .gap_2() - .pr_8() - .border_r_1() - .border_color(cx.theme().colors().border_variant) - .child( - self.section_label( cx).child( - Label::new("Get Started") - .size(LabelSize::XSmall) - .color(Color::Muted), - ), - ) - .child( - Button::new("choose-theme", "Choose a Theme") - .icon(IconName::SwatchBook) - .icon_size(IconSize::XSmall) - .icon_color(Color::Muted) - .icon_position(IconPosition::Start) - .on_click(cx.listener(|this, _, window, cx| { - telemetry::event!("Welcome Theme Changed"); - this.workspace - .update(cx, |_workspace, cx| { - window.dispatch_action(zed_actions::theme_selector::Toggle::default().boxed_clone(), cx); - }) - .ok(); - })), - ) - .child( - Button::new("choose-keymap", "Choose a Keymap") - .icon(IconName::Keyboard) - .icon_size(IconSize::XSmall) - .icon_color(Color::Muted) - .icon_position(IconPosition::Start) - .on_click(cx.listener(|this, _, window, cx| { - telemetry::event!("Welcome Keymap Changed"); - this.workspace - .update(cx, |workspace, cx| { - base_keymap_picker::toggle( - workspace, - &Default::default(), - window, cx, - ) - }) - .ok(); - })), - ) - .when(!DisableAiSettings::get_global(cx).disable_ai, |parent| { - parent.child( - Button::new( - "edit_prediction_onboarding", - edit_prediction_label, - ) - .disabled(edit_prediction_provider_is_zed) - .icon(IconName::ZedPredict) - .icon_size(IconSize::XSmall) - .icon_color(Color::Muted) - .icon_position(IconPosition::Start) - .on_click( - cx.listener(|_, _, window, cx| { - telemetry::event!("Welcome Screen Try Edit Prediction clicked"); - window.dispatch_action(zed_actions::OpenZedPredictOnboarding.boxed_clone(), cx); - }), - ), - ) - }) - .child( - Button::new("edit settings", "Edit Settings") - .icon(IconName::Settings) - .icon_size(IconSize::XSmall) - .icon_color(Color::Muted) - .icon_position(IconPosition::Start) - .on_click(cx.listener(|_, _, window, cx| { - telemetry::event!("Welcome Settings Edited"); - window.dispatch_action(Box::new( - zed_actions::OpenSettings, - ), cx); - })), - ) - - ) - .child( - v_flex() - .gap_2() - .child( - self.section_label(cx).child( - Label::new("Resources") - .size(LabelSize::XSmall) - .color(Color::Muted), - ), - ) - .when(cfg!(target_os = "macos"), |el| { - el.child( - Button::new("install-cli", "Install the CLI") - .icon(IconName::Terminal) - .icon_size(IconSize::XSmall) - .icon_color(Color::Muted) - .icon_position(IconPosition::Start) - .on_click(cx.listener(|this, _, window, cx| { - telemetry::event!("Welcome CLI Installed"); - this.workspace.update(cx, |_, cx|{ - install_cli::install_cli(window, cx); - }).log_err(); - })), - ) - }) - .child( - Button::new("view-docs", "View Documentation") - .icon(IconName::FileCode) - .icon_size(IconSize::XSmall) - .icon_color(Color::Muted) - .icon_position(IconPosition::Start) - .on_click(cx.listener(|_, _, _, cx| { - telemetry::event!("Welcome Documentation Viewed"); - cx.open_url(DOCS_URL); - })), - ) - .child( - Button::new("explore-extensions", "Explore Extensions") - .icon(IconName::Blocks) - .icon_size(IconSize::XSmall) - .icon_color(Color::Muted) - .icon_position(IconPosition::Start) - .on_click(cx.listener(|_, _, window, cx| { - telemetry::event!("Welcome Extensions Page Opened"); - window.dispatch_action(Box::new( - zed_actions::Extensions::default(), - ), cx); - })), - ) - ), - ) - .child( - v_container() - .px_2() - .gap_2() - .child( - h_flex() - .justify_between() - .child( - CheckboxWithLabel::new( - "enable-vim", - Label::new("Enable Vim Mode"), - if VimModeSetting::get_global(cx).0 { - ui::ToggleState::Selected - } else { - ui::ToggleState::Unselected - }, - cx.listener(move |this, selection, _window, cx| { - telemetry::event!("Welcome Vim Mode Toggled"); - this.update_settings::( - selection, - cx, - |setting, value| *setting = Some(value), - ); - }), - ) - .fill() - .elevation(ElevationIndex::ElevatedSurface), - ) - .child( - IconButton::new("vim-mode", IconName::Info) - .icon_size(IconSize::XSmall) - .icon_color(Color::Muted) - .tooltip( - Tooltip::text( - "You can also toggle Vim Mode via the command palette or Editor Controls menu.") - ), - ), - ) - .child( - CheckboxWithLabel::new( - "enable-crash", - Label::new("Send Crash Reports"), - if TelemetrySettings::get_global(cx).diagnostics { - ui::ToggleState::Selected - } else { - ui::ToggleState::Unselected - }, - cx.listener(move |this, selection, _window, cx| { - telemetry::event!("Welcome Diagnostic Telemetry Toggled"); - this.update_settings::(selection, cx, { - move |settings, value| { - settings.diagnostics = Some(value); - telemetry::event!( - "Settings Changed", - setting = "diagnostic telemetry", - value - ); - } - }); - }), - ) - .fill() - .elevation(ElevationIndex::ElevatedSurface), - ) - .child( - CheckboxWithLabel::new( - "enable-telemetry", - Label::new("Send Telemetry"), - if TelemetrySettings::get_global(cx).metrics { - ui::ToggleState::Selected - } else { - ui::ToggleState::Unselected - }, - cx.listener(move |this, selection, _window, cx| { - telemetry::event!("Welcome Metric Telemetry Toggled"); - this.update_settings::(selection, cx, { - move |settings, value| { - settings.metrics = Some(value); - telemetry::event!( - "Settings Changed", - setting = "metric telemetry", - value - ); - } - }); - }), - ) - .fill() - .elevation(ElevationIndex::ElevatedSurface), - ), - ), - ) - } -} - -impl WelcomePage { - pub fn new(workspace: &Workspace, cx: &mut Context) -> Entity { - let this = cx.new(|cx| { - cx.on_release(|_: &mut Self, _| { - telemetry::event!("Welcome Page Closed"); - }) - .detach(); - - WelcomePage { - focus_handle: cx.focus_handle(), - workspace: workspace.weak_handle(), - telemetry: workspace.client().telemetry().clone(), - _settings_subscription: cx - .observe_global::(move |_, cx| cx.notify()), - } - }); - - this - } - - fn section_label(&self, cx: &mut App) -> Div { - div() - .pl_1() - .font_buffer(cx) - .text_color(Color::Muted.color(cx)) - } - - fn update_settings( - &mut self, - selection: &ToggleState, - cx: &mut Context, - callback: impl 'static + Send + Fn(&mut T::FileContent, bool), - ) { - if let Some(workspace) = self.workspace.upgrade() { - let fs = workspace.read(cx).app_state().fs.clone(); - let selection = *selection; - settings::update_settings_file::(fs, cx, move |settings, _| { - let value = match selection { - ToggleState::Unselected => false, - ToggleState::Selected => true, - _ => return, - }; - - callback(settings, value) - }); - } - } -} - -impl EventEmitter for WelcomePage {} - -impl Focusable for WelcomePage { - fn focus_handle(&self, _: &App) -> gpui::FocusHandle { - self.focus_handle.clone() - } -} - -impl Item for WelcomePage { - type Event = ItemEvent; - - fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { - "Welcome".into() - } - - fn telemetry_event_text(&self) -> Option<&'static str> { - Some("Welcome Page Opened") - } - - fn show_toolbar(&self) -> bool { - false - } - - fn clone_on_split( - &self, - _workspace_id: Option, - _: &mut Window, - cx: &mut Context, - ) -> Option> { - Some(cx.new(|cx| WelcomePage { - focus_handle: cx.focus_handle(), - workspace: self.workspace.clone(), - telemetry: self.telemetry.clone(), - _settings_subscription: cx.observe_global::(move |_, cx| cx.notify()), - })) - } - - fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) { - f(*event) - } -} diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 98794e54cd..fb78c62f9e 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -248,8 +248,6 @@ actions!( ToggleZoom, /// Stops following a collaborator. Unfollow, - /// Shows the welcome screen. - Welcome, /// Restores the banner. RestoreBanner, /// Toggles expansion of the selected item. diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 5997e43864..4335f2d5a1 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -2,7 +2,7 @@ description = "The fast, collaborative code editor." edition.workspace = true name = "zed" -version = "0.200.0" +version = "0.201.0" publish.workspace = true license = "GPL-3.0-or-later" authors = ["Zed Team "] @@ -157,7 +157,6 @@ vim_mode_setting.workspace = true watch.workspace = true web_search.workspace = true web_search_providers.workspace = true -welcome.workspace = true workspace-hack.workspace = true workspace.workspace = true zed_actions.workspace = true diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 3084bfddad..fd987ef6c5 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -20,6 +20,7 @@ use gpui::{App, AppContext as _, Application, AsyncApp, Focusable as _, UpdateGl use gpui_tokio::Tokio; use http_client::{Url, read_proxy_from_env}; use language::LanguageRegistry; +use onboarding::{FIRST_OPEN, show_onboarding_view}; use prompt_store::PromptBuilder; use reqwest_client::ReqwestClient; @@ -44,7 +45,6 @@ use theme::{ }; use util::{ResultExt, TryFutureExt, maybe}; use uuid::Uuid; -use welcome::{FIRST_OPEN, show_welcome_view}; use workspace::{ AppState, SerializedWorkspaceLocation, Toast, Workspace, WorkspaceSettings, WorkspaceStore, notifications::NotificationId, @@ -623,7 +623,6 @@ pub fn main() { feedback::init(cx); markdown_preview::init(cx); svg_preview::init(cx); - welcome::init(cx); onboarding::init(cx); settings_ui::init(cx); extensions_ui::init(cx); @@ -1044,7 +1043,7 @@ async fn restore_or_create_workspace(app_state: Arc, cx: &mut AsyncApp } } } else if matches!(KEY_VALUE_STORE.read_kvp(FIRST_OPEN), Ok(None)) { - cx.update(|cx| show_welcome_view(app_state, cx))?.await?; + cx.update(|cx| show_onboarding_view(app_state, cx))?.await?; } else { cx.update(|cx| { workspace::open_new( diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 8c89a7d85a..23020d3a9b 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -34,6 +34,8 @@ use image_viewer::ImageInfo; use language_tools::lsp_tool::{self, LspTool}; use migrate::{MigrationBanner, MigrationEvent, MigrationNotification, MigrationType}; use migrator::{migrate_keymap, migrate_settings}; +use onboarding::DOCS_URL; +use onboarding::multibuffer_hint::MultibufferHint; pub use open_listener::*; use outline_panel::OutlinePanel; use paths::{ @@ -67,7 +69,6 @@ use util::markdown::MarkdownString; use util::{ResultExt, asset_str}; use uuid::Uuid; use vim_mode_setting::VimModeSetting; -use welcome::{DOCS_URL, MultibufferHint}; use workspace::notifications::{NotificationId, dismiss_app_notification, show_app_notification}; use workspace::{ AppState, NewFile, NewWindow, OpenLog, Toast, Workspace, WorkspaceSettings, @@ -3975,7 +3976,6 @@ mod tests { client::init(&app_state.client, cx); language::init(cx); workspace::init(app_state.clone(), cx); - welcome::init(cx); onboarding::init(cx); Project::init_settings(cx); app_state @@ -4380,7 +4380,6 @@ mod tests { "toolchain", "variable_list", "vim", - "welcome", "workspace", "zed", "zed_predict_onboarding", diff --git a/crates/zed/src/zed/app_menus.rs b/crates/zed/src/zed/app_menus.rs index 53eec42ba0..9df55a2fb1 100644 --- a/crates/zed/src/zed/app_menus.rs +++ b/crates/zed/src/zed/app_menus.rs @@ -249,7 +249,7 @@ pub fn app_menus() -> Vec { ), MenuItem::action("View Telemetry", zed_actions::OpenTelemetryLog), MenuItem::action("View Dependency Licenses", zed_actions::OpenLicenses), - MenuItem::action("Show Welcome", workspace::Welcome), + MenuItem::action("Show Welcome", onboarding::ShowWelcome), MenuItem::action("Give Feedback...", zed_actions::feedback::GiveFeedback), MenuItem::separator(), MenuItem::action( diff --git a/crates/zed/src/zed/component_preview.rs b/crates/zed/src/zed/component_preview.rs index ac889a7ad9..4609ecce9b 100644 --- a/crates/zed/src/zed/component_preview.rs +++ b/crates/zed/src/zed/component_preview.rs @@ -761,7 +761,7 @@ impl Render for ComponentPreview { ) .track_scroll(self.nav_scroll_handle.clone()) .p_2p5() - .w(px(229.)) + .w(px(231.)) // Matches perfectly with the size of the "Component Preview" tab, if that's the first one in the pane .h_full() .flex_1(), ) diff --git a/crates/zed/src/zed/open_listener.rs b/crates/zed/src/zed/open_listener.rs index 2fd9b0a68c..82d3795e94 100644 --- a/crates/zed/src/zed/open_listener.rs +++ b/crates/zed/src/zed/open_listener.rs @@ -15,6 +15,8 @@ use futures::{FutureExt, SinkExt, StreamExt}; use git_ui::file_diff_view::FileDiffView; use gpui::{App, AsyncApp, Global, WindowHandle}; use language::Point; +use onboarding::FIRST_OPEN; +use onboarding::show_onboarding_view; use recent_projects::{SshSettings, open_ssh_project}; use remote::SshConnectionOptions; use settings::Settings; @@ -24,7 +26,6 @@ use std::thread; use std::time::Duration; use util::ResultExt; use util::paths::PathWithPosition; -use welcome::{FIRST_OPEN, show_welcome_view}; use workspace::item::ItemHandle; use workspace::{AppState, OpenOptions, SerializedWorkspaceLocation, Workspace}; @@ -378,7 +379,7 @@ async fn open_workspaces( if grouped_locations.is_empty() { // If we have no paths to open, show the welcome screen if this is the first launch if matches!(KEY_VALUE_STORE.read_kvp(FIRST_OPEN), Ok(None)) { - cx.update(|cx| show_welcome_view(app_state, cx).detach()) + cx.update(|cx| show_onboarding_view(app_state, cx).detach()) .log_err(); } // If not the first launch, show an empty window with empty editor diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index fc936d6bd0..c7af36f431 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -21,6 +21,7 @@ - [Icon Themes](./icon-themes.md) - [Visual Customization](./visual-customization.md) - [Vim Mode](./vim.md) +- [Helix Mode](./helix.md) diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index 1996e1c4ee..5d11dfe833 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -3195,10 +3195,16 @@ Run the `theme selector: toggle` action in the command palette to see a current ## Vim -- Description: Whether or not to enable vim mode (work in progress). +- Description: Whether or not to enable vim mode. See the [Vim documentation](./vim.md) for more details on configuration. - Setting: `vim_mode` - Default: `false` +## Helix Mode + +- Description: Whether or not to enable Helix mode. Enabling `helix_mode` also enables `vim_mode`. See the [Helix documentation](./helix.md) for more details. +- Setting: `helix_mode` +- Default: `false` + ## Project Panel - Description: Customize project panel diff --git a/docs/src/helix.md b/docs/src/helix.md new file mode 100644 index 0000000000..ddf997d3f0 --- /dev/null +++ b/docs/src/helix.md @@ -0,0 +1,11 @@ +# Helix Mode + +_Work in progress! Not all Helix keybindings are implemented yet._ + +Zed's Helix mode is an emulation layer that brings Helix-style keybindings and modal editing to Zed. It builds upon Zed's [Vim mode](./vim.md), so much of the core functionality is shared. Enabling `helix_mode` will also enable `vim_mode`. + +For a guide on Vim-related features that are also available in Helix mode, please refer to our [Vim mode documentation](./vim.md). + +To check the current status of Helix mode, or to request a missing Helix feature, checkout out the ["Are we Helix yet?" discussion](https://github.com/zed-industries/zed/discussions/33580). + +For a detailed list of Helix's default keybindings, please visit the [official Helix documentation](https://docs.helix-editor.com/keymap.html). diff --git a/docs/src/key-bindings.md b/docs/src/key-bindings.md index feed912787..9fc94840b7 100644 --- a/docs/src/key-bindings.md +++ b/docs/src/key-bindings.md @@ -14,7 +14,7 @@ If you're used to a specific editor's defaults you can set a `base_keymap` in yo - TextMate - None (disables _all_ key bindings) -You can also enable `vim_mode`, which adds vim bindings too. +You can also enable `vim_mode` or `helix_mode`, which add modal bindings. For more information, see the documentation for [Vim mode](./vim.md) and [Helix mode](./helix.md). ## User keymaps @@ -119,7 +119,7 @@ It's worth noting that attributes are only available on the node they are define Note: Before Zed v0.197.x, the ! operator only looked at one node at a time, and `>` meant "parent" not "ancestor". This meant that `!Editor` would match the context `Workspace > Pane > Editor`, because (confusingly) the Pane matches `!Editor`, and that `os=macos > Editor` did not match the context `Workspace > Pane > Editor` because of the intermediate `Pane` node. -If you're using Vim mode, we have information on how [vim modes influence the context](./vim.md#contexts) +If you're using Vim mode, we have information on how [vim modes influence the context](./vim.md#contexts). Helix mode is built on top of Vim mode and uses the same contexts. ### Actions diff --git a/docs/src/telemetry.md b/docs/src/telemetry.md index 107aef5a96..46c39a88ae 100644 --- a/docs/src/telemetry.md +++ b/docs/src/telemetry.md @@ -4,7 +4,8 @@ Zed collects anonymous telemetry data to help the team understand how people are ## Configuring Telemetry Settings -You have full control over what data is sent out by Zed. To enable or disable some or all telemetry types, open your `settings.json` file via {#action zed::OpenSettings}({#kb zed::OpenSettings}) from the command palette. +You have full control over what data is sent out by Zed. +To enable or disable some or all telemetry types, open your `settings.json` file via {#action zed::OpenSettings}({#kb zed::OpenSettings}) from the command palette. Insert and tweak the following: @@ -15,8 +16,6 @@ Insert and tweak the following: }, ``` -The telemetry settings can also be configured via the welcome screen, which can be invoked via the {#action workspace::Welcome} action in the command palette. - ## Dataflow Telemetry is sent from the application to our servers. Data is proxied through our servers to enable us to easily switch analytics services. We currently use: diff --git a/extensions/emmet/Cargo.toml b/extensions/emmet/Cargo.toml index ff9debdea9..2fbdf2a7e5 100644 --- a/extensions/emmet/Cargo.toml +++ b/extensions/emmet/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zed_emmet" -version = "0.0.5" +version = "0.0.6" edition.workspace = true publish.workspace = true license = "Apache-2.0" diff --git a/extensions/emmet/extension.toml b/extensions/emmet/extension.toml index 0ebb801f9d..a1848400b8 100644 --- a/extensions/emmet/extension.toml +++ b/extensions/emmet/extension.toml @@ -1,7 +1,7 @@ id = "emmet" name = "Emmet" description = "Emmet support" -version = "0.0.5" +version = "0.0.6" schema_version = 1 authors = ["Piotr Osiewicz "] repository = "https://github.com/zed-industries/zed" diff --git a/extensions/emmet/src/emmet.rs b/extensions/emmet/src/emmet.rs index e4fb3cf814..1434e16e88 100644 --- a/extensions/emmet/src/emmet.rs +++ b/extensions/emmet/src/emmet.rs @@ -5,7 +5,7 @@ struct EmmetExtension { did_find_server: bool, } -const SERVER_PATH: &str = "node_modules/.bin/emmet-language-server"; +const SERVER_PATH: &str = "node_modules/@olrtg/emmet-language-server/dist/index.js"; const PACKAGE_NAME: &str = "@olrtg/emmet-language-server"; impl EmmetExtension {