Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 58 additions & 1 deletion app/src/settings_view/appearance_page.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ use crate::window_settings::{
use crate::workspace::header_toolbar_editor::HeaderToolbarInlineEditor;
use crate::workspace::tab_settings::{
DirectoryTabColor, PreserveActiveTabColor, ShowCodeReviewButton, ShowIndicatorsButton,
ShowVerticalTabPanelInRestoredWindows, TabCloseButtonPosition, TabSettings,
ShowTabNumbers, ShowVerticalTabPanelInRestoredWindows, TabCloseButtonPosition, TabSettings,
TabSettingsChangedEvent, UseLatestUserPromptAsConversationTitleInTabNames, UseVerticalTabs,
WorkspaceDecorationVisibility,
};
Expand Down Expand Up @@ -444,6 +444,7 @@ pub enum AppearancePageAction {
ToggleMatchNotebookToMonospaceFontSize,
ToggleMatchAIToTerminalFontFamily,
ToggleTabIndicators,
ToggleShowTabNumbers,
ToggleShowCodeReviewButton,
TogglePreserveActiveTabColor,
ToggleVerticalTabs,
Expand Down Expand Up @@ -583,6 +584,7 @@ impl TypedActionView for AppearanceSettingsPageView {
ctx.open_url(url);
}
ToggleTabIndicators => self.toggle_tab_indicators(ctx),
ToggleShowTabNumbers => self.toggle_show_tab_numbers(ctx),
ToggleShowCodeReviewButton => self.toggle_show_code_review_button(ctx),
TogglePreserveActiveTabColor => self.toggle_preserve_active_tab_color(ctx),
ToggleVerticalTabs => self.toggle_vertical_tabs(ctx),
Expand Down Expand Up @@ -1357,6 +1359,7 @@ impl AppearanceSettingsPageView {
let tab_settings = TabSettings::as_ref(ctx);
let mut tab_settings_widgets: Vec<Box<dyn SettingsWidget<View = Self>>> =
vec![Box::new(TabIndicatorWidget::default())];
tab_settings_widgets.push(Box::new(ShowTabNumbersWidget::default()));
if !FeatureFlag::OpenWarpNewSettingsModes.is_enabled() {
tab_settings_widgets.push(Box::new(CodeReviewButtonWidget::default()));
}
Expand Down Expand Up @@ -2271,6 +2274,15 @@ impl AppearanceSettingsPageView {
);
}

fn toggle_show_tab_numbers(&mut self, ctx: &mut ViewContext<Self>) {
let tab_settings = TabSettings::handle(ctx);
let new_value = !*tab_settings.as_ref(ctx).show_tab_numbers.value();

ctx.update_model(&tab_settings, move |tab_settings, ctx| {
report_if_error!(tab_settings.show_tab_numbers.set_value(new_value, ctx));
});
}

fn toggle_show_code_review_button(&mut self, ctx: &mut ViewContext<Self>) {
let tab_settings = TabSettings::handle(ctx);
let new_value = !*tab_settings.as_ref(ctx).show_code_review_button.value();
Expand Down Expand Up @@ -4460,6 +4472,51 @@ impl SettingsWidget for TabIndicatorWidget {
}
}

#[derive(Default)]
struct ShowTabNumbersWidget {
switch_state: SwitchStateHandle,
}

impl SettingsWidget for ShowTabNumbersWidget {
type View = AppearanceSettingsPageView;

fn search_terms(&self) -> &str {
"tab number index position cmd shortcut switch"
}

fn render(
&self,
view: &Self::View,
appearance: &Appearance,
app: &AppContext,
) -> Box<dyn Element> {
let tab_settings = TabSettings::as_ref(app);

render_body_item::<AppearancePageAction>(
"Show tab numbers".into(),
None,
LocalOnlyIconState::for_setting(
ShowTabNumbers::storage_key(),
ShowTabNumbers::sync_to_cloud(),
&mut view.local_only_icon_tooltip_states.borrow_mut(),
app,
),
ToggleState::Enabled,
appearance,
appearance
.ui_builder()
.switch(self.switch_state.clone())
.check(*tab_settings.show_tab_numbers)
.build()
.on_click(move |ctx, _, _| {
ctx.dispatch_typed_action(AppearancePageAction::ToggleShowTabNumbers);
})
.finish(),
None,
)
}
}

#[derive(Default)]
struct CodeReviewButtonWidget {
switch_state: SwitchStateHandle,
Expand Down
15 changes: 14 additions & 1 deletion app/src/tab.rs
Original file line number Diff line number Diff line change
Expand Up @@ -698,6 +698,10 @@ pub struct TabComponent<'a> {
title: String,
has_custom_title: bool,
tab_index: usize,
/// When `true`, the tab's 1-based position number is rendered before its
/// title to make the Cmd+1..9 switch shortcuts discoverable. Controlled by
/// the `appearance.tabs.show_tab_numbers` setting.
show_tab_number: bool,
styles: TabStyles,
ui_builder: UiBuilder,
indicator: Indicator,
Expand Down Expand Up @@ -813,6 +817,7 @@ impl<'a> TabComponent<'a> {
.as_ref(ctx)
.is_terminal_pane_being_shared(ctx);
let should_show_indicators = *TabSettings::as_ref(ctx).show_indicators.value();
let show_tab_number = *TabSettings::as_ref(ctx).show_tab_numbers.value();
let are_inputs_synced = SyncedInputState::as_ref(ctx)
.should_sync_this_pane_group(tab.pane_group.id(), tab.pane_group.window_id(ctx));

Expand Down Expand Up @@ -867,6 +872,7 @@ impl<'a> TabComponent<'a> {
title,
has_custom_title: tab.pane_group.as_ref(ctx).custom_title(ctx).is_some(),
tab_index,
show_tab_number,
styles: TabStyles::default(appearance, tab.color()),
ui_builder: appearance.ui_builder().clone(),
indicator,
Expand Down Expand Up @@ -1087,8 +1093,15 @@ impl<'a> TabComponent<'a> {
)
.finish()
} else {
// Optionally prefix the title with the tab's 1-based position so the
// Cmd+1..9 switch shortcuts are discoverable at a glance.
let title = if self.show_tab_number {
format!("{} {}", self.tab_index + 1, self.title)
} else {
self.title.clone()
};
Text::new_inline(
self.title.clone(),
title,
self.styles
.default
.font_family_id
Expand Down
9 changes: 9 additions & 0 deletions app/src/workspace/tab_settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,15 @@ define_settings_group!(TabSettings, settings: [
toml_path: "appearance.tabs.show_indicators_button",
description: "Whether to show activity indicators on tabs.",
},
show_tab_numbers: ShowTabNumbers {
type: bool,
default: false,
supported_platforms: SupportedPlatforms::ALL,
sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes),
private: false,
toml_path: "appearance.tabs.show_tab_numbers",
description: "Whether to show each tab's position number (matching the Cmd+1..9 switch shortcuts) before its title.",
},
show_code_review_button: ShowCodeReviewButton {
type: bool,
default: true,
Expand Down
3 changes: 3 additions & 0 deletions app/src/workspace/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3531,6 +3531,9 @@ impl Workspace {
self.sync_panel_positions_from_config(ctx);
ctx.notify();
}
TabSettingsChangedEvent::ShowTabNumbers { .. } => {
ctx.notify();
}
}
}

Expand Down
51 changes: 42 additions & 9 deletions app/src/workspace/view/vertical_tabs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,7 @@ fn render_pane_row_element(
rename_editor: _,
is_pane_being_renamed,
pane_rename_editor: _,
tab_number: _,
} = props;
let is_selected = is_active_tab && is_focused;
let mut row = Hoverable::new(mouse_state, move |state| {
Expand Down Expand Up @@ -691,6 +692,11 @@ struct PaneProps<'a> {
rename_editor: Option<ViewHandle<EditorView>>,
is_pane_being_renamed: bool,
pane_rename_editor: Option<ViewHandle<EditorView>>,
/// When `Some(n)`, this row represents tab `n` (1-based) and a tab-number
/// label is drawn before its icon. Controlled by the
/// `appearance.tabs.show_tab_numbers` setting; set only on each tab's
/// representative row so the Cmd+1..9 shortcuts are discoverable.
tab_number: Option<usize>,
}

struct PaneRowState {
Expand Down Expand Up @@ -1794,6 +1800,7 @@ fn render_tab_group_internal(
let pane_group_id = tab.pane_group.id();
let visible_pane_ids = pane_group.visible_pane_ids();
let resolved_mode = resolve_vertical_tabs_mode(app);
let show_tab_number = *TabSettings::as_ref(app).show_tab_numbers.value();
let display_granularity = match resolved_mode {
VerticalTabsResolvedMode::Panes => VerticalTabsDisplayGranularity::Panes,
VerticalTabsResolvedMode::FocusedSession | VerticalTabsResolvedMode::Summary => {
Expand Down Expand Up @@ -1897,7 +1904,7 @@ fn render_tab_group_internal(
.entry(*pane_id)
.or_default()
.clone();
let Some(pane_props) = PaneProps::new(
let Some(mut pane_props) = PaneProps::new(
pane_group,
*pane_id,
pane_group_id,
Expand All @@ -1922,6 +1929,7 @@ fn render_tab_group_internal(
) else {
return Empty::new().finish();
};
pane_props.tab_number = show_tab_number.then_some(tab_index + 1);
rows.add_child(render_summary_tab_item(
pane_props,
summary
Expand All @@ -1932,7 +1940,7 @@ fn render_tab_group_internal(
));
return rows.finish();
}
for (pane_id, row_mouse_state) in &row_mouse_states {
for (row_idx, (pane_id, row_mouse_state)) in row_mouse_states.iter().enumerate() {
let pane_color = per_pane_colors
.as_ref()
.and_then(|map| map.get(pane_id).copied())
Expand All @@ -1950,7 +1958,7 @@ fn render_tab_group_internal(
let is_pane_being_renamed = workspace
.current_workspace_state
.is_pane_being_renamed(locator);
let Some(pane_props) = PaneProps::new(
let Some(mut pane_props) = PaneProps::new(
pane_group,
*pane_id,
pane_group_id,
Expand All @@ -1975,6 +1983,8 @@ fn render_tab_group_internal(
) else {
continue;
};
// Label only the tab's first row so each tab shows a single number.
pane_props.tab_number = (show_tab_number && row_idx == 0).then_some(tab_index + 1);
let view_mode = *TabSettings::as_ref(app).vertical_tabs_view_mode.value();
let row = match view_mode {
VerticalTabsViewMode::Compact => render_compact_pane_row(pane_props, app),
Expand Down Expand Up @@ -2537,10 +2547,14 @@ fn render_pane_row(props: PaneProps<'_>, app: &AppContext) -> Box<dyn Element> {
content_col.finish()
};

let content = Flex::row()
let mut content = Flex::row()
.with_main_axis_size(MainAxisSize::Max)
.with_cross_axis_alignment(icon_alignment)
.with_spacing(ICON_WITH_STATUS_GAP)
.with_spacing(ICON_WITH_STATUS_GAP);
if let Some(number) = props.tab_number {
content.add_child(render_tab_number_label(number, appearance));
}
let content = content
.with_child(icon)
.with_child(Shrinkable::new(1., text_content).finish())
.finish();
Expand Down Expand Up @@ -2895,6 +2909,7 @@ impl<'a> PaneProps<'a> {
rename_editor,
is_pane_being_renamed,
pane_rename_editor,
tab_number: None,
})
}

Expand Down Expand Up @@ -3515,6 +3530,16 @@ fn render_text_line(
.finish()
}

/// Renders the 1-based tab-number label shown before a vertical tab's icon when
/// `appearance.tabs.show_tab_numbers` is enabled. The surrounding row applies
/// `ICON_WITH_STATUS_GAP` spacing, so no extra margin is needed here.
fn render_tab_number_label(number: usize, appearance: &Appearance) -> Box<dyn Element> {
let theme = appearance.theme();
Text::new_inline(format!("{number}"), appearance.ui_font_family(), 12.)
.with_color(theme.sub_text_color(theme.background()).into())
.finish()
}

fn render_inline_tab_rename_editor(
rename_editor: &ViewHandle<EditorView>,
appearance: &Appearance,
Expand Down Expand Up @@ -3762,10 +3787,14 @@ fn render_summary_tab_item(
);
}

let content = Flex::row()
let mut content = Flex::row()
.with_main_axis_size(MainAxisSize::Max)
.with_cross_axis_alignment(CrossAxisAlignment::Start)
.with_spacing(ICON_WITH_STATUS_GAP)
.with_spacing(ICON_WITH_STATUS_GAP);
if let Some(number) = props.tab_number {
content.add_child(render_tab_number_label(number, appearance));
}
let content = content
.with_child(icon)
.with_child(Shrinkable::new(1., text_col.finish()).finish())
.finish();
Expand Down Expand Up @@ -6253,10 +6282,14 @@ fn render_compact_pane_row(props: PaneProps<'_>, app: &AppContext) -> Box<dyn El
text_col.add_child(subtitle);
}

let content = Flex::row()
let mut content = Flex::row()
.with_main_axis_size(MainAxisSize::Max)
.with_cross_axis_alignment(icon_alignment)
.with_spacing(ICON_WITH_STATUS_GAP)
.with_spacing(ICON_WITH_STATUS_GAP);
if let Some(number) = props.tab_number {
content.add_child(render_tab_number_label(number, appearance));
}
let content = content
.with_child(icon)
.with_child(Shrinkable::new(1., text_col.finish()).finish())
.finish();
Expand Down
74 changes: 74 additions & 0 deletions specs/GH4028/product.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Product Spec: Show tab numbers on tabs

**Issue:** [warpdotdev/warp#4028](https://github.com/warpdotdev/warp/issues/4028)
**Figma:** none provided

## Summary

Add an optional **"Show tab numbers"** setting that prefixes each tab with its 1‑based position number, so the existing `Cmd+1..9` tab‑switch shortcuts are discoverable at a glance. The number is presentation‑only and must never leak into copied titles or tab search.

## Problem

Warp already binds `Cmd+1..8` to "activate the Nth tab" and `Cmd+9` to "activate the last tab" (`WorkspaceAction::ActivateTabByNumber`). But nothing in the UI tells the user which number maps to which tab, so the only way to use the shortcut is to count tabs by hand. Users with many tabs (the issue author keeps ~5 Neovim tabs) cannot tell which `Cmd+N` jumps where, and fall back to the mouse or `Shift+Cmd+{`/`}`.

The issue requests exactly this:

> Can we add a feature to show the tab index, so that we can easily switch tabs?
> `1 Device:~/Documents | 2 Device:~/Documents/Projects | 3 Device:~`

## Goals

- A user-toggleable setting that shows each tab's 1‑based position number.
- Works for both **horizontal tabs** and **vertical tabs** (Warp's two tab layouts).
- The displayed number matches the `Cmd+N` shortcut that activates that tab.
- Off by default (opt‑in), so existing users see no change.
- Presentation‑only: copying a tab title, tab search, and the stored title are unaffected.

## Non-goals

- Re-binding or changing the `Cmd+1..9` shortcuts themselves (already exist).
- Showing numbers for tabs beyond what the shortcuts cover in any special way — every tab simply shows its true position.
- A separate badge/gutter visual style; this spec uses a simple inline number prefix. Visual refinement can be a follow-up.

## User experience

### Setting

- **Settings → Appearance → Tabs → "Show tab numbers"** — a toggle, default **off**.
- Equivalent TOML: `appearance.tabs.show_tab_numbers = true`.
- Searchable in settings by terms like "tab number", "index", "position", "shortcut".

### Behavior when enabled

Horizontal tabs render the number before the title:

```
[ 1 zsh ][ 2 vim ][ 3 logs ● ]
```

Vertical tabs render the number before each tab's icon on the tab's representative row:

```
1 ~/Documents
2 ~/Documents/Projects
3 ~
```

Toggling the setting updates open windows live (no restart).

## Behavior invariants (testable)

1. When `show_tab_numbers` is **off** (default), tabs render exactly as today — no number shown, in both layouts.
2. When **on**, the tab at zero-based index `i` displays the number `i + 1`.
3. The displayed number is consistent with the shortcut: pressing `Cmd+N` activates the tab showing `N` (for `N` in 1..=8; `Cmd+9` activates the last tab, which shows its own position number).
4. In **vertical tabs**, exactly one number is shown per tab (on its representative/first row), never one-per-pane duplicates, across Expanded, Compact, and Summary view modes.
5. Toggling the setting re-renders visible tabs without requiring a restart.
6. The number is presentation-only: the value returned by "Copy tab title", the stored tab title, and tab search text are identical whether the setting is on or off.
7. While a tab is being renamed, the number prefix is not shown (the user is editing the title).

## Edge cases

- **Many tabs (>9):** every tab still shows its true position number; only `Cmd+1..9` have shortcuts, which is unchanged.
- **Filtered/searched vertical tabs:** numbers reflect the true workspace index, so they may be non-contiguous when the list is filtered — this is intentional so the number keeps matching the `Cmd+N` shortcut.
- **Custom-titled tabs:** the number is prefixed before the custom title; the custom title itself is unchanged.
- **Single tab:** shows "1" when enabled.
Loading