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 f79af953ccbc12c2e633393ad171016d112d815a..bc4752bf119476e31bc3b67c45541f8ab76ffa78 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 0e66308f373954112c77c85f67abdd409960e2d9..b09c0015c5c357d49b75795d949a431a93ee2a5c 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 b5c8bf02487fc72e83254dfd651ec047f957fffa..d610470e63a38ac033c0aa8fd5600c27f5d74f0a 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 cb3d268a63f947cb2687c980d366b81f729a5163..6b96051a5fe62288e5aa1a8ae88386e368e317ea 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 94e267c85aba6cce0be6ae8491a30c49bf901f05..840ed0eda34e2286ec684da82a8d6ef60faec892 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 35b932158bad65357741d2c2e667680bd0c106e4..560033be34a6b11240cb0a0267e035461fd490ca 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 b0fd67add03dc4f429b4b800b864122c64d41302..044a6b2922e83d7e23e1587759a751d71961a24e 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 78d30adb4a53ccf5ac2b0bbe9b0e1d93a26e9f2b..57494bd42b49ad066dc131487a83ba07db211336 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 554cfe113a426b943314a6bce0238fb95bd8ff39..bb7192026ee97c137f149ff94ea5cf356d18d716 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 74105a0213a9394c4ff13e5b4384a17ce7206059..9a2abb9929a0de350cd2e9f3dbcf912a4c4ba3ca 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 acd26e2d7fd4d2bde679255b2a0e79e53781d6f3..eca26d2b733148f3cc45946e179e1f1234dba389 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 974f205fa684b045fb1654830a43a1903746ae0f..756f866bf139c87d427abd76659e881445f8a46d 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 45af03623a4a5fcc2c30dbff9200cc704d728d5c..9d4f34b5cdeaadef7c8424b34594e97d82b8223f 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 18602356c587ad251df052aa533a007e3f6eadfc..b0ead9ea9b36d54ef6bda7a362237e90222f3573 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 476a05b29fffb59621ef721b356403a524691383..232024e554e0995b2c7c1c0e3a69d0a8d97f9ac2 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 0eebddb640e5c8301eb1597b5c1599c7bdacc074..734d39cfe6a462c0d0ff8940ad8bda396139d826 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 92570f72c9ad5a5a73ffe97036666a5b144ea7d6..430b656f09679bf5c0e21512615c3c447e001f8b 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 d7ec18902853bbfd6d656de513bdf161b063310c..3f8b2f52d5d5a71951303ad4084dd9fa59ba2300 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 eb93f97d3f5c3d21cd3a1ee10ba4f6096d5e4972..36ad7e528c10aa786d6c586aa3ee60a8ef3fce42 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 0a8043a1c60ad2b9ac7bd42dc5e3025cac4ab701..5795265e3696a36fbebf11137ac12922c0b6a1a1 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 68f54e052834123c1e3d247f913af4469f98bd06..0969b0edf64d82cf0860ce01353ea59bcd095068 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 b90e925fed30ef66d7452223cabfaaa89c27589c..8b29ab6298ef58fa6dcc7c8041b3fcac209a52fa 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 2098c866734d029f5194e176ed5aede1d928e59b..3a14181db004d0f4aef7f1572aed4485476aaba6 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 c6b554349d0169dff120e76056de50833efcbce9..bbcb196b293d45b6d9948291ad07190dda0c0cb2 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 65eeae795e5f0a36747640ce82d2e618709429c1..8badba47386fde27af4644f02c3eee2bd0ca0b6c 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 4a539a64a7c740f00adc37cfa311b8a47a279af1..07f3e63b2449c3842de5db0c84663371be30a7fa 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 abae381276b76b282974f3ae65b3fdb0ebdcc0ac..df7ce574abadc6d6709fe5bb572e60b39041b8b8 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 6223e32190fd782456cdf88371b32acbafd27949..40447cf2fcd3615043d5ad52a7cfb6b25a71bd92 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 140599d22a4b91eee8db456a0e9f57c16c5a6f1c..15b86c3f77f3ab613607c563717b49598a3320b3 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 65c420c6bf085956f8152ce0fae61c7dae1dc862..3428a99bf839fe2d8a944b4b1539b3f5af73f0e9 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 55545016d53e286ab9433aaa6cb816afb626ed7b..027268e7d70aa8dcf6184c86f3cc141f46a418a5 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 14553c016eb2ee935acadc881561b4e32be44881..25cfcfba7b6c6fe237790f5f5ceb13562b65db32 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 74853569bcee3ae2e66edb11c55707c6b6b1f490..3323c1de6a68d6c4b4d3b97e2ae3cae4472d7836 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 1e55e5a9f41acc99633c1a6d59f1907e3224bf5a..3645993edad5e9132fc1e2f0f6c85e3ec0e123db 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 540a3de990ec99fa455974d387afd76527b413f6..52e7c0ea5dd13be77ee3c0a71ed12837c3735428 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 26440ce1e4845b5f91cea62382e4e90685ce95e3..00aa340e66d402bf840b65aff3c75d35f2d67c28 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 e3556a5ad2529603b553681d5a6bc3bf54940a8e..1eb142cffe098feabe8075e8aca2cb0481ec3284 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 156424dde6006735a479c13ae5d9980e5b8d6b6e..febb83d6838bb4733b056d9775fe1922e343fa6c 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 60ec195c40b519d1cfdcdef74a14702535b2f141..8bdb4c614ea073153777601298507b6f3a8c61a0 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 3ecc847bed9da09c21a17efd8a94e18dd5c21675..52f0bf8748de1ac47a94ef6f6ad22a243440aa7b 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 {}