Skip to content

Commit 894b39a

Browse files
alyginConradIrwinmikayla-maki
authored
Add tab switcher (#7987)
The Tab Switcher implementation (#7653): - `ctrl-tab` opens the Tab Switcher and moves selection to the previously selcted tab. It also cycles selection forward. - `ctrl-shift-tab` opens the Tab Switcher and moves selection to the last tab in the list. It also cycles selection backward. - Tab is selected and the Tab Switcher is closed on the shortcut modifier key (`ctrl` by default) release. - List items are in reverse activation history order. - The list reacts to the item changes in background (new tab, tab closed, tab title changed etc.) Intentionally not in scope of this PR: - File icons - Close buttons I will come back to these features. I think they need to be implemented in separate PRs, and be synchronized with changes in how tabs are rendered, to reuse the code as it's done in the current implementation. The Tab Switcher looks usable even without them. Known Issues: Tab Switcher doesn't react to mouse click on a list item. It's not a tab switcher specific problem, it looks like ctrl-clicks are not handled the same way in Zed as cmd-clicks. For instance, menu items can be activated with cmd-click, but don't react to ctrl-click. Since the Tab Switcher's default keybinding is `ctrl-tab`, the user can only click an item with `ctrl` pushed down, thus preventing `on_click()` from firing. fixes #7653, #7321 Release Notes: - Added Tab Switcher which is accessible via `ctrl-tab` and `ctrl-shift-tab` (#7653) (#7321) Related issues: - Unblocks #7356, I hope 😄 How it looks and works (it's only `ctrl-tab`'s and `ctrl-shift-tab`'s, no `enter`'s or mouse clicks): https://github.com/zed-industries/zed/assets/2101250/4ad4ec6a-5314-481b-8b35-7ac85e43eb92 --------- Co-authored-by: Conrad Irwin <[email protected]> Co-authored-by: Mikayla Maki <[email protected]>
1 parent 9c22009 commit 894b39a

File tree

13 files changed

+715
-56
lines changed

13 files changed

+715
-56
lines changed

Cargo.lock

Lines changed: 23 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ members = [
7777
"crates/story",
7878
"crates/storybook",
7979
"crates/sum_tree",
80+
"crates/tab_switcher",
8081
"crates/terminal",
8182
"crates/terminal_view",
8283
"crates/text",
@@ -188,6 +189,7 @@ sqlez_macros = { path = "crates/sqlez_macros" }
188189
story = { path = "crates/story" }
189190
storybook = { path = "crates/storybook" }
190191
sum_tree = { path = "crates/sum_tree" }
192+
tab_switcher = { path = "crates/tab_switcher" }
191193
terminal = { path = "crates/terminal" }
192194
terminal_view = { path = "crates/terminal_view" }
193195
text = { path = "crates/text" }

assets/keymaps/default-linux.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -263,9 +263,7 @@
263263
{
264264
"context": "Pane",
265265
"bindings": {
266-
"ctrl-shift-tab": "pane::ActivatePrevItem",
267266
"ctrl-pageup": "pane::ActivatePrevItem",
268-
"ctrl-tab": "pane::ActivateNextItem",
269267
"ctrl-pagedown": "pane::ActivateNextItem",
270268
"ctrl-w": "pane::CloseActiveItem",
271269
"alt-ctrl-t": "pane::CloseInactiveItems",
@@ -420,6 +418,8 @@
420418
"ctrl-k ctrl-t": "theme_selector::Toggle",
421419
"ctrl-t": "project_symbols::Toggle",
422420
"ctrl-p": "file_finder::Toggle",
421+
"ctrl-tab": "tab_switcher::Toggle",
422+
"ctrl-shift-tab": ["tab_switcher::Toggle", { "select_last": true }],
423423
"ctrl-e": "file_finder::Toggle",
424424
"ctrl-shift-p": "command_palette::Toggle",
425425
"ctrl-shift-m": "diagnostics::Deploy",
@@ -589,6 +589,10 @@
589589
"context": "FileFinder",
590590
"bindings": { "ctrl-shift-p": "file_finder::SelectPrev" }
591591
},
592+
{
593+
"context": "TabSwitcher",
594+
"bindings": { "ctrl-shift-tab": "menu::SelectPrev" }
595+
},
592596
{
593597
"context": "Terminal",
594598
"bindings": {

assets/keymaps/default-macos.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"cmd-enter": "menu::SecondaryConfirm",
1818
"escape": "menu::Cancel",
1919
"cmd-escape": "menu::Cancel",
20+
"ctrl-escape": "menu::Cancel",
2021
"ctrl-c": "menu::Cancel",
2122
"shift-enter": "menu::UseSelectedQuery",
2223
"cmd-shift-w": "workspace::CloseWindow",
@@ -441,6 +442,8 @@
441442
"cmd-k cmd-t": "theme_selector::Toggle",
442443
"cmd-t": "project_symbols::Toggle",
443444
"cmd-p": "file_finder::Toggle",
445+
"ctrl-tab": "tab_switcher::Toggle",
446+
"ctrl-shift-tab": ["tab_switcher::Toggle", { "select_last": true }],
444447
"cmd-shift-p": "command_palette::Toggle",
445448
"cmd-shift-m": "diagnostics::Deploy",
446449
"cmd-shift-e": "project_panel::ToggleFocus",
@@ -603,6 +606,10 @@
603606
"context": "FileFinder",
604607
"bindings": { "cmd-shift-p": "file_finder::SelectPrev" }
605608
},
609+
{
610+
"context": "TabSwitcher",
611+
"bindings": { "ctrl-shift-tab": "menu::SelectPrev" }
612+
},
606613
{
607614
"context": "Terminal",
608615
"bindings": {

crates/picker/src/head.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,13 @@ impl Head {
2828
Self::Editor(editor)
2929
}
3030

31-
pub fn empty(cx: &mut WindowContext) -> Self {
32-
Self::Empty(cx.new_view(|cx| EmptyHead::new(cx)))
31+
pub fn empty<V: 'static>(
32+
blur_handler: impl FnMut(&mut V, &mut ViewContext<'_, V>) + 'static,
33+
cx: &mut ViewContext<V>,
34+
) -> Self {
35+
let head = cx.new_view(|cx| EmptyHead::new(cx));
36+
cx.on_blur(&head.focus_handle(cx), blur_handler).detach();
37+
Self::Empty(head)
3338
}
3439
}
3540

crates/picker/src/picker.rs

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ use anyhow::Result;
22
use editor::{scroll::Autoscroll, Editor};
33
use gpui::{
44
div, list, prelude::*, uniform_list, AnyElement, AppContext, ClickEvent, DismissEvent,
5-
EventEmitter, FocusHandle, FocusableView, Length, ListState, Render, Task,
6-
UniformListScrollHandle, View, ViewContext, WindowContext,
5+
EventEmitter, FocusHandle, FocusableView, Length, ListState, MouseButton, MouseUpEvent, Render,
6+
Task, UniformListScrollHandle, View, ViewContext, WindowContext,
77
};
88
use head::Head;
99
use std::{sync::Arc, time::Duration};
@@ -116,7 +116,7 @@ impl<D: PickerDelegate> Picker<D> {
116116
/// A picker, which displays its matches using `gpui::uniform_list`, all matches should have the same height.
117117
/// If `PickerDelegate::render_match` can return items with different heights, use `Picker::list`.
118118
pub fn nonsearchable_uniform_list(delegate: D, cx: &mut ViewContext<Self>) -> Self {
119-
let head = Head::empty(cx);
119+
let head = Head::empty(Self::on_empty_head_blur, cx);
120120

121121
Self::new(delegate, ContainerKind::UniformList, head, cx)
122122
}
@@ -313,6 +313,13 @@ impl<D: PickerDelegate> Picker<D> {
313313
}
314314
}
315315

316+
fn on_empty_head_blur(&mut self, cx: &mut ViewContext<Self>) {
317+
let Head::Empty(_) = &self.head else {
318+
panic!("unexpected call");
319+
};
320+
self.cancel(&menu::Cancel, cx);
321+
}
322+
316323
pub fn refresh(&mut self, cx: &mut ViewContext<Self>) {
317324
let query = self.query(cx);
318325
self.update_matches(query, cx);
@@ -394,6 +401,16 @@ impl<D: PickerDelegate> Picker<D> {
394401
.on_click(cx.listener(move |this, event: &ClickEvent, cx| {
395402
this.handle_click(ix, event.down.modifiers.command, cx)
396403
}))
404+
// As of this writing, GPUI intercepts `ctrl-[mouse-event]`s on macOS
405+
// and produces right mouse button events. This matches platforms norms
406+
// but means that UIs which depend on holding ctrl down (such as the tab
407+
// switcher) can't be clicked on. Hence, this handler.
408+
.on_mouse_up(
409+
MouseButton::Right,
410+
cx.listener(move |this, event: &MouseUpEvent, cx| {
411+
this.handle_click(ix, event.modifiers.command, cx)
412+
}),
413+
)
397414
.children(
398415
self.delegate
399416
.render_match(ix, ix == self.delegate.selected_index(), cx),

crates/tab_switcher/Cargo.toml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
[package]
2+
name = "tab_switcher"
3+
version = "0.1.0"
4+
edition = "2021"
5+
publish = false
6+
license = "GPL-3.0-or-later"
7+
8+
[lib]
9+
path = "src/tab_switcher.rs"
10+
doctest = false
11+
12+
[dependencies]
13+
collections.workspace = true
14+
gpui.workspace = true
15+
menu.workspace = true
16+
picker.workspace = true
17+
serde.workspace = true
18+
ui.workspace = true
19+
util.workspace = true
20+
workspace.workspace = true
21+
22+
[dev-dependencies]
23+
anyhow.workspace = true
24+
ctor.workspace = true
25+
editor.workspace = true
26+
env_logger.workspace = true
27+
gpui = { workspace = true, features = ["test-support"] }
28+
language = { workspace = true, features = ["test-support"] }
29+
project.workspace = true
30+
serde_json.workspace = true
31+
theme = { workspace = true, features = ["test-support"] }
32+
workspace = { workspace = true, features = ["test-support"] }

crates/tab_switcher/LICENSE-GPL

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../LICENSE-GPL

0 commit comments

Comments
 (0)