From 545b06f01065b2e61da916fba41fa18c6897805c Mon Sep 17 00:00:00 2001 From: cameron Date: Fri, 13 Mar 2026 03:34:56 +0000 Subject: [PATCH] my friend claude is great at ~~stealing~~ taking inspiration from hidden gems --- crates/editor/src/editor.rs | 104 ++++++++++++++++++ crates/file_finder/src/file_finder.rs | 17 +++ .../src/language_selector.rs | 17 +++ crates/outline/src/outline.rs | 17 +++ crates/project_panel/src/project_panel.rs | 16 +++ crates/search/src/search.rs | 31 ++++++ .../src/settings_profile_selector.rs | 17 +++ crates/tasks_ui/src/tasks_ui.rs | 31 ++++++ crates/terminal_view/src/terminal_view.rs | 17 +++ crates/vim/src/vim.rs | 12 ++ crates/workspace/src/workspace.rs | 76 +++++++++++-- 11 files changed, 346 insertions(+), 9 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index dc2696eb2ca83999934cab6cdee82e364657c70e..6d830d9870ca19b9dbe6c74d20ac88488afc9a36 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -332,6 +332,46 @@ pub enum HideMouseCursorOrigin { MovementAction, } +const CURSOR_FOR_EVERY_LINE_TIP_MESSAGE: &str = "\ +Need a cursor on every line to manipulate text in bulk? \ +Select a block of text, then use `editor: split selection into lines` \ +to place a cursor at the end of each line. \ +Now you can edit all lines simultaneously with multicursors."; + +const COPY_AND_TRIM_TIP_MESSAGE: &str = "\ +When copying indented code, extra leading whitespace can be annoying. \ +Use `editor: copy and trim` instead of a regular copy \u{2014} \ +it strips the common leading indentation from the selected lines, \ +giving you a cleaner paste."; + +const DIFF_CLIPBOARD_TIP_MESSAGE: &str = "\ +Found two similar code blocks and need to spot the differences? \ +Copy one block to your clipboard, select the other, then run \ +`editor: diff clipboard with selection` from the command palette. \ +The diff view shows exactly what varies between them."; + +const SELECTIONS_IN_MULTIBUFFER_TIP_MESSAGE: &str = "\ +When using multicursors across a large file, reviewing scattered \ +selections can be tedious. Run `editor: open selections in multibuffer` \ +to consolidate all your selections into a compact view where \ +they're just a few lines apart."; + +const QUICK_NEW_LINES_TIP_MESSAGE: &str = "\ +No need to move your cursor to the end of a line before pressing Enter. \ +Use `editor: newline below` to start a new line below the current one, \ +or `editor: newline above` to insert one above \u{2014} \ +your cursor jumps right to the new line."; + +const RUN_NEAREST_TEST_TIP_MESSAGE: &str = "\ +Iterating on a test? Use `editor: spawn nearest task` to run the test \ +closest to your cursor. It's the most convenient keyboard-driven way \ +to repeatedly run a single test without navigating to the terminal."; + +const SELECT_ALL_MATCHES_TIP_MESSAGE: &str = "\ +Place your cursor on a word and use `editor: select all matches` \ +to instantly create selections on every occurrence of that word \ +in the buffer. Then edit them all at once with multicursors."; + pub fn init(cx: &mut App) { cx.set_global(GlobalBlameRenderer(Arc::new(()))); cx.set_global(breadcrumbs::RenderBreadcrumbText(render_breadcrumb_text)); @@ -387,6 +427,70 @@ pub fn init(cx: &mut App) { )) as Arc }); _ = multi_buffer::EXCERPT_CONTEXT_LINES.set(multibuffer_context_lines); + + workspace::welcome::register_tip( + workspace::welcome::Tip { + title: "A Cursor for Every Line".into(), + message: CURSOR_FOR_EVERY_LINE_TIP_MESSAGE.into(), + icon: Some(ui::IconName::CursorIBeam), + mentioned_actions: vec![Box::new(SplitSelectionIntoLines::default())], + }, + cx, + ); + workspace::welcome::register_tip( + workspace::welcome::Tip { + title: "Copy and Trim".into(), + message: COPY_AND_TRIM_TIP_MESSAGE.into(), + icon: Some(ui::IconName::Copy), + mentioned_actions: vec![Box::new(CopyAndTrim)], + }, + cx, + ); + workspace::welcome::register_tip( + workspace::welcome::Tip { + title: "Diff Clipboard With Selection".into(), + message: DIFF_CLIPBOARD_TIP_MESSAGE.into(), + icon: Some(ui::IconName::Diff), + mentioned_actions: vec![Box::new(DiffClipboardWithSelection)], + }, + cx, + ); + workspace::welcome::register_tip( + workspace::welcome::Tip { + title: "Gather Your Selections in a Multibuffer".into(), + message: SELECTIONS_IN_MULTIBUFFER_TIP_MESSAGE.into(), + icon: Some(ui::IconName::SelectAll), + mentioned_actions: vec![Box::new(OpenSelectionsInMultibuffer)], + }, + cx, + ); + workspace::welcome::register_tip( + workspace::welcome::Tip { + title: "Quick New Lines Above and Below".into(), + message: QUICK_NEW_LINES_TIP_MESSAGE.into(), + icon: Some(ui::IconName::Return), + mentioned_actions: vec![Box::new(NewlineBelow), Box::new(NewlineAbove)], + }, + cx, + ); + workspace::welcome::register_tip( + workspace::welcome::Tip { + title: "Run the Nearest Test".into(), + message: RUN_NEAREST_TEST_TIP_MESSAGE.into(), + icon: Some(ui::IconName::PlayFilled), + mentioned_actions: vec![Box::new(SpawnNearestTask::default())], + }, + cx, + ); + workspace::welcome::register_tip( + workspace::welcome::Tip { + title: "Select All Matches".into(), + message: SELECT_ALL_MATCHES_TIP_MESSAGE.into(), + icon: Some(ui::IconName::SelectAll), + mentioned_actions: vec![Box::new(SelectAllMatches)], + }, + cx, + ); } pub fn set_blame_renderer(renderer: impl BlameRenderer + 'static, cx: &mut App) { diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index a1e64964ff578ed263e9e89a610997423f33f7c0..b318f959b7e859761d792e9ec8ac763dc7856156 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -88,7 +88,24 @@ pub struct FileFinder { init_modifiers: Option, } +const TELESCOPE_TIP_MESSAGE: &str = "\ +Prefer a fuzzy file finder like Vim's Telescope? Install `television` on \ +your system, then create a custom task in `tasks.json` with \ +`\"command\": \"zed \\\"$(tv files)\\\"\"` and `\"hide\": \"always\"`. Bind it to \ +`cmd-p` / `ctrl-p` using `task::Spawn` for a Telescope-like experience \ +right in Zed."; + pub fn init(cx: &mut App) { + workspace::welcome::register_tip( + workspace::welcome::Tip { + title: "Emulate Vim's Telescope".into(), + message: TELESCOPE_TIP_MESSAGE.into(), + icon: Some(ui::IconName::MagnifyingGlass), + mentioned_actions: vec![], + }, + cx, + ); + cx.observe_new(FileFinder::register).detach(); cx.observe_new(OpenPathPrompt::register).detach(); cx.observe_new(OpenPathPrompt::register_new_path).detach(); diff --git a/crates/language_selector/src/language_selector.rs b/crates/language_selector/src/language_selector.rs index 17a39d4979a1321a4b0e612bff228f186098babf..521aa360fefae3fbbb730d44716036f10c25114a 100644 --- a/crates/language_selector/src/language_selector.rs +++ b/crates/language_selector/src/language_selector.rs @@ -26,7 +26,24 @@ actions!( ] ); +const FILE_TYPES_TIP_MESSAGE: &str = "\ +Zed doesn't recognize your file extension? Use the `file_types` setting to \ +map it permanently. For example, `\"file_types\": { \"JavaScript\": \ +[\"*.jsm\"] }` makes Zed treat `.jsm` files as JavaScript — with full \ +syntax highlighting and language server support. Check the extensions store \ +first, though, in case direct support already exists."; + pub fn init(cx: &mut App) { + workspace::welcome::register_tip( + workspace::welcome::Tip { + title: "Map Unknown File Types to Known Languages".into(), + message: FILE_TYPES_TIP_MESSAGE.into(), + icon: Some(ui::IconName::FileCode), + mentioned_actions: vec![], + }, + cx, + ); + cx.observe_new(LanguageSelector::register).detach(); } diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs index 454f6f0b578ce25785f0a356251c8af64776772f..23f13ee51a923f169d7fd66a035e844e3411dc2f 100644 --- a/crates/outline/src/outline.rs +++ b/crates/outline/src/outline.rs @@ -22,6 +22,13 @@ use ui::{ListItem, ListItemSpacing, prelude::*}; use util::ResultExt; use workspace::{DismissDecision, ModalView}; +const OUTLINE_TIP_MESSAGE: &str = "\ +The outline modal (`outline: toggle`) filters symbols by name. But if you include \ +a space in your query, it also matches contextual keywords like visibility modifiers \ +and declaration types. Try queries like `pub fn` to find all public functions, \ +`trait ` (with a trailing space) for trait definitions, or `impl Foo for` to find \ +trait implementations."; + pub fn init(cx: &mut App) { cx.observe_new(OutlineView::register).detach(); zed_actions::outline::TOGGLE_OUTLINE @@ -33,6 +40,16 @@ pub fn init(cx: &mut App) { toggle(editor, &Default::default(), window, cx); }) .ok(); + + workspace::welcome::register_tip( + workspace::welcome::Tip { + title: "Filter by Context in the Outline".into(), + message: OUTLINE_TIP_MESSAGE.into(), + icon: Some(ui::IconName::ListTree), + mentioned_actions: vec![Box::new(zed_actions::outline::ToggleOutline)], + }, + cx, + ); } pub fn toggle( diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 068fb8d71fa883e9d2b518c7d19adacea74fadcb..f7b9d1533b5422980cddce07148250c6c21f919b 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -443,7 +443,23 @@ impl FoldedAncestors { } } +const COMPARE_MARKED_FILES_TIP_MESSAGE: &str = "\ +Need to compare two files? In the project panel, select multiple files \ +(hold `cmd`/`ctrl` while clicking), then right-click and choose \ +\"Compare Marked Files\" to open a diff view of the two most recently \ +selected files."; + pub fn init(cx: &mut App) { + workspace::welcome::register_tip( + workspace::welcome::Tip { + title: "Compare Marked Files".into(), + message: COMPARE_MARKED_FILES_TIP_MESSAGE.into(), + icon: Some(ui::IconName::Diff), + mentioned_actions: vec![Box::new(CompareMarkedFiles)], + }, + cx, + ); + cx.observe_new(|workspace: &mut Workspace, _, _| { workspace.register_action(|workspace, _: &ToggleFocus, window, cx| { workspace.toggle_panel_focus::(window, cx); diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index d2104492bebf529821f8ad8571fd3fbb8bdbc69e..efb62a8d91a6fcf091aa9a98f42708afa214c855 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -20,10 +20,41 @@ pub mod project_search; pub(crate) mod search_bar; pub mod search_status_button; +const SELECT_ALL_REGEX_MATCHES_TIP_MESSAGE: &str = "\ +Need to edit every occurrence of a complex pattern? Open buffer search, enable the regex \ +filter, type your pattern, then press `alt-enter` (or click the 'Select All Matches' button). \ +Every match becomes a selection you can edit simultaneously \u{2014} far more powerful than simple \ +find-and-replace."; + +const MULTIPLE_PROJECT_SEARCH_TABS_TIP_MESSAGE: &str = "\ +By default, project search reuses a single tab. Want to keep multiple searches open? In an \ +existing search tab, type your new query and press `cmd-enter` (macOS) / `ctrl-enter` \ +(Windows/Linux) to open results in a new tab. You can also rebind `cmd-shift-f` to \ +`workspace::NewSearch` for fresh tabs every time."; + pub fn init(cx: &mut App) { menu::init(); buffer_search::init(cx); project_search::init(cx); + + workspace::welcome::register_tip( + workspace::welcome::Tip { + title: "Select All Regex Matches".into(), + message: SELECT_ALL_REGEX_MATCHES_TIP_MESSAGE.into(), + icon: Some(ui::IconName::Regex), + mentioned_actions: vec![Box::new(SelectAllMatches)], + }, + cx, + ); + workspace::welcome::register_tip( + workspace::welcome::Tip { + title: "Multiple Project Search Tabs".into(), + message: MULTIPLE_PROJECT_SEARCH_TABS_TIP_MESSAGE.into(), + icon: Some(ui::IconName::MagnifyingGlass), + mentioned_actions: vec![Box::new(workspace::NewSearch)], + }, + cx, + ); } actions!( diff --git a/crates/settings_profile_selector/src/settings_profile_selector.rs b/crates/settings_profile_selector/src/settings_profile_selector.rs index 7ca91e3767efb6b550af7887e70a0187fed6daad..91b13a1710203b80bb10b33bc2ed532e6aebf69b 100644 --- a/crates/settings_profile_selector/src/settings_profile_selector.rs +++ b/crates/settings_profile_selector/src/settings_profile_selector.rs @@ -7,7 +7,24 @@ use settings::{ActiveSettingsProfileName, SettingsStore}; use ui::{HighlightedLabel, ListItem, ListItemSpacing, prelude::*}; use workspace::{ModalView, Workspace}; +const SETTINGS_PROFILES_TIP_MESSAGE: &str = "\ +Define settings profiles in your `settings.json` to switch configurations \ +on the fly. Create named profiles under `\"profiles\"` with any settings \ +you want \u{2014} font sizes, themes, panel layouts \u{2014} then toggle between them \ +with `settings profile selector: toggle`. Great for switching between \ +coding, presenting, and writing modes."; + pub fn init(cx: &mut App) { + workspace::welcome::register_tip( + workspace::welcome::Tip { + title: "Quick-Swap Settings with Profiles".into(), + message: SETTINGS_PROFILES_TIP_MESSAGE.into(), + icon: Some(IconName::Settings), + mentioned_actions: vec![Box::new(zed_actions::settings_profile_selector::Toggle)], + }, + cx, + ); + cx.on_action(|_: &zed_actions::settings_profile_selector::Toggle, cx| { workspace::with_active_or_new_workspace(cx, |workspace, window, cx| { toggle_settings_profile_selector(workspace, window, cx); diff --git a/crates/tasks_ui/src/tasks_ui.rs b/crates/tasks_ui/src/tasks_ui.rs index fdacef3b193beb8a656916edb61fbff1a200385b..aceb389f2b857067406d09a83a3f500de7ca3533 100644 --- a/crates/tasks_ui/src/tasks_ui.rs +++ b/crates/tasks_ui/src/tasks_ui.rs @@ -9,6 +9,18 @@ use workspace::Workspace; mod modal; +const CUSTOMIZE_TASKS_TIP_MESSAGE: &str = "\ +Need to tweak a task before running it? In the task modal (`task: spawn`), press \ +`tab` to expand any task into its full command. Edit it however you like \u{2014} for example, \ +add `RUST_BACKTRACE=1` \u{2014} then press `alt-enter` to run it as a oneshot task. The \ +modified task is saved to your history for easy reuse."; + +const LAZYGIT_TASK_TIP_MESSAGE: &str = "\ +Want a full-featured Git TUI inside Zed? Add a Lazygit task to your `tasks.json` \ +with `\"command\": \"lazygit\"`, `\"reveal_target\": \"center\"`, and \ +`\"use_new_terminal\": true`. Then bind it to a keybinding like `cmd-shift-g` using \ +`task::Spawn` with `{ \"task_name\": \"LazyGit\" }` for instant access."; + pub use modal::{Rerun, ShowAttachModal, Spawn, TaskOverrides, TasksModal}; pub fn init(cx: &mut App) { @@ -93,6 +105,25 @@ pub fn init(cx: &mut App) { }, ) .detach(); + + workspace::welcome::register_tip( + workspace::welcome::Tip { + title: "Customize Tasks on the Fly".into(), + message: CUSTOMIZE_TASKS_TIP_MESSAGE.into(), + icon: Some(ui::IconName::Terminal), + mentioned_actions: vec![], + }, + cx, + ); + workspace::welcome::register_tip( + workspace::welcome::Tip { + title: "Lazygit as a Task".into(), + message: LAZYGIT_TASK_TIP_MESSAGE.into(), + icon: Some(ui::IconName::Terminal), + mentioned_actions: vec![], + }, + cx, + ); } fn spawn_task_or_modal( diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index e4ed410ef79897770d2a27aaef10017b1d284390..2bc9eed772531f9bd4fa44d3c76d26672869e3dc 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -97,10 +97,27 @@ actions!( #[action(namespace = terminal)] pub struct RenameTerminal; +const TMUX_SESSION_TIP_MESSAGE: &str = "\ +If you use tmux, configure Zed's terminal to automatically attach to a \ +project-specific session. Set `\"terminal\": { \"shell\": { \ +\"with_arguments\": { \"program\": \"/bin/zsh\", \"args\": [\"-c\", \ +\"tmux new-session -A -s \\\"$(basename \\\"$PWD\\\")\\\"\"] } } }` in \ +your settings. Each project gets its own persistent tmux session."; + pub fn init(cx: &mut App) { assistant_slash_command::init(cx); terminal_panel::init(cx); + workspace::welcome::register_tip( + workspace::welcome::Tip { + title: "Project-Specific Tmux Sessions".into(), + message: TMUX_SESSION_TIP_MESSAGE.into(), + icon: Some(ui::IconName::Terminal), + mentioned_actions: vec![], + }, + cx, + ); + register_serializable_item::(cx); cx.observe_new(|workspace: &mut Workspace, _window, _cx| { diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 8c551bcd2768043ae416157c80d4d2f9faa19092..1621a41bf393356a9bfce9dfab0adbb3ae4d2f57 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -275,10 +275,22 @@ actions!( ] ); +const SEND_KEYSTROKES_TIP_MESSAGE: &str = "In Vim mode, you can map keybindings to replay sequences of keystrokes — just like Vim macros but permanent. Use `workspace::SendKeystrokes` in your keymap. For example, in visual mode, `\"\\d\": [\"workspace::SendKeystrokes\", \"\\\" a s dbg!( ctrl-r a ) escape\"]` wraps a selection with `dbg!()` in Rust."; + /// Initializes the `vim` crate. pub fn init(cx: &mut App) { VimGlobals::register(cx); + workspace::welcome::register_tip( + workspace::welcome::Tip { + title: "Custom Vim Macros with SendKeystrokes".into(), + message: SEND_KEYSTROKES_TIP_MESSAGE.into(), + icon: Some(ui::IconName::Keyboard), + mentioned_actions: vec![], + }, + cx, + ); + cx.observe_new(Vim::register).detach(); cx.observe_new(|workspace: &mut Workspace, _, _| { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 29863aaca5bb683d5eaabfc16e3dcbf99ad07bd3..e8899b490241ec3b777c780a7e8fae9ab71a805e 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -717,14 +717,36 @@ 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"} -``` -"#; +const COMMAND_PALETTE_TIP_MESSAGE: &str = "\ +The command palette is your gateway to every action in Zed. \ +Open it to quickly find and run any command, toggle settings, \ +or navigate your project \u{2014} all without leaving the keyboard."; + +const PROSE_WRITING_TIP_MESSAGE: &str = "\ +Want a distraction-free writing environment? \ +Use `workspace: close all docks` to hide panels, \ +`workspace: close inactive tabs and panes` to clean up, \ +then `workspace: toggle centered layout` to center your content. \ +Chain all three with `action::Sequence` in your keymap for a one-key zen mode."; + +const SEQUENCE_ACTIONS_TIP_MESSAGE: &str = "\ +Want one keybinding to do multiple things? \ +Use `action::Sequence` in your keymap to chain actions together. \ +For example, you could close all docks, clean up tabs, \ +and toggle centered layout \u{2014} all with a single keystroke."; + +const AUTOSAVE_TIP_MESSAGE: &str = "\ +Tired of forgetting to save before running tests? \ +Add `\"autosave\": \"on_focus_change\"` to your settings, \ +and Zed will automatically save your files whenever you switch \ +to a terminal or another application."; + +const OPEN_IN_NEW_PANE_TIP_MESSAGE: &str = "\ +When opening a file from the project panel or file finder, \ +hold `cmd` (macOS) or `ctrl` (Linux/Windows) to open it in a new pane \ +to the right instead of replacing your current tab. \ +When jumping to a definition, hold `alt` in addition to `cmd`/`ctrl` \ +for the same effect."; pub fn init(app_state: Arc, cx: &mut App) { component::init(); @@ -735,12 +757,48 @@ pub fn init(app_state: Arc, cx: &mut App) { welcome::register_tip( welcome::Tip { title: "Master the Command Palette".into(), - message: MESSAGE.into(), + message: COMMAND_PALETTE_TIP_MESSAGE.into(), icon: Some(ui::IconName::Sparkle), mentioned_actions: vec![Box::new(zed_actions::command_palette::Toggle)], }, cx, ); + welcome::register_tip( + welcome::Tip { + title: "Set Up a Prose-Writing Focus".into(), + message: PROSE_WRITING_TIP_MESSAGE.into(), + icon: Some(ui::IconName::CursorIBeam), + mentioned_actions: vec![Box::new(ToggleCenteredLayout)], + }, + cx, + ); + welcome::register_tip( + welcome::Tip { + title: "Sequence Multiple Actions".into(), + message: SEQUENCE_ACTIONS_TIP_MESSAGE.into(), + icon: Some(ui::IconName::PlayFilled), + mentioned_actions: vec![], + }, + cx, + ); + welcome::register_tip( + welcome::Tip { + title: "Auto-Save on Focus Change".into(), + message: AUTOSAVE_TIP_MESSAGE.into(), + icon: Some(ui::IconName::Settings), + mentioned_actions: vec![Box::new(zed_actions::OpenSettings)], + }, + cx, + ); + welcome::register_tip( + welcome::Tip { + title: "Open Files in a New Pane".into(), + message: OPEN_IN_NEW_PANE_TIP_MESSAGE.into(), + icon: Some(ui::IconName::Split), + mentioned_actions: vec![], + }, + cx, + ); cx.on_action(|_: &CloseWindow, cx| Workspace::close_global(cx)) .on_action(|_: &Reload, cx| reload(cx))