diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index d13360a7c5403d997cfb2363f33cfe3b257dcef1..ac345ab4f101f77ce63afd1bec7df29ae81b8597 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -680,29 +680,8 @@ impl PickerDelegate for CommandPaletteDelegate { } } -pub 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 -} +/// Re-export for external callers that were using this from command_palette. +pub use gpui::humanize_action_name; impl std::fmt::Debug for Command { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { diff --git a/crates/gpui/src/action.rs b/crates/gpui/src/action.rs index 1ab619ff171dbeab8a0843393874e7184320e0db..a6474c9e9c6f69e5b46ddaf57e65eac3d54a70fd 100644 --- a/crates/gpui/src/action.rs +++ b/crates/gpui/src/action.rs @@ -187,6 +187,33 @@ impl dyn Action { } } +/// Converts a raw action name like `"editor::GoToDefinition"` into a +/// human-readable form like `"editor: go to definition"`, matching the +/// display style used in the command palette. +pub 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 +} + /// Error type for `Keystroke::parse`. This is used instead of `anyhow::Error` so that Zed can use /// markdown to display it. #[derive(Debug)] diff --git a/crates/workspace/src/welcome.rs b/crates/workspace/src/welcome.rs index 4eb096d49592cacc798861c90692567d72e69175..4b3d4ec16e63e22e38fc9da2d8e129b7bb2f4ea8 100644 --- a/crates/workspace/src/welcome.rs +++ b/crates/workspace/src/welcome.rs @@ -8,14 +8,17 @@ use gpui::WeakEntity; use gpui::{ Action, App, Context, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, Global, InteractiveElement, ParentElement, Render, Styled, Task, Window, actions, + humanize_action_name, }; +use markdown::{Markdown, MarkdownElement, MarkdownFont, MarkdownStyle}; use menu::{SelectNext, SelectPrevious}; use project::DisableAiSettings; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::Settings; use ui::{ - ButtonLike, Divider, DividerColor, IconButtonShape, KeyBinding, Vector, VectorName, prelude::*, + ButtonLike, Divider, DividerColor, IconButtonShape, KeyBinding, TextSize, Vector, VectorName, + prelude::*, }; use util::ResultExt; use zed_actions::{Extensions, OpenOnboarding, OpenSettings, agent, command_palette}; @@ -63,30 +66,6 @@ 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; @@ -313,6 +292,7 @@ pub struct WelcomePage { focus_handle: FocusHandle, fallback_to_recent_projects: bool, tip_index: usize, + tip_message_markdown: Option>, recent_workspaces: Option< Vec<( WorkspaceId, @@ -355,15 +335,40 @@ impl WelcomePage { .detach(); } + let tip_index = tip_index_for_today(cx); + let tip_message_markdown = Self::build_tip_markdown(tip_index, &workspace, cx); + WelcomePage { workspace, focus_handle, fallback_to_recent_projects, - tip_index: tip_index_for_today(cx), + tip_index, + tip_message_markdown, recent_workspaces: None, } } + fn build_tip_markdown( + tip_index: usize, + workspace: &WeakEntity, + cx: &mut Context, + ) -> Option> { + let registry = cx.try_global::()?; + let tip = registry.tips.get(tip_index)?; + let message = tip.message.trim().to_string(); + let language_registry = workspace + .upgrade() + .map(|ws| ws.read(cx).app_state().languages.clone()); + Some(cx.new(|cx| Markdown::new(message.into(), language_registry, None, cx))) + } + + fn set_tip_index(&mut self, index: usize, cx: &mut Context) { + self.tip_index = index; + self.tip_message_markdown = + Self::build_tip_markdown(self.tip_index, &self.workspace, cx); + cx.notify(); + } + fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context) { window.focus_next(cx); cx.notify(); @@ -379,8 +384,7 @@ impl WelcomePage { .try_global::() .map_or(0, |r| r.tips.len()); if count > 0 { - self.tip_index = (self.tip_index + 1) % count; - cx.notify(); + self.set_tip_index((self.tip_index + 1) % count, cx); } } @@ -389,8 +393,7 @@ impl WelcomePage { .try_global::() .map_or(0, |r| r.tips.len()); if count > 0 { - self.tip_index = (self.tip_index + count - 1) % count; - cx.notify(); + self.set_tip_index((self.tip_index + count - 1) % count, cx); } } @@ -466,23 +469,25 @@ impl WelcomePage { ) } - fn render_tip_section(&self, cx: &App) -> Option { + fn render_tip_section( + &self, + window: &mut Window, + cx: &App, + ) -> Option { let registry = cx.try_global::()?; let tip = registry.tips.get(self.tip_index)?; let focus = &self.focus_handle; + let tip_markdown = self.tip_message_markdown.clone()?; 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() + .px_1() + .mb_2() + .gap_2() .items_center() .child( Label::new("TIP OF THE DAY") @@ -490,9 +495,13 @@ impl WelcomePage { .color(Color::Muted) .size(LabelSize::XSmall), ) + .child( + Divider::horizontal().color(DividerColor::BorderVariant), + ) .child( h_flex() .gap_1() + .flex_shrink_0() .items_center() .child( IconButton::new("prev-tip", IconName::ChevronLeft) @@ -530,9 +539,13 @@ impl WelcomePage { ), ) .child( - Label::new(tip.message.trim().to_string()) - .color(Color::Muted) - .size(LabelSize::Small), + MarkdownElement::new(tip_markdown, tip_markdown_style(window, cx)) + .text_size(TextSize::Default.rems(cx)) + .code_block_renderer(markdown::CodeBlockRenderer::Default { + copy_button: false, + copy_button_on_hover: false, + border: true, + }), ) .when(!tip.mentioned_actions.is_empty(), |this| { this.child( @@ -576,8 +589,14 @@ impl WelcomePage { } } +fn tip_markdown_style(window: &Window, cx: &App) -> MarkdownStyle { + let mut style = MarkdownStyle::themed(MarkdownFont::Editor, window, cx); + style.base_text_style.color = cx.theme().colors().text_muted; + style +} + impl Render for WelcomePage { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let (first_section, second_section) = CONTENT; let first_section_entries = first_section.entries.len(); let last_index = first_section_entries + second_section.entries.len(); @@ -656,7 +675,7 @@ impl Render for WelcomePage { ) .child(first_section.render(Default::default(), &self.focus_handle, cx)) .child(second_section) - .children(self.render_tip_section(cx)) + .children(self.render_tip_section(window, cx)) .when(!self.fallback_to_recent_projects, |this| { this.child( v_flex().gap_1().child(Divider::horizontal()).child( diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index f87e5d5e4e9c95c86cad0741caf9e36969ae5322..29863aaca5bb683d5eaabfc16e3dcbf99ad07bd3 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -717,6 +717,15 @@ pub fn prompt_for_open_path_and_open( .detach(); } +const MESSAGE: &str = r#" +command palette is good. use it + +hey look markdown: +```json +{"fun": "times"} +``` +"#; + pub fn init(app_state: Arc, cx: &mut App) { component::init(); theme_preview::init(cx); @@ -726,14 +735,7 @@ pub fn init(app_state: Arc, cx: &mut App) { 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(), + message: MESSAGE.into(), icon: Some(ui::IconName::Sparkle), mentioned_actions: vec![Box::new(zed_actions::command_palette::Toggle)], },