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 <conrad.irwin@gmail.com>
This commit is contained in:
parent
52eef3c35d
commit
4dff47ae20
40 changed files with 360 additions and 181 deletions
|
@ -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 {}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue