workspace tab cycle commands + ctrl+pagedown/up (T-405 part 1)

Add workspace.tab.next / workspace.tab.previous commands that cycle the
Slots.workspace tab strip with wraparound (no-op under two tabs), bound
ctrl+pagedown / ctrl+pageup across every preset via defaultBindings. Single-
chord, so no global matcher needed. Activating a tab also focuses the
workspace slot.

Part 2 (vim gt/gT) is deferred — it needs the global multi-chord matcher
T-404 introduces. T-405 stays open for that follow-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-13 18:06:36 +02:00
co-authored by Claude Opus 4.8
parent 054eaf6cd4
commit 5a21ea949a
5 changed files with 95 additions and 0 deletions
@@ -1,6 +1,7 @@
import 'package:clide/builtin/default_layout/default_layout.dart';
import 'package:clide/extension/extension.dart';
import 'package:clide/kernel/kernel.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import '../../helpers/kernel_fixture.dart';
@@ -151,5 +152,45 @@ void main() {
// Sidebar auto-expanded.
expect(f.services.arrangement.isCollapsed(Slots.sidebar), isFalse);
});
test('workspace.tab.next/previous cycle the workspace tabs with wraparound (T-405)', () async {
f.services.extensions.register(DefaultLayoutExtension());
await f.services.extensions.activateAll();
final panels = f.services.panels;
for (final id in ['wt.a', 'wt.b', 'wt.c']) {
panels.contribute(TabContribution(id: id, slot: Slots.workspace, title: id, build: (_) => const SizedBox.shrink()));
}
panels.setTabOrder(Slots.workspace, ['wt.a', 'wt.b', 'wt.c']);
panels.activateTab(Slots.workspace, 'wt.a');
await f.services.commands.execute('workspace.tab.next');
expect(panels.activeTabIn(Slots.workspace), 'wt.b');
await f.services.commands.execute('workspace.tab.next');
expect(panels.activeTabIn(Slots.workspace), 'wt.c');
await f.services.commands.execute('workspace.tab.next'); // wrap forward
expect(panels.activeTabIn(Slots.workspace), 'wt.a');
await f.services.commands.execute('workspace.tab.previous'); // wrap backward
expect(panels.activeTabIn(Slots.workspace), 'wt.c');
});
test('ctrl+pagedown/up resolve to the workspace tab-cycle commands across presets (T-405)', () async {
f.services.extensions.register(DefaultLayoutExtension());
await f.services.extensions.activateAll();
final km = f.services.keymap.keymap;
expect((km?.resolve(KeyChord.parse('ctrl+pagedown'), const {}) as InvokeCommandIntent?)?.commandId, 'workspace.tab.next');
expect((km?.resolve(KeyChord.parse('ctrl+pageup'), const {}) as InvokeCommandIntent?)?.commandId, 'workspace.tab.previous');
});
test('workspace tab cycle is a no-op with fewer than two tabs (T-405)', () async {
f.services.extensions.register(DefaultLayoutExtension());
await f.services.extensions.activateAll();
final panels = f.services.panels;
panels.contribute(TabContribution(id: 'only', slot: Slots.workspace, title: 'only', build: (_) => const SizedBox.shrink()));
panels.activateTab(Slots.workspace, 'only');
final r = await f.services.commands.execute('workspace.tab.next');
expect(r.ok, isTrue);
expect(r.data['cycled'], isFalse);
expect(panels.activeTabIn(Slots.workspace), 'only');
});
});
}