Detailed changes
@@ -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",
@@ -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 {
@@ -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 {}
@@ -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<SharedString> {
- Some("Configuration".into())
+ fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
+ "Configuration".into()
}
}
@@ -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<SharedString> {
- 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)) {
@@ -108,8 +108,8 @@ impl EventEmitter<()> for ContextHistory {}
impl Item for ContextHistory {
type Event = ();
- fn tab_content_text(&self, _window: &Window, _cx: &App) -> Option<SharedString> {
- Some("History".into())
+ fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
+ "History".into()
}
}
@@ -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,
);
@@ -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<Workspace>, 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::<Editor>(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::<Editor>(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::<Editor>(cx).unwrap();
- assert_eq!(editor.tab_description(0, cx).unwrap(), "2.js");
+ assert_eq!(editor.tab_content_text(0, cx), "2.js");
});
}
@@ -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 {
@@ -735,8 +735,8 @@ impl From<ComponentId> for ActivePageId {
impl Item for ComponentPreview {
type Event = ItemEvent;
- fn tab_content_text(&self, _window: &Window, _cx: &App) -> Option<SharedString> {
- 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> {
@@ -727,8 +727,8 @@ impl Item for DapLogView {
Editor::to_item_events(event, f)
}
- fn tab_content_text(&self, _window: &Window, _cx: &App) -> Option<SharedString> {
- 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> {
@@ -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 {
@@ -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<SharedString> {
- Some(self.kind.to_shared_string())
+ fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
+ self.kind.to_shared_string()
}
fn tab_content(
@@ -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()
@@ -619,9 +619,12 @@ impl Item for Editor {
None
}
- fn tab_description(&self, detail: usize, cx: &App) -> Option<SharedString> {
- 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<Icon> {
@@ -285,8 +285,8 @@ impl Item for ProposedChangesEditor {
Some(Icon::new(IconName::Diff))
}
- fn tab_content_text(&self, _window: &Window, _cx: &App) -> Option<SharedString> {
- Some(self.title.clone())
+ fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
+ self.title.clone()
}
fn as_searchable(&self, _: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
@@ -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<SharedString> {
- Some("Extensions".into())
+ fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
+ "Extensions".into()
}
fn telemetry_event_text(&self) -> Option<&'static str> {
@@ -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<ui::SharedString> {
let short_sha = self.commit.sha.get(0..16).unwrap_or(&*self.commit.sha);
let subject = self.commit.message.split('\n').next().unwrap();
@@ -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")
}
@@ -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<Icon> {
let path = self.image_item.read(cx).path();
ItemSettings::get_global(cx)
@@ -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<SharedString> {
- 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> {
@@ -1058,8 +1058,8 @@ impl Item for LspLogView {
Editor::to_item_events(event, f)
}
- fn tab_content_text(&self, _window: &Window, _cx: &App) -> Option<SharedString> {
- 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> {
@@ -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<SharedString> {
- 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> {
@@ -35,8 +35,7 @@ pub struct MarkdownPreviewView {
contents: Option<ParsedMarkdown>,
selected_block: usize,
list_state: ListState,
- tab_description: Option<String>,
- fallback_tab_description: SharedString,
+ tab_content_text: SharedString,
language_registry: Arc<LanguageRegistry>,
parsing_markdown_task: Option<Task<Result<()>>>,
}
@@ -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<Editor>,
workspace: WeakEntity<Workspace>,
language_registry: Arc<LanguageRegistry>,
- fallback_description: Option<SharedString>,
+ tab_content_text: SharedString,
window: &mut Window,
cx: &mut Context<Workspace>,
) -> Entity<Self> {
@@ -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<SharedString> {
- 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> {
@@ -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 {}
@@ -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<Icon> {
@@ -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<SharedString> {
- 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> {
@@ -446,7 +446,7 @@ impl Item for ProjectSearchView {
Some(Icon::new(IconName::MagnifyingGlass))
}
- fn tab_content_text(&self, _: &Window, cx: &App) -> Option<SharedString> {
+ fn tab_content_text(&self, _detail: usize, cx: &App) -> SharedString {
let last_query: Option<SharedString> = 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> {
@@ -289,8 +289,8 @@ impl EventEmitter<()> for ProjectIndexDebugView {}
impl Item for ProjectIndexDebugView {
type Event = ();
- fn tab_content_text(&self, _window: &Window, _cx: &App) -> Option<SharedString> {
- Some("Project Index (Debug)".into())
+ fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
+ "Project Index (Debug)".into()
}
fn clone_on_split(
@@ -160,8 +160,8 @@ impl Item for SettingsPage {
Some(Icon::new(IconName::Settings))
}
- fn tab_content_text(&self, _window: &Window, _cx: &App) -> Option<SharedString> {
- Some("Settings".into())
+ fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
+ "Settings".into()
}
fn show_toolbar(&self) -> bool {
@@ -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
@@ -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<Picker<TabSwitcherDelegate>>,
@@ -53,7 +54,19 @@ impl TabSwitcher {
) {
workspace.register_action(|workspace, action: &Toggle, window, cx| {
let Some(tab_switcher) = workspace.active_modal::<Self>(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::<Self>(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<Workspace>,
) {
@@ -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>) -> Self {
+ fn new(
+ delegate: TabSwitcherDelegate,
+ window: &mut Window,
+ is_global: bool,
+ cx: &mut Context<Self>,
+ ) -> 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<Pane>,
item_index: usize,
item: Box<dyn ItemHandle>,
detail: usize,
@@ -175,27 +210,34 @@ pub struct TabSwitcherDelegate {
tab_switcher: WeakEntity<TabSwitcher>,
selected_index: usize,
pane: WeakEntity<Pane>,
+ workspace: WeakEntity<Workspace>,
project: Entity<Project>,
matches: Vec<TabMatch>,
+ is_all_panes: bool,
}
impl TabSwitcherDelegate {
+ #[allow(clippy::complexity)]
fn new(
project: Entity<Project>,
- action: &Toggle,
+ select_last: bool,
tab_switcher: WeakEntity<TabSwitcher>,
pane: WeakEntity<Pane>,
+ workspace: WeakEntity<Workspace>,
+ is_all_panes: bool,
window: &mut Window,
cx: &mut Context<TabSwitcher>,
) -> 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<Box<dyn ItemHandle>> =
+ 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::<Vec<_>>();
+ 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<Picker<Self>>,
+ ) {
+ 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<Picker<Self>>,
) -> 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<Picker<TabSwitcherDelegate>>,
) {
- 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<Picker<TabSwitcherDelegate>>) {
@@ -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
}
@@ -796,8 +796,8 @@ fn generate_commands(_: &App) -> Vec<VimCommand> {
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),
@@ -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<SharedString> {
- Some("Welcome".into())
+ fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
+ "Welcome".into()
}
fn telemetry_event_text(&self) -> Option<&'static str> {
@@ -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<Self::Event> + 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<Self::Event> + 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<SharedString> {
- None
- }
+ fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString;
fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
None
@@ -283,10 +277,6 @@ pub trait Item: Focusable + EventEmitter<Self::Event> + Render + Sized {
self.tab_tooltip_text(cx).map(TabTooltipContent::Text)
}
- fn tab_description(&self, _: usize, _: &App) -> Option<SharedString> {
- None
- }
-
fn to_item_events(_event: &Self::Event, _f: impl FnMut(ItemEvent)) {}
fn deactivated(&mut self, _window: &mut Window, _: &mut Context<Self>) {}
@@ -492,8 +482,8 @@ pub trait ItemHandle: 'static + Send {
cx: &mut App,
handler: Box<dyn Fn(ItemEvent, &mut Window, &mut App)>,
) -> gpui::Subscription;
- fn tab_description(&self, detail: usize, cx: &App) -> Option<SharedString>;
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<Icon>;
fn tab_tooltip_text(&self, cx: &App) -> Option<SharedString>;
fn tab_tooltip_content(&self, cx: &App) -> Option<TabTooltipContent>;
@@ -616,13 +606,12 @@ impl<T: Item> ItemHandle for Entity<T> {
self.read(cx).telemetry_event_text()
}
- fn tab_description(&self, detail: usize, cx: &App) -> Option<SharedString> {
- 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<Icon> {
self.read(cx).tab_icon(window, cx)
@@ -1450,11 +1439,15 @@ pub mod test {
f(*event)
}
- fn tab_description(&self, detail: usize, _: &App) -> Option<SharedString> {
- 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> {
@@ -502,6 +502,7 @@ impl Pane {
fn focus_in(&mut self, window: &mut Window, cx: &mut Context<Self>) {
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<ProjectPath>) -> String {
format!("{path} contains unsaved edits. Do you want to save it?")
}
-pub fn tab_details(items: &[Box<dyn ItemHandle>], cx: &App) -> Vec<usize> {
+pub fn tab_details(items: &[Box<dyn ItemHandle>], _window: &Window, cx: &App) -> Vec<usize> {
let mut tab_details = items.iter().map(|_| 0).collect::<Vec<_>>();
let mut tab_descriptions = HashMap::default();
let mut done = false;
@@ -3641,15 +3645,12 @@ pub fn tab_details(items: &[Box<dyn ItemHandle>], cx: &App) -> Vec<usize> {
// 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);
}
}
@@ -93,8 +93,8 @@ impl Item for SharedScreen {
Some(Icon::new(IconName::Screen))
}
- fn tab_content_text(&self, _window: &Window, _cx: &App) -> Option<SharedString> {
- 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> {
@@ -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<SharedString> {
+ 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> {
@@ -1460,6 +1460,27 @@ impl Workspace {
&self.project
}
+ pub fn recently_activated_items(&self, cx: &App) -> HashMap<EntityId, usize> {
+ let mut history: HashMap<EntityId, usize> = 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 {}