diff --git a/crates/git_ui/src/git_ui.rs b/crates/git_ui/src/git_ui.rs index 1a9866fcc6e7ef420742620dab3faa2f38bfa5f5..25813e52fbec93f34ffba8485681cd1c6262f38a 100644 --- a/crates/git_ui/src/git_ui.rs +++ b/crates/git_ui/src/git_ui.rs @@ -47,10 +47,25 @@ pub mod stash_picker; pub mod text_diff_view; pub mod worktree_picker; +const BRANCH_DIFF_TIP_MESSAGE: &str = " +Want to review your changes but you've already made some commits? Use the Branch \ +Diff to see the changes compared to the default branch. +"; + pub fn init(cx: &mut App) { editor::set_blame_renderer(blame_ui::GitBlameRenderer, cx); commit_view::init(cx); + workspace::welcome::register_tip( + workspace::welcome::Tip { + title: "Review Your Branch Diff".into(), + message: BRANCH_DIFF_TIP_MESSAGE.into(), + icon: Some(ui::IconName::GitBranch), + mentioned_actions: vec![Box::new(project_diff::BranchDiff)], + }, + cx, + ); + cx.observe_new(|editor: &mut Editor, _, cx| { conflict_view::register_editor(editor, editor.buffer().clone(), cx); }) diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index 9d7fe83736be8d1d9ed79d85708c5ed0574b7e3a..9631e389787cf9d5aab139bec29a6ebcb4d111e6 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/crates/settings_ui/src/settings_ui.rs @@ -387,11 +387,21 @@ struct SettingsFieldMetadata { should_do_titlecase: Option, } +// const SETTINGS_PROFILES_TIP_MESSAGE: &str = " + +// "; + pub fn init(cx: &mut App) { init_renderers(cx); let queue = ProjectSettingsUpdateQueue::new(cx); cx.set_global(queue); + // workspace::welcome::register_tip(workspace::welcome::Tip { + // icon: Some(IconName::Settings), + // title: "Quickly switch settings profiles".into(), + // message + // }, cx); + cx.observe_new(|workspace: &mut workspace::Workspace, _, _| { workspace .register_action( diff --git a/crates/workspace/src/welcome.rs b/crates/workspace/src/welcome.rs index 92f1cb4840731bedda5b0b6751f44bfdcdb8ea52..4eb096d49592cacc798861c90692567d72e69175 100644 --- a/crates/workspace/src/welcome.rs +++ b/crates/workspace/src/welcome.rs @@ -2,19 +2,21 @@ use crate::{ NewFile, Open, PathList, SerializedWorkspaceLocation, WORKSPACE_DB, Workspace, WorkspaceId, item::{Item, ItemEvent}, }; -use chrono::{DateTime, Utc}; +use chrono::{DateTime, Datelike, Utc}; use git::Clone as GitClone; use gpui::WeakEntity; use gpui::{ - Action, App, Context, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, - ParentElement, Render, Styled, Task, Window, actions, + Action, App, Context, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, Global, + InteractiveElement, ParentElement, Render, Styled, Task, Window, actions, }; use menu::{SelectNext, SelectPrevious}; use project::DisableAiSettings; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::Settings; -use ui::{ButtonLike, Divider, DividerColor, KeyBinding, Vector, VectorName, prelude::*}; +use ui::{ + ButtonLike, Divider, DividerColor, IconButtonShape, KeyBinding, Vector, VectorName, prelude::*, +}; use util::ResultExt; use zed_actions::{Extensions, OpenOnboarding, OpenSettings, agent, command_palette}; @@ -33,6 +35,70 @@ actions!( ] ); +actions!( + welcome, + [ + /// Show the next tip of the day + NextTip, + /// Show the previous tip of the day + PreviousTip, + ] +); + +pub struct Tip { + pub title: SharedString, + pub message: SharedString, + pub icon: Option, + pub mentioned_actions: Vec>, +} + +#[derive(Default)] +struct TipRegistry { + tips: Vec, +} + +impl Global for TipRegistry {} + +pub fn register_tip(tip: Tip, cx: &mut App) { + cx.default_global::().tips.push(tip); +} + +fn humanize_action_name(name: &str) -> String { + let capacity = name.len() + name.chars().filter(|c| c.is_uppercase()).count(); + let mut result = String::with_capacity(capacity); + for char in name.chars() { + if char == ':' { + if result.ends_with(':') { + result.push(' '); + } else { + result.push(':'); + } + } else if char == '_' { + result.push(' '); + } else if char.is_uppercase() { + if !result.ends_with(' ') { + result.push(' '); + } + result.extend(char.to_lowercase()); + } else { + result.push(char); + } + } + result +} + +fn tip_index_for_today(cx: &App) -> usize { + let Some(registry) = cx.try_global::() else { + return 0; + }; + let count = registry.tips.len(); + if count == 0 { + return 0; + } + let today = Utc::now().date_naive(); + today.num_days_from_ce() as usize % count +} + #[derive(IntoElement)] struct SectionHeader { title: SharedString, @@ -246,6 +312,7 @@ pub struct WelcomePage { workspace: WeakEntity, focus_handle: FocusHandle, fallback_to_recent_projects: bool, + tip_index: usize, recent_workspaces: Option< Vec<( WorkspaceId, @@ -292,6 +359,7 @@ impl WelcomePage { workspace, focus_handle, fallback_to_recent_projects, + tip_index: tip_index_for_today(cx), recent_workspaces: None, } } @@ -306,6 +374,26 @@ impl WelcomePage { cx.notify(); } + fn next_tip(&mut self, _: &NextTip, _window: &mut Window, cx: &mut Context) { + let count = cx + .try_global::() + .map_or(0, |r| r.tips.len()); + if count > 0 { + self.tip_index = (self.tip_index + 1) % count; + cx.notify(); + } + } + + fn previous_tip(&mut self, _: &PreviousTip, _window: &mut Window, cx: &mut Context) { + let count = cx + .try_global::() + .map_or(0, |r| r.tips.len()); + if count > 0 { + self.tip_index = (self.tip_index + count - 1) % count; + cx.notify(); + } + } + fn open_recent_project( &mut self, action: &OpenRecentProject, @@ -377,6 +465,115 @@ impl WelcomePage { self.focus_handle.clone(), ) } + + fn render_tip_section(&self, cx: &App) -> Option { + let registry = cx.try_global::()?; + let tip = registry.tips.get(self.tip_index)?; + let focus = &self.focus_handle; + + Some( + v_flex() + .w_full() + .p_4() + .border_1() + .border_color(cx.theme().colors().border_variant) + .rounded_md() + .bg(cx.theme().colors().surface_background) + .gap_3() + .child( + h_flex() + .justify_between() + .items_center() + .child( + Label::new("TIP OF THE DAY") + .buffer_font(cx) + .color(Color::Muted) + .size(LabelSize::XSmall), + ) + .child( + h_flex() + .gap_1() + .items_center() + .child( + IconButton::new("prev-tip", IconName::ChevronLeft) + .shape(IconButtonShape::Square) + .icon_size(IconSize::Small) + .on_click(|_, window, cx| { + window + .dispatch_action(PreviousTip.boxed_clone(), cx); + }), + ) + .child( + IconButton::new("next-tip", IconName::ChevronRight) + .shape(IconButtonShape::Square) + .icon_size(IconSize::Small) + .on_click(|_, window, cx| { + window + .dispatch_action(NextTip.boxed_clone(), cx); + }), + ), + ), + ) + .child( + h_flex() + .gap_2() + .items_center() + .when_some(tip.icon, |this, icon| { + this.child( + Icon::new(icon).size(IconSize::Medium).color(Color::Accent), + ) + }) + .child( + Label::new(tip.title.clone()) + .weight(FontWeight::BOLD) + .size(LabelSize::Large), + ), + ) + .child( + Label::new(tip.message.trim().to_string()) + .color(Color::Muted) + .size(LabelSize::Small), + ) + .when(!tip.mentioned_actions.is_empty(), |this| { + this.child( + h_flex() + .gap_2() + .items_center() + .flex_wrap() + .child( + Label::new("Try it out:") + .color(Color::Muted) + .size(LabelSize::Small) + .weight(FontWeight::BOLD), + ) + .children(tip.mentioned_actions.iter().map(|action| { + let action_name = humanize_action_name(action.name()); + let action_clone = action.boxed_clone(); + let focus_handle = focus.clone(); + ButtonLike::new(SharedString::from(format!( + "tip-action-{action_name}" + ))) + .size(ButtonSize::Compact) + .child( + h_flex() + .gap_1p5() + .items_center() + .child( + Label::new(action_name).size(LabelSize::Small), + ) + .child(KeyBinding::for_action_in( + action.as_ref(), focus, cx, + )), + ) + .on_click(move |_, window, cx| { + focus_handle + .dispatch_action(&*action_clone, window, cx); + }) + })), + ) + }), + ) + } } impl Render for WelcomePage { @@ -418,6 +615,8 @@ impl Render for WelcomePage { .on_action(cx.listener(Self::select_previous)) .on_action(cx.listener(Self::select_next)) .on_action(cx.listener(Self::open_recent_project)) + .on_action(cx.listener(Self::next_tip)) + .on_action(cx.listener(Self::previous_tip)) .size_full() .justify_center() .overflow_hidden() @@ -430,12 +629,15 @@ impl Render for WelcomePage { .max_w(px(1100.)) .child( v_flex() + .id("welcome-content") .flex_1() - .justify_center() + .h_full() .max_w_128() .mx_auto() .gap_6() .overflow_x_hidden() + .overflow_y_scroll() + .child(div().flex_grow()) .child( h_flex() .w_full() @@ -454,6 +656,7 @@ impl Render for WelcomePage { ) .child(first_section.render(Default::default(), &self.focus_handle, cx)) .child(second_section) + .children(self.render_tip_section(cx)) .when(!self.fallback_to_recent_projects, |this| { this.child( v_flex().gap_1().child(Divider::horizontal()).child( @@ -469,7 +672,8 @@ impl Render for WelcomePage { }), ), ) - }), + }) + .child(div().flex_grow()), ), ) } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index b57b5028a4e5558b1f90c715463165ba68d914e3..f87e5d5e4e9c95c86cad0741caf9e36969ae5322 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -723,6 +723,23 @@ pub fn init(app_state: Arc, cx: &mut App) { toast_layer::init(cx); history_manager::init(app_state.fs.clone(), cx); + welcome::register_tip( + welcome::Tip { + title: "Master the Command Palette".into(), + message: "The command palette is your gateway to everything in Zed. Instead of \ + hunting through menus, you can quickly find and execute any command by typing a \ + few letters of its name. It supports fuzzy matching, so you don't need to \ + remember exact command names. Whether you want to change your theme, toggle a \ + panel, run a task, or trigger a Git operation, the command palette has you \ + covered. Try building muscle memory by using it for actions you'd normally reach \ + for with a mouse." + .into(), + icon: Some(ui::IconName::Sparkle), + mentioned_actions: vec![Box::new(zed_actions::command_palette::Toggle)], + }, + cx, + ); + cx.on_action(|_: &CloseWindow, cx| Workspace::close_global(cx)) .on_action(|_: &Reload, cx| reload(cx)) .on_action({