From 4dff47ae2083025b217f8464c9f7dbd168d55ccf Mon Sep 17 00:00:00 2001 From: Julia Ryan Date: Mon, 28 Apr 2025 02:21:27 -0700 Subject: [PATCH] Add searchable global tab switcher (#28047) resolves #24655 resolves #23945 I haven't yet added a default binding for the new command. #27797 added `:ls` and `:buffers` which in my opinion should use the global searchable version given that that matches the vim semantics of those commands better than just showing the tabs in the local pane. There's also a question of what to do when you select a tab from another pane, should the focus jump to that pane or should that tab move to the currently focused pane? For now I've implemented the former. Release Notes: - Added `tab_switcher::ToggleAll` to search open tabs from all panes and focus the selected one. --------- Co-authored-by: Conrad Irwin --- Cargo.lock | 2 + crates/agent/src/agent_diff.rs | 4 + .../src/context_picker/completion_provider.rs | 4 + .../assistant/src/assistant_configuration.rs | 4 +- .../src/context_editor.rs | 4 +- .../src/context_history.rs | 4 +- crates/auto_update_ui/src/auto_update_ui.rs | 4 +- crates/collab/src/tests/following_tests.rs | 43 ++--- crates/collab_ui/src/channel_view.rs | 4 + .../src/component_preview.rs | 4 +- crates/debugger_tools/src/dap_log.rs | 4 +- crates/debugger_ui/src/session.rs | 3 + crates/debugger_ui/src/session/running.rs | 4 +- crates/diagnostics/src/diagnostics.rs | 4 + crates/editor/src/items.rs | 9 +- crates/editor/src/proposed_changes_editor.rs | 4 +- crates/extensions_ui/src/extensions_ui.rs | 4 +- crates/git_ui/src/commit_view.rs | 14 +- crates/git_ui/src/project_diff.rs | 4 + crates/image_viewer/src/image_viewer.rs | 21 ++- crates/language_tools/src/key_context_view.rs | 4 +- crates/language_tools/src/lsp_log.rs | 4 +- crates/language_tools/src/syntax_tree_view.rs | 4 +- .../src/markdown_preview_view.rs | 25 +-- .../project_panel/src/project_panel_tests.rs | 4 + crates/repl/src/notebook/notebook_ui.rs | 14 +- crates/repl/src/repl_sessions_ui.rs | 4 +- crates/search/src/project_search.rs | 11 +- .../src/project_index_debug_view.rs | 4 +- crates/settings_ui/src/settings_ui.rs | 4 +- crates/tab_switcher/Cargo.toml | 2 + crates/tab_switcher/src/tab_switcher.rs | 174 +++++++++++++++--- crates/terminal_view/src/terminal_view.rs | 5 + crates/vim/src/command.rs | 4 +- crates/welcome/src/welcome.rs | 4 +- crates/workspace/src/item.rs | 41 ++--- crates/workspace/src/pane.rs | 45 ++--- crates/workspace/src/shared_screen.rs | 4 +- crates/workspace/src/theme_preview.rs | 4 +- crates/workspace/src/workspace.rs | 32 +++- 40 files changed, 360 insertions(+), 181 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f79af953cc..bc4752bf11 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14312,6 +14312,7 @@ dependencies = [ "ctor", "editor", "env_logger 0.11.8", + "fuzzy", "gpui", "language", "menu", @@ -14321,6 +14322,7 @@ dependencies = [ "serde", "serde_json", "settings", + "smol", "theme", "ui", "util", diff --git a/crates/agent/src/agent_diff.rs b/crates/agent/src/agent_diff.rs index 0e66308f37..b09c0015c5 100644 --- a/crates/agent/src/agent_diff.rs +++ b/crates/agent/src/agent_diff.rs @@ -597,6 +597,10 @@ impl Item for AgentDiff { editor.added_to_workspace(workspace, window, cx) }); } + + fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { + "Agent Diff".into() + } } impl Render for AgentDiff { diff --git a/crates/agent/src/context_picker/completion_provider.rs b/crates/agent/src/context_picker/completion_provider.rs index b5c8bf0248..d610470e63 100644 --- a/crates/agent/src/context_picker/completion_provider.rs +++ b/crates/agent/src/context_picker/completion_provider.rs @@ -1045,6 +1045,10 @@ mod tests { fn include_in_nav_history() -> bool { false } + + fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { + "Test".into() + } } impl EventEmitter<()> for AtMentionEditor {} diff --git a/crates/assistant/src/assistant_configuration.rs b/crates/assistant/src/assistant_configuration.rs index cb3d268a63..6b96051a5f 100644 --- a/crates/assistant/src/assistant_configuration.rs +++ b/crates/assistant/src/assistant_configuration.rs @@ -193,7 +193,7 @@ impl Focusable for ConfigurationView { impl Item for ConfigurationView { type Event = ConfigurationViewEvent; - fn tab_content_text(&self, _window: &Window, _cx: &App) -> Option { - Some("Configuration".into()) + fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { + "Configuration".into() } } diff --git a/crates/assistant_context_editor/src/context_editor.rs b/crates/assistant_context_editor/src/context_editor.rs index 94e267c85a..840ed0eda3 100644 --- a/crates/assistant_context_editor/src/context_editor.rs +++ b/crates/assistant_context_editor/src/context_editor.rs @@ -3160,8 +3160,8 @@ impl Focusable for ContextEditor { impl Item for ContextEditor { type Event = editor::EditorEvent; - fn tab_content_text(&self, _window: &Window, cx: &App) -> Option { - Some(util::truncate_and_trailoff(&self.title(cx), MAX_TAB_TITLE_LEN).into()) + fn tab_content_text(&self, _detail: usize, cx: &App) -> SharedString { + util::truncate_and_trailoff(&self.title(cx), MAX_TAB_TITLE_LEN).into() } fn to_item_events(event: &Self::Event, mut f: impl FnMut(item::ItemEvent)) { diff --git a/crates/assistant_context_editor/src/context_history.rs b/crates/assistant_context_editor/src/context_history.rs index 35b932158b..560033be34 100644 --- a/crates/assistant_context_editor/src/context_history.rs +++ b/crates/assistant_context_editor/src/context_history.rs @@ -108,8 +108,8 @@ impl EventEmitter<()> for ContextHistory {} impl Item for ContextHistory { type Event = (); - fn tab_content_text(&self, _window: &Window, _cx: &App) -> Option { - Some("History".into()) + fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { + "History".into() } } diff --git a/crates/auto_update_ui/src/auto_update_ui.rs b/crates/auto_update_ui/src/auto_update_ui.rs index b0fd67add0..044a6b2922 100644 --- a/crates/auto_update_ui/src/auto_update_ui.rs +++ b/crates/auto_update_ui/src/auto_update_ui.rs @@ -91,7 +91,7 @@ fn view_release_notes_locally( let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); - let tab_description = SharedString::from(body.title.to_string()); + let tab_content = SharedString::from(body.title.to_string()); let editor = cx.new(|cx| { Editor::for_multibuffer(buffer, Some(project), window, cx) }); @@ -102,7 +102,7 @@ fn view_release_notes_locally( editor, workspace_handle, language_registry, - Some(tab_description), + tab_content, window, cx, ); diff --git a/crates/collab/src/tests/following_tests.rs b/crates/collab/src/tests/following_tests.rs index 78d30adb4a..57494bd42b 100644 --- a/crates/collab/src/tests/following_tests.rs +++ b/crates/collab/src/tests/following_tests.rs @@ -1517,10 +1517,7 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut workspace.leader_for_pane(workspace.active_pane()) ); let item = workspace.active_item(cx).unwrap(); - assert_eq!( - item.tab_description(0, cx).unwrap(), - SharedString::from("w.rs") - ); + assert_eq!(item.tab_content_text(0, cx), SharedString::from("w.rs")); }); // TODO: in app code, this would be done by the collab_ui. @@ -1546,10 +1543,7 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut executor.run_until_parked(); workspace_b_project_a.update(&mut cx_b2, |workspace, cx| { let item = workspace.active_item(cx).unwrap(); - assert_eq!( - item.tab_description(0, cx).unwrap(), - SharedString::from("x.rs") - ); + assert_eq!(item.tab_content_text(0, cx), SharedString::from("x.rs")); }); workspace_a.update_in(cx_a, |workspace, window, cx| { @@ -1564,7 +1558,7 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut workspace.leader_for_pane(workspace.active_pane()) ); let item = workspace.active_pane().read(cx).active_item().unwrap(); - assert_eq!(item.tab_description(0, cx).unwrap(), "x.rs"); + assert_eq!(item.tab_content_text(0, cx), "x.rs"); }); // b moves to y.rs in b's project, a is still following but can't yet see @@ -1625,10 +1619,7 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut workspace.leader_for_pane(workspace.active_pane()) ); let item = workspace.active_item(cx).unwrap(); - assert_eq!( - item.tab_description(0, cx).unwrap(), - SharedString::from("y.rs") - ); + assert_eq!(item.tab_content_text(0, cx), SharedString::from("y.rs")); }); } @@ -1885,13 +1876,7 @@ fn pane_summaries(workspace: &Entity, cx: &mut VisualTestContext) -> items: pane .items() .enumerate() - .map(|(ix, item)| { - ( - ix == active_ix, - item.tab_description(0, cx) - .map_or(String::new(), |s| s.to_string()), - ) - }) + .map(|(ix, item)| (ix == active_ix, item.tab_content_text(0, cx).into())) .collect(), } }) @@ -2179,7 +2164,7 @@ async fn test_following_to_channel_notes_other_workspace( cx_a.run_until_parked(); workspace_a.update(cx_a, |workspace, cx| { let editor = workspace.active_item(cx).unwrap(); - assert_eq!(editor.tab_description(0, cx).unwrap(), "1.txt"); + assert_eq!(editor.tab_content_text(0, cx), "1.txt"); }); // b joins channel and is following a @@ -2188,7 +2173,7 @@ async fn test_following_to_channel_notes_other_workspace( let (workspace_b, cx_b) = client_b.active_workspace(cx_b); workspace_b.update(cx_b, |workspace, cx| { let editor = workspace.active_item(cx).unwrap(); - assert_eq!(editor.tab_description(0, cx).unwrap(), "1.txt"); + assert_eq!(editor.tab_content_text(0, cx), "1.txt"); }); // a opens a second workspace and the channel notes @@ -2212,13 +2197,13 @@ async fn test_following_to_channel_notes_other_workspace( workspace_a.update(cx_a, |workspace, cx| { let editor = workspace.active_item(cx).unwrap(); - assert_eq!(editor.tab_description(0, cx).unwrap(), "1.txt"); + assert_eq!(editor.tab_content_text(0, cx), "1.txt"); }); // b should follow a back workspace_b.update(cx_b, |workspace, cx| { let editor = workspace.active_item_as::(cx).unwrap(); - assert_eq!(editor.tab_description(0, cx).unwrap(), "1.txt"); + assert_eq!(editor.tab_content_text(0, cx), "1.txt"); }); } @@ -2238,7 +2223,7 @@ async fn test_following_while_deactivated(cx_a: &mut TestAppContext, cx_b: &mut cx_a.run_until_parked(); workspace_a.update(cx_a, |workspace, cx| { let editor = workspace.active_item(cx).unwrap(); - assert_eq!(editor.tab_description(0, cx).unwrap(), "1.txt"); + assert_eq!(editor.tab_content_text(0, cx), "1.txt"); }); // b joins channel and is following a @@ -2247,7 +2232,7 @@ async fn test_following_while_deactivated(cx_a: &mut TestAppContext, cx_b: &mut let (workspace_b, cx_b) = client_b.active_workspace(cx_b); workspace_b.update(cx_b, |workspace, cx| { let editor = workspace.active_item(cx).unwrap(); - assert_eq!(editor.tab_description(0, cx).unwrap(), "1.txt"); + assert_eq!(editor.tab_content_text(0, cx), "1.txt"); }); // stop following @@ -2260,7 +2245,7 @@ async fn test_following_while_deactivated(cx_a: &mut TestAppContext, cx_b: &mut workspace_b.update(cx_b, |workspace, cx| { let editor = workspace.active_item_as::(cx).unwrap(); - assert_eq!(editor.tab_description(0, cx).unwrap(), "1.txt"); + assert_eq!(editor.tab_content_text(0, cx), "1.txt"); }); // a opens a file in a new window @@ -2281,12 +2266,12 @@ async fn test_following_while_deactivated(cx_a: &mut TestAppContext, cx_b: &mut workspace_a.update(cx_a, |workspace, cx| { let editor = workspace.active_item(cx).unwrap(); - assert_eq!(editor.tab_description(0, cx).unwrap(), "2.js"); + assert_eq!(editor.tab_content_text(0, cx), "2.js"); }); // b should follow a back workspace_b.update(cx_b, |workspace, cx| { let editor = workspace.active_item_as::(cx).unwrap(); - assert_eq!(editor.tab_description(0, cx).unwrap(), "2.js"); + assert_eq!(editor.tab_content_text(0, cx), "2.js"); }); } diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs index 554cfe113a..bb7192026e 100644 --- a/crates/collab_ui/src/channel_view.rs +++ b/crates/collab_ui/src/channel_view.rs @@ -540,6 +540,10 @@ impl Item for ChannelView { fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) { Editor::to_item_events(event, f) } + + fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { + "Channels".into() + } } impl FollowableItem for ChannelView { diff --git a/crates/component_preview/src/component_preview.rs b/crates/component_preview/src/component_preview.rs index 74105a0213..9a2abb9929 100644 --- a/crates/component_preview/src/component_preview.rs +++ b/crates/component_preview/src/component_preview.rs @@ -735,8 +735,8 @@ impl From for ActivePageId { impl Item for ComponentPreview { type Event = ItemEvent; - fn tab_content_text(&self, _window: &Window, _cx: &App) -> Option { - Some("Component Preview".into()) + fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { + "Component Preview".into() } fn telemetry_event_text(&self) -> Option<&'static str> { diff --git a/crates/debugger_tools/src/dap_log.rs b/crates/debugger_tools/src/dap_log.rs index acd26e2d7f..eca26d2b73 100644 --- a/crates/debugger_tools/src/dap_log.rs +++ b/crates/debugger_tools/src/dap_log.rs @@ -727,8 +727,8 @@ impl Item for DapLogView { Editor::to_item_events(event, f) } - fn tab_content_text(&self, _window: &Window, _cx: &App) -> Option { - Some("DAP Logs".into()) + fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { + "DAP Logs".into() } fn telemetry_event_text(&self) -> Option<&'static str> { diff --git a/crates/debugger_ui/src/session.rs b/crates/debugger_ui/src/session.rs index 974f205fa6..756f866bf1 100644 --- a/crates/debugger_ui/src/session.rs +++ b/crates/debugger_ui/src/session.rs @@ -170,6 +170,9 @@ impl Focusable for DebugSession { impl Item for DebugSession { type Event = DebugPanelItemEvent; + fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { + "Debugger".into() + } } impl FollowableItem for DebugSession { diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs index 45af03623a..9d4f34b5cd 100644 --- a/crates/debugger_ui/src/session/running.rs +++ b/crates/debugger_ui/src/session/running.rs @@ -139,8 +139,8 @@ impl Item for SubView { /// This is used to serialize debugger pane layouts /// A SharedString gets converted to a enum and back during serialization/deserialization. - fn tab_content_text(&self, _window: &Window, _cx: &App) -> Option { - Some(self.kind.to_shared_string()) + fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { + self.kind.to_shared_string() } fn tab_content( diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 18602356c5..b0ead9ea9b 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -568,6 +568,10 @@ impl Item for ProjectDiagnosticsEditor { Some("Project Diagnostics".into()) } + fn tab_content_text(&self, _detail: usize, _: &App) -> SharedString { + "Diagnostics".into() + } + fn tab_content(&self, params: TabContentParams, _window: &Window, _: &App) -> AnyElement { h_flex() .gap_1() diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 476a05b29f..232024e554 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -619,9 +619,12 @@ impl Item for Editor { None } - fn tab_description(&self, detail: usize, cx: &App) -> Option { - let path = path_for_buffer(&self.buffer, detail, true, cx)?; - Some(path.to_string_lossy().to_string().into()) + fn tab_content_text(&self, detail: usize, cx: &App) -> SharedString { + if let Some(path) = path_for_buffer(&self.buffer, detail, true, cx) { + path.to_string_lossy().to_string().into() + } else { + "untitled".into() + } } fn tab_icon(&self, _: &Window, cx: &App) -> Option { diff --git a/crates/editor/src/proposed_changes_editor.rs b/crates/editor/src/proposed_changes_editor.rs index 0eebddb640..734d39cfe6 100644 --- a/crates/editor/src/proposed_changes_editor.rs +++ b/crates/editor/src/proposed_changes_editor.rs @@ -285,8 +285,8 @@ impl Item for ProposedChangesEditor { Some(Icon::new(IconName::Diff)) } - fn tab_content_text(&self, _window: &Window, _cx: &App) -> Option { - Some(self.title.clone()) + fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { + self.title.clone() } fn as_searchable(&self, _: &Entity) -> Option> { diff --git a/crates/extensions_ui/src/extensions_ui.rs b/crates/extensions_ui/src/extensions_ui.rs index 92570f72c9..430b656f09 100644 --- a/crates/extensions_ui/src/extensions_ui.rs +++ b/crates/extensions_ui/src/extensions_ui.rs @@ -1398,8 +1398,8 @@ impl Focusable for ExtensionsPage { impl Item for ExtensionsPage { type Event = ItemEvent; - fn tab_content_text(&self, _window: &Window, _cx: &App) -> Option { - Some("Extensions".into()) + fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { + "Extensions".into() } fn telemetry_event_text(&self) -> Option<&'static str> { diff --git a/crates/git_ui/src/commit_view.rs b/crates/git_ui/src/commit_view.rs index d7ec189028..3f8b2f52d5 100644 --- a/crates/git_ui/src/commit_view.rs +++ b/crates/git_ui/src/commit_view.rs @@ -19,7 +19,7 @@ use std::{ path::{Path, PathBuf}, sync::Arc, }; -use ui::{Color, Icon, IconName, Label, LabelCommon as _}; +use ui::{Color, Icon, IconName, Label, LabelCommon as _, SharedString}; use util::{ResultExt, truncate_and_trailoff}; use workspace::{ Item, ItemHandle as _, ItemNavHistory, ToolbarItemLocation, Workspace, @@ -409,10 +409,8 @@ impl Item for CommitView { Some(Icon::new(IconName::GitBranch).color(Color::Muted)) } - fn tab_content(&self, params: TabContentParams, _window: &Window, _: &App) -> AnyElement { - let short_sha = self.commit.sha.get(0..7).unwrap_or(&*self.commit.sha); - let subject = truncate_and_trailoff(self.commit.message.split('\n').next().unwrap(), 20); - Label::new(format!("{short_sha} - {subject}",)) + fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement { + Label::new(self.tab_content_text(params.detail.unwrap_or_default(), cx)) .color(if params.selected { Color::Default } else { @@ -421,6 +419,12 @@ impl Item for CommitView { .into_any_element() } + fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { + let short_sha = self.commit.sha.get(0..7).unwrap_or(&*self.commit.sha); + let subject = truncate_and_trailoff(self.commit.message.split('\n').next().unwrap(), 20); + format!("{short_sha} - {subject}").into() + } + fn tab_tooltip_text(&self, _: &App) -> Option { let short_sha = self.commit.sha.get(0..16).unwrap_or(&*self.commit.sha); let subject = self.commit.message.split('\n').next().unwrap(); diff --git a/crates/git_ui/src/project_diff.rs b/crates/git_ui/src/project_diff.rs index eb93f97d3f..36ad7e528c 100644 --- a/crates/git_ui/src/project_diff.rs +++ b/crates/git_ui/src/project_diff.rs @@ -547,6 +547,10 @@ impl Item for ProjectDiff { .into_any_element() } + fn tab_content_text(&self, _detail: usize, _: &App) -> SharedString { + "Uncommitted Changes".into() + } + fn telemetry_event_text(&self) -> Option<&'static str> { Some("Project Diff Opened") } diff --git a/crates/image_viewer/src/image_viewer.rs b/crates/image_viewer/src/image_viewer.rs index 0a8043a1c6..5795265e36 100644 --- a/crates/image_viewer/src/image_viewer.rs +++ b/crates/image_viewer/src/image_viewer.rs @@ -99,7 +99,7 @@ impl Item for ImageView { Some(file_path.into()) } - fn tab_content(&self, params: TabContentParams, _: &Window, cx: &App) -> AnyElement { + fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement { let project_path = self.image_item.read(cx).project_path(cx); let label_color = if ItemSettings::get_global(cx).git_status { @@ -121,20 +121,23 @@ impl Item for ImageView { params.text_color() }; - let title = self - .image_item - .read(cx) - .file - .file_name(cx) - .to_string_lossy() - .to_string(); - Label::new(title) + Label::new(self.tab_content_text(params.detail.unwrap_or_default(), cx)) .single_line() .color(label_color) .when(params.preview, |this| this.italic()) .into_any_element() } + fn tab_content_text(&self, _: usize, cx: &App) -> SharedString { + self.image_item + .read(cx) + .file + .file_name(cx) + .to_string_lossy() + .to_string() + .into() + } + fn tab_icon(&self, _: &Window, cx: &App) -> Option { let path = self.image_item.read(cx).path(); ItemSettings::get_global(cx) diff --git a/crates/language_tools/src/key_context_view.rs b/crates/language_tools/src/key_context_view.rs index 68f54e0528..0969b0edf6 100644 --- a/crates/language_tools/src/key_context_view.rs +++ b/crates/language_tools/src/key_context_view.rs @@ -150,8 +150,8 @@ impl Item for KeyContextView { fn to_item_events(_: &Self::Event, _: impl FnMut(workspace::item::ItemEvent)) {} - fn tab_content_text(&self, _window: &Window, _cx: &App) -> Option { - Some("Keyboard Context".into()) + fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { + "Keyboard Context".into() } fn telemetry_event_text(&self) -> Option<&'static str> { diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index b90e925fed..8b29ab6298 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -1058,8 +1058,8 @@ impl Item for LspLogView { Editor::to_item_events(event, f) } - fn tab_content_text(&self, _window: &Window, _cx: &App) -> Option { - Some("LSP Logs".into()) + fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { + "LSP Logs".into() } fn telemetry_event_text(&self) -> Option<&'static str> { diff --git a/crates/language_tools/src/syntax_tree_view.rs b/crates/language_tools/src/syntax_tree_view.rs index 2098c86673..3a14181db0 100644 --- a/crates/language_tools/src/syntax_tree_view.rs +++ b/crates/language_tools/src/syntax_tree_view.rs @@ -401,8 +401,8 @@ impl Item for SyntaxTreeView { fn to_item_events(_: &Self::Event, _: impl FnMut(workspace::item::ItemEvent)) {} - fn tab_content_text(&self, _window: &Window, _cx: &App) -> Option { - Some("Syntax Tree".into()) + fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { + "Syntax Tree".into() } fn telemetry_event_text(&self) -> Option<&'static str> { diff --git a/crates/markdown_preview/src/markdown_preview_view.rs b/crates/markdown_preview/src/markdown_preview_view.rs index c6b554349d..bbcb196b29 100644 --- a/crates/markdown_preview/src/markdown_preview_view.rs +++ b/crates/markdown_preview/src/markdown_preview_view.rs @@ -35,8 +35,7 @@ pub struct MarkdownPreviewView { contents: Option, selected_block: usize, list_state: ListState, - tab_description: Option, - fallback_tab_description: SharedString, + tab_content_text: SharedString, language_registry: Arc, parsing_markdown_task: Option>>, } @@ -130,7 +129,7 @@ impl MarkdownPreviewView { editor, workspace_handle, language_registry, - None, + "Markdown Preview".into(), window, cx, ) @@ -141,7 +140,7 @@ impl MarkdownPreviewView { active_editor: Entity, workspace: WeakEntity, language_registry: Arc, - fallback_description: Option, + tab_content_text: SharedString, window: &mut Window, cx: &mut Context, ) -> Entity { @@ -262,10 +261,8 @@ impl MarkdownPreviewView { workspace: workspace.clone(), contents: None, list_state, - tab_description: None, + tab_content_text, language_registry, - fallback_tab_description: fallback_description - .unwrap_or_else(|| "Markdown Preview".into()), parsing_markdown_task: None, }; @@ -343,10 +340,8 @@ impl MarkdownPreviewView { }, ); - self.tab_description = editor - .read(cx) - .tab_description(0, cx) - .map(|tab_description| format!("Preview {}", tab_description)); + let tab_content = editor.read(cx).tab_content_text(0, cx); + self.tab_content_text = format!("Preview {}", tab_content).into(); self.active_editor = Some(EditorState { editor, @@ -496,12 +491,8 @@ impl Item for MarkdownPreviewView { Some(Icon::new(IconName::FileDoc)) } - fn tab_content_text(&self, _window: &Window, _cx: &App) -> Option { - Some(if let Some(description) = &self.tab_description { - description.clone().into() - } else { - self.fallback_tab_description.clone() - }) + fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { + self.tab_content_text.clone() } fn telemetry_event_text(&self) -> Option<&'static str> { diff --git a/crates/project_panel/src/project_panel_tests.rs b/crates/project_panel/src/project_panel_tests.rs index 65eeae795e..8badba4738 100644 --- a/crates/project_panel/src/project_panel_tests.rs +++ b/crates/project_panel/src/project_panel_tests.rs @@ -5308,6 +5308,10 @@ impl ProjectItem for TestProjectItemView { impl Item for TestProjectItemView { type Event = (); + + fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { + "Test".into() + } } impl EventEmitter<()> for TestProjectItemView {} diff --git a/crates/repl/src/notebook/notebook_ui.rs b/crates/repl/src/notebook/notebook_ui.rs index 4a539a64a7..07f3e63b24 100644 --- a/crates/repl/src/notebook/notebook_ui.rs +++ b/crates/repl/src/notebook/notebook_ui.rs @@ -731,17 +731,21 @@ impl Item for NotebookEditor { } fn tab_content(&self, params: TabContentParams, window: &Window, cx: &App) -> AnyElement { + Label::new(self.tab_content_text(params.detail.unwrap_or(0), cx)) + .single_line() + .color(params.text_color()) + .when(params.preview, |this| this.italic()) + .into_any_element() + } + + fn tab_content_text(&self, _detail: usize, cx: &App) -> SharedString { let path = &self.notebook_item.read(cx).path; let title = path .file_name() .unwrap_or_else(|| path.as_os_str()) .to_string_lossy() .to_string(); - Label::new(title) - .single_line() - .color(params.text_color()) - .when(params.preview, |this| this.italic()) - .into_any_element() + title.into() } fn tab_icon(&self, _window: &Window, _cx: &App) -> Option { diff --git a/crates/repl/src/repl_sessions_ui.rs b/crates/repl/src/repl_sessions_ui.rs index abae381276..df7ce574ab 100644 --- a/crates/repl/src/repl_sessions_ui.rs +++ b/crates/repl/src/repl_sessions_ui.rs @@ -178,8 +178,8 @@ impl Focusable for ReplSessionsPage { impl Item for ReplSessionsPage { type Event = ItemEvent; - fn tab_content_text(&self, _window: &Window, _cx: &App) -> Option { - Some("REPL Sessions".into()) + fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { + "REPL Sessions".into() } fn telemetry_event_text(&self) -> Option<&'static str> { diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 6223e32190..40447cf2fc 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -446,7 +446,7 @@ impl Item for ProjectSearchView { Some(Icon::new(IconName::MagnifyingGlass)) } - fn tab_content_text(&self, _: &Window, cx: &App) -> Option { + fn tab_content_text(&self, _detail: usize, cx: &App) -> SharedString { let last_query: Option = self .entity .read(cx) @@ -457,11 +457,10 @@ impl Item for ProjectSearchView { let query_text = util::truncate_and_trailoff(&query, MAX_TAB_TITLE_LEN); query_text.into() }); - Some( - last_query - .filter(|query| !query.is_empty()) - .unwrap_or_else(|| "Project Search".into()), - ) + + last_query + .filter(|query| !query.is_empty()) + .unwrap_or_else(|| "Project Search".into()) } fn telemetry_event_text(&self) -> Option<&'static str> { diff --git a/crates/semantic_index/src/project_index_debug_view.rs b/crates/semantic_index/src/project_index_debug_view.rs index 140599d22a..15b86c3f77 100644 --- a/crates/semantic_index/src/project_index_debug_view.rs +++ b/crates/semantic_index/src/project_index_debug_view.rs @@ -289,8 +289,8 @@ impl EventEmitter<()> for ProjectIndexDebugView {} impl Item for ProjectIndexDebugView { type Event = (); - fn tab_content_text(&self, _window: &Window, _cx: &App) -> Option { - Some("Project Index (Debug)".into()) + fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { + "Project Index (Debug)".into() } fn clone_on_split( diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index 65c420c6bf..3428a99bf8 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/crates/settings_ui/src/settings_ui.rs @@ -160,8 +160,8 @@ impl Item for SettingsPage { Some(Icon::new(IconName::Settings)) } - fn tab_content_text(&self, _window: &Window, _cx: &App) -> Option { - Some("Settings".into()) + fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { + "Settings".into() } fn show_toolbar(&self) -> bool { diff --git a/crates/tab_switcher/Cargo.toml b/crates/tab_switcher/Cargo.toml index 55545016d5..027268e7d7 100644 --- a/crates/tab_switcher/Cargo.toml +++ b/crates/tab_switcher/Cargo.toml @@ -15,6 +15,7 @@ doctest = false [dependencies] collections.workspace = true editor.workspace = true +fuzzy.workspace = true gpui.workspace = true menu.workspace = true picker.workspace = true @@ -22,6 +23,7 @@ project.workspace = true schemars.workspace = true serde.workspace = true settings.workspace = true +smol.workspace = true ui.workspace = true util.workspace = true workspace.workspace = true diff --git a/crates/tab_switcher/src/tab_switcher.rs b/crates/tab_switcher/src/tab_switcher.rs index 14553c016e..25cfcfba7b 100644 --- a/crates/tab_switcher/src/tab_switcher.rs +++ b/crates/tab_switcher/src/tab_switcher.rs @@ -3,6 +3,7 @@ mod tab_switcher_tests; use collections::HashMap; use editor::items::entry_git_aware_label_color; +use fuzzy::StringMatchCandidate; use gpui::{ Action, AnyElement, App, Context, DismissEvent, Entity, EntityId, EventEmitter, FocusHandle, Focusable, Modifiers, ModifiersChangedEvent, MouseButton, MouseUpEvent, ParentElement, Render, @@ -13,7 +14,7 @@ use project::Project; use schemars::JsonSchema; use serde::Deserialize; use settings::Settings; -use std::sync::Arc; +use std::{cmp::Reverse, sync::Arc}; use ui::{ListItem, ListItemSpacing, Tooltip, prelude::*}; use util::ResultExt; use workspace::{ @@ -32,7 +33,7 @@ pub struct Toggle { } impl_actions!(tab_switcher, [Toggle]); -actions!(tab_switcher, [CloseSelectedItem]); +actions!(tab_switcher, [CloseSelectedItem, ToggleAll]); pub struct TabSwitcher { picker: Entity>, @@ -53,7 +54,19 @@ impl TabSwitcher { ) { workspace.register_action(|workspace, action: &Toggle, window, cx| { let Some(tab_switcher) = workspace.active_modal::(cx) else { - Self::open(action, workspace, window, cx); + Self::open(workspace, action.select_last, false, window, cx); + return; + }; + + tab_switcher.update(cx, |tab_switcher, cx| { + tab_switcher + .picker + .update(cx, |picker, cx| picker.cycle_selection(window, cx)) + }); + }); + workspace.register_action(|workspace, _action: &ToggleAll, window, cx| { + let Some(tab_switcher) = workspace.active_modal::(cx) else { + Self::open(workspace, false, true, window, cx); return; }; @@ -66,8 +79,9 @@ impl TabSwitcher { } fn open( - action: &Toggle, workspace: &mut Workspace, + select_last: bool, + is_global: bool, window: &mut Window, cx: &mut Context, ) { @@ -90,24 +104,43 @@ impl TabSwitcher { }) } + let weak_workspace = workspace.weak_handle(); let project = workspace.project().clone(); workspace.toggle_modal(window, cx, |window, cx| { let delegate = TabSwitcherDelegate::new( project, - action, + select_last, cx.entity().downgrade(), weak_pane, + weak_workspace, + is_global, window, cx, ); - TabSwitcher::new(delegate, window, cx) + TabSwitcher::new(delegate, window, is_global, cx) }); } - fn new(delegate: TabSwitcherDelegate, window: &mut Window, cx: &mut Context) -> Self { + fn new( + delegate: TabSwitcherDelegate, + window: &mut Window, + is_global: bool, + cx: &mut Context, + ) -> Self { + let init_modifiers = if is_global { + None + } else { + window.modifiers().modified().then_some(window.modifiers()) + }; Self { - picker: cx.new(|cx| Picker::nonsearchable_uniform_list(delegate, window, cx)), - init_modifiers: window.modifiers().modified().then_some(window.modifiers()), + picker: cx.new(|cx| { + if is_global { + Picker::uniform_list(delegate, window, cx) + } else { + Picker::nonsearchable_uniform_list(delegate, window, cx) + } + }), + init_modifiers, } } @@ -163,7 +196,9 @@ impl Render for TabSwitcher { } } +#[derive(Clone)] struct TabMatch { + pane: WeakEntity, item_index: usize, item: Box, detail: usize, @@ -175,27 +210,34 @@ pub struct TabSwitcherDelegate { tab_switcher: WeakEntity, selected_index: usize, pane: WeakEntity, + workspace: WeakEntity, project: Entity, matches: Vec, + is_all_panes: bool, } impl TabSwitcherDelegate { + #[allow(clippy::complexity)] fn new( project: Entity, - action: &Toggle, + select_last: bool, tab_switcher: WeakEntity, pane: WeakEntity, + workspace: WeakEntity, + is_all_panes: bool, window: &mut Window, cx: &mut Context, ) -> Self { Self::subscribe_to_updates(&pane, window, cx); Self { - select_last: action.select_last, + select_last, tab_switcher, selected_index: 0, pane, + workspace, project, matches: Vec::new(), + is_all_panes, } } @@ -212,7 +254,8 @@ impl TabSwitcherDelegate { PaneEvent::AddItem { .. } | PaneEvent::RemovedItem { .. } | PaneEvent::Remove { .. } => tab_switcher.picker.update(cx, |picker, cx| { - picker.delegate.update_matches(window, cx); + let query = picker.query(cx); + picker.delegate.update_matches(query, window, cx); cx.notify(); }), _ => {} @@ -221,7 +264,91 @@ impl TabSwitcherDelegate { .detach(); } - fn update_matches(&mut self, _window: &mut Window, cx: &mut App) { + fn update_all_pane_matches(&mut self, query: String, window: &mut Window, cx: &mut App) { + let Some(workspace) = self.workspace.upgrade() else { + return; + }; + let mut all_items = Vec::new(); + let mut item_index = 0; + for pane_handle in workspace.read(cx).panes() { + let pane = pane_handle.read(cx); + let items: Vec> = + pane.items().map(|item| item.boxed_clone()).collect(); + for ((_detail, item), detail) in items + .iter() + .enumerate() + .zip(tab_details(&items, window, cx)) + { + all_items.push(TabMatch { + pane: pane_handle.downgrade(), + item_index, + item: item.clone(), + detail, + preview: pane.is_active_preview_item(item.item_id()), + }); + item_index += 1; + } + } + + let matches = if query.is_empty() { + let history = workspace.read(cx).recently_activated_items(cx); + for item in &all_items { + eprintln!( + "{:?} {:?}", + item.item.tab_content_text(0, cx), + (Reverse(history.get(&item.item.item_id())), item.item_index) + ) + } + eprintln!(""); + all_items + .sort_by_key(|tab| (Reverse(history.get(&tab.item.item_id())), tab.item_index)); + all_items + } else { + let candidates = all_items + .iter() + .enumerate() + .flat_map(|(ix, tab_match)| { + Some(StringMatchCandidate::new( + ix, + &tab_match.item.tab_content_text(0, cx), + )) + }) + .collect::>(); + smol::block_on(fuzzy::match_strings( + &candidates, + &query, + true, + 10000, + &Default::default(), + cx.background_executor().clone(), + )) + .into_iter() + .map(|m| all_items[m.candidate_id].clone()) + .collect() + }; + + let selected_item_id = self.selected_item_id(); + self.matches = matches; + self.selected_index = self.compute_selected_index(selected_item_id); + } + + fn update_matches( + &mut self, + query: String, + window: &mut Window, + cx: &mut Context>, + ) { + if self.is_all_panes { + // needed because we need to borrow the workspace, but that may be borrowed when the picker + // calls update_matches. + let this = cx.entity(); + window.defer(cx, move |window, cx| { + this.update(cx, |this, cx| { + this.delegate.update_all_pane_matches(query, window, cx); + }) + }); + return; + } let selected_item_id = self.selected_item_id(); self.matches.clear(); let Some(pane) = self.pane.upgrade() else { @@ -240,8 +367,9 @@ impl TabSwitcherDelegate { items .iter() .enumerate() - .zip(tab_details(&items, cx)) + .zip(tab_details(&items, window, cx)) .map(|((item_index, item), detail)| TabMatch { + pane: self.pane.clone(), item_index, item: item.boxed_clone(), detail, @@ -348,11 +476,11 @@ impl PickerDelegate for TabSwitcherDelegate { fn update_matches( &mut self, - _raw_query: String, + raw_query: String, window: &mut Window, cx: &mut Context>, ) -> Task<()> { - self.update_matches(window, cx); + self.update_matches(raw_query, window, cx); Task::ready(()) } @@ -362,15 +490,17 @@ impl PickerDelegate for TabSwitcherDelegate { window: &mut Window, cx: &mut Context>, ) { - let Some(pane) = self.pane.upgrade() else { - return; - }; let Some(selected_match) = self.matches.get(self.selected_index()) else { return; }; - pane.update(cx, |pane, cx| { - pane.activate_item(selected_match.item_index, true, true, window, cx); - }); + selected_match + .pane + .update(cx, |pane, cx| { + if let Some(index) = pane.index_for_item(selected_match.item.as_ref()) { + pane.activate_item(index, true, true, window, cx); + } + }) + .ok(); } fn dismissed(&mut self, _: &mut Window, cx: &mut Context>) { diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 74853569bc..3323c1de6a 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -1462,6 +1462,11 @@ impl Item for TerminalView { .into_any() } + fn tab_content_text(&self, detail: usize, cx: &App) -> SharedString { + let terminal = self.terminal().read(cx); + terminal.title(detail == 0).into() + } + fn telemetry_event_text(&self) -> Option<&'static str> { None } diff --git a/crates/vim/src/command.rs b/crates/vim/src/command.rs index 1e55e5a9f4..3645993eda 100644 --- a/crates/vim/src/command.rs +++ b/crates/vim/src/command.rs @@ -796,8 +796,8 @@ fn generate_commands(_: &App) -> Vec { VimCommand::new(("bf", "irst"), workspace::ActivateItem(0)), VimCommand::new(("br", "ewind"), workspace::ActivateItem(0)), VimCommand::new(("bl", "ast"), workspace::ActivateLastItem), - VimCommand::str(("buffers", ""), "tab_switcher::Toggle"), - VimCommand::str(("ls", ""), "tab_switcher::Toggle"), + VimCommand::str(("buffers", ""), "tab_switcher::ToggleAll"), + VimCommand::str(("ls", ""), "tab_switcher::ToggleAll"), VimCommand::new(("new", ""), workspace::NewFileSplitHorizontal), VimCommand::new(("vne", "w"), workspace::NewFileSplitVertical), VimCommand::new(("tabe", "dit"), workspace::NewFile), diff --git a/crates/welcome/src/welcome.rs b/crates/welcome/src/welcome.rs index 540a3de990..52e7c0ea5d 100644 --- a/crates/welcome/src/welcome.rs +++ b/crates/welcome/src/welcome.rs @@ -420,8 +420,8 @@ impl Focusable for WelcomePage { impl Item for WelcomePage { type Event = ItemEvent; - fn tab_content_text(&self, _window: &Window, _cx: &App) -> Option { - Some("Welcome".into()) + fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { + "Welcome".into() } fn telemetry_event_text(&self) -> Option<&'static str> { diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index 26440ce1e4..00aa340e66 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -30,7 +30,7 @@ use std::{ time::Duration, }; use theme::Theme; -use ui::{Color, Element as _, Icon, IntoElement, Label, LabelCommon}; +use ui::{Color, Icon, IntoElement, Label, LabelCommon}; use util::ResultExt; pub const LEADER_UPDATE_THROTTLE: Duration = Duration::from_millis(200); @@ -247,10 +247,8 @@ pub trait Item: Focusable + EventEmitter + Render + Sized { /// /// By default this returns a [`Label`] that displays that text from /// `tab_content_text`. - fn tab_content(&self, params: TabContentParams, window: &Window, cx: &App) -> AnyElement { - let Some(text) = self.tab_content_text(window, cx) else { - return gpui::Empty.into_any(); - }; + fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement { + let text = self.tab_content_text(params.detail.unwrap_or_default(), cx); Label::new(text) .color(params.text_color()) @@ -258,11 +256,7 @@ pub trait Item: Focusable + EventEmitter + Render + Sized { } /// Returns the textual contents of the tab. - /// - /// Use this if you don't need to customize the tab contents. - fn tab_content_text(&self, _window: &Window, _cx: &App) -> Option { - None - } + fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString; fn tab_icon(&self, _window: &Window, _cx: &App) -> Option { None @@ -283,10 +277,6 @@ pub trait Item: Focusable + EventEmitter + Render + Sized { self.tab_tooltip_text(cx).map(TabTooltipContent::Text) } - fn tab_description(&self, _: usize, _: &App) -> Option { - None - } - fn to_item_events(_event: &Self::Event, _f: impl FnMut(ItemEvent)) {} fn deactivated(&mut self, _window: &mut Window, _: &mut Context) {} @@ -492,8 +482,8 @@ pub trait ItemHandle: 'static + Send { cx: &mut App, handler: Box, ) -> gpui::Subscription; - fn tab_description(&self, detail: usize, cx: &App) -> Option; fn tab_content(&self, params: TabContentParams, window: &Window, cx: &App) -> AnyElement; + fn tab_content_text(&self, detail: usize, cx: &App) -> SharedString; fn tab_icon(&self, window: &Window, cx: &App) -> Option; fn tab_tooltip_text(&self, cx: &App) -> Option; fn tab_tooltip_content(&self, cx: &App) -> Option; @@ -616,13 +606,12 @@ impl ItemHandle for Entity { self.read(cx).telemetry_event_text() } - fn tab_description(&self, detail: usize, cx: &App) -> Option { - self.read(cx).tab_description(detail, cx) - } - fn tab_content(&self, params: TabContentParams, window: &Window, cx: &App) -> AnyElement { self.read(cx).tab_content(params, window, cx) } + fn tab_content_text(&self, detail: usize, cx: &App) -> SharedString { + self.read(cx).tab_content_text(detail, cx) + } fn tab_icon(&self, window: &Window, cx: &App) -> Option { self.read(cx).tab_icon(window, cx) @@ -1450,11 +1439,15 @@ pub mod test { f(*event) } - fn tab_description(&self, detail: usize, _: &App) -> Option { - self.tab_descriptions.as_ref().and_then(|descriptions| { - let description = *descriptions.get(detail).or_else(|| descriptions.last())?; - Some(description.into()) - }) + fn tab_content_text(&self, detail: usize, _cx: &App) -> SharedString { + self.tab_descriptions + .as_ref() + .and_then(|descriptions| { + let description = *descriptions.get(detail).or_else(|| descriptions.last())?; + description.into() + }) + .unwrap_or_default() + .into() } fn telemetry_event_text(&self) -> Option<&'static str> { diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index e3556a5ad2..1eb142cffe 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -502,6 +502,7 @@ impl Pane { fn focus_in(&mut self, window: &mut Window, cx: &mut Context) { if !self.was_focused { self.was_focused = true; + self.update_history(self.active_item_index); cx.emit(Event::Focus); cx.notify(); } @@ -1095,17 +1096,7 @@ impl Pane { prev_item.deactivated(window, cx); } } - if let Some(newly_active_item) = self.items.get(index) { - self.activation_history - .retain(|entry| entry.entity_id != newly_active_item.item_id()); - self.activation_history.push(ActivationHistoryEntry { - entity_id: newly_active_item.item_id(), - timestamp: self - .next_activation_timestamp - .fetch_add(1, Ordering::SeqCst), - }); - } - + self.update_history(index); self.update_toolbar(window, cx); self.update_status_bar(window, cx); @@ -1127,6 +1118,19 @@ impl Pane { } } + fn update_history(&mut self, index: usize) { + if let Some(newly_active_item) = self.items.get(index) { + self.activation_history + .retain(|entry| entry.entity_id != newly_active_item.item_id()); + self.activation_history.push(ActivationHistoryEntry { + entity_id: newly_active_item.item_id(), + timestamp: self + .next_activation_timestamp + .fetch_add(1, Ordering::SeqCst), + }); + } + } + pub fn activate_prev_item( &mut self, activate_pane: bool, @@ -2634,7 +2638,7 @@ impl Pane { .items .iter() .enumerate() - .zip(tab_details(&self.items, cx)) + .zip(tab_details(&self.items, window, cx)) .map(|((ix, item), detail)| { self.render_tab(ix, &**item, detail, &focus_handle, window, cx) }) @@ -3632,7 +3636,7 @@ fn dirty_message_for(buffer_path: Option) -> String { format!("{path} contains unsaved edits. Do you want to save it?") } -pub fn tab_details(items: &[Box], cx: &App) -> Vec { +pub fn tab_details(items: &[Box], _window: &Window, cx: &App) -> Vec { let mut tab_details = items.iter().map(|_| 0).collect::>(); let mut tab_descriptions = HashMap::default(); let mut done = false; @@ -3641,15 +3645,12 @@ pub fn tab_details(items: &[Box], cx: &App) -> Vec { // Store item indices by their tab description. for (ix, (item, detail)) in items.iter().zip(&tab_details).enumerate() { - if let Some(description) = item.tab_description(*detail, cx) { - if *detail == 0 - || Some(&description) != item.tab_description(detail - 1, cx).as_ref() - { - tab_descriptions - .entry(description) - .or_insert(Vec::new()) - .push(ix); - } + let description = item.tab_content_text(*detail, cx); + if *detail == 0 || description != item.tab_content_text(detail - 1, cx) { + tab_descriptions + .entry(description) + .or_insert(Vec::new()) + .push(ix); } } diff --git a/crates/workspace/src/shared_screen.rs b/crates/workspace/src/shared_screen.rs index 156424dde6..febb83d683 100644 --- a/crates/workspace/src/shared_screen.rs +++ b/crates/workspace/src/shared_screen.rs @@ -93,8 +93,8 @@ impl Item for SharedScreen { Some(Icon::new(IconName::Screen)) } - fn tab_content_text(&self, _window: &Window, _cx: &App) -> Option { - Some(format!("{}'s screen", self.user.github_login).into()) + fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { + format!("{}'s screen", self.user.github_login).into() } fn telemetry_event_text(&self) -> Option<&'static str> { diff --git a/crates/workspace/src/theme_preview.rs b/crates/workspace/src/theme_preview.rs index 60ec195c40..8bdb4c614e 100644 --- a/crates/workspace/src/theme_preview.rs +++ b/crates/workspace/src/theme_preview.rs @@ -80,9 +80,9 @@ impl Item for ThemePreview { fn to_item_events(_: &Self::Event, _: impl FnMut(crate::item::ItemEvent)) {} - fn tab_content_text(&self, window: &Window, cx: &App) -> Option { + fn tab_content_text(&self, _detail: usize, cx: &App) -> SharedString { let name = cx.theme().name.clone(); - Some(format!("{} Preview", name).into()) + format!("{} Preview", name).into() } fn telemetry_event_text(&self) -> Option<&'static str> { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 3ecc847bed..52f0bf8748 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1460,6 +1460,27 @@ impl Workspace { &self.project } + pub fn recently_activated_items(&self, cx: &App) -> HashMap { + let mut history: HashMap = HashMap::default(); + + for pane_handle in &self.panes { + let pane = pane_handle.read(cx); + + for entry in pane.activation_history() { + history.insert( + entry.entity_id, + history + .get(&entry.entity_id) + .cloned() + .unwrap_or(0) + .max(entry.timestamp), + ); + } + } + + history + } + pub fn recent_navigation_history_iter( &self, cx: &App, @@ -2105,7 +2126,7 @@ impl Workspace { .flat_map(|pane| { pane.read(cx).items().filter_map(|item| { if item.is_dirty(cx) { - item.tab_description(0, cx); + item.tab_content_text(0, cx); Some((pane.downgrade(), item.boxed_clone())) } else { None @@ -9022,6 +9043,9 @@ mod tests { impl Item for TestPngItemView { type Event = (); + fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { + "".into() + } } impl EventEmitter<()> for TestPngItemView {} impl Focusable for TestPngItemView { @@ -9094,6 +9118,9 @@ mod tests { impl Item for TestIpynbItemView { type Event = (); + fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { + "".into() + } } impl EventEmitter<()> for TestIpynbItemView {} impl Focusable for TestIpynbItemView { @@ -9137,6 +9164,9 @@ mod tests { impl Item for TestAlternatePngItemView { type Event = (); + fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { + "".into() + } } impl EventEmitter<()> for TestAlternatePngItemView {}