add Zed-style application menu bar: File / View / Help (T-48)
A custom in-window menu bar in the hat (no native menu, D-7), built from the command registry so it stays in sync and satisfies D-6 parity. - Menu model + hybrid resolver (menu_model.dart): a curated File/View/Help tree where a MenuAutoFill node sweeps in unplaced view.* commands; titles + keybindings come from the registry/keymap; unregistered or enabledWhen-false items render disabled (greyed), never hidden. - Widgets: MenuBar row in the hat (chrome tokens), anchored MenuDropdown overlay (dropdown tokens), two-column MenuItemRow with inline keybinding. - Full keyboard: Alt+mnemonic opens (hook in _RootShell._onKey), arrows navigate, Enter activates, Esc closes, Left/Right switch menus. - Commands: file.openFolder / file.newWindow / file.closeWorkspace / help.about, registered by MenuBarExtension(services:). File logic lifted out of the project switcher into FileActions (one source of truth; the switcher now dispatches the commands). Ctrl+O / Ctrl+Shift+N are now real keybindings in default.yaml. - Help → About: version/commit/date/repo from build-info + the bundled dependency licenses parsed from assets/licenses.yaml. Edit/Selection menus are deferred to T-271/T-272 (need focused-surface command routing). Tests: resolver + controller + licenses parse (pure); menu-bar widget (open/close/execute/disabled/Esc/arrow/Enter/Left-Right); FileActions + Open dialog; app-level Alt+F, non-repo dialog, and closeWorkspace. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
/// Tests for FileActions + the typed-path Open dialog (T-48). The open/close
|
||||
/// paths drive real `git` via project.open, so they run as plain async tests
|
||||
/// (no fake-async).
|
||||
library;
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:clide/builtin/menubar/src/file_actions.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';
|
||||
|
||||
void main() {
|
||||
late KernelFixture f;
|
||||
|
||||
setUp(() async {
|
||||
f = await KernelFixture.create();
|
||||
// A landing tab so openPath's activateTab has a real target.
|
||||
f.services.panels.contribute(TabContribution(id: 'claude.primary', slot: Slots.workspace, title: 'Claude', build: (_) => const SizedBox()));
|
||||
});
|
||||
tearDown(() => f.dispose());
|
||||
|
||||
test('openPath opens a git repo and activates the landing tab', () async {
|
||||
final ok = await FileActions(f.services).openPath(Directory.current.path);
|
||||
expect(ok, isTrue);
|
||||
expect(f.services.project.isOpen, isTrue);
|
||||
expect(f.services.panels.activeTabIn(Slots.workspace), 'claude.primary');
|
||||
});
|
||||
|
||||
test('openPath returns false for a non-repo directory', () async {
|
||||
final tmp = await Directory.systemTemp.createTemp('clide-fa-');
|
||||
addTearDown(() => tmp.delete(recursive: true));
|
||||
expect(await FileActions(f.services).openPath(tmp.path), isFalse);
|
||||
});
|
||||
|
||||
test('closeWorkspace closes the active project', () async {
|
||||
final fa = FileActions(f.services);
|
||||
await fa.openPath(Directory.current.path);
|
||||
expect(f.services.project.isOpen, isTrue);
|
||||
fa.closeWorkspace();
|
||||
expect(f.services.project.isOpen, isFalse);
|
||||
});
|
||||
|
||||
Widget harness(Widget child) => Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: ClideKernel(
|
||||
services: f.services,
|
||||
child: ClideTheme(
|
||||
controller: f.services.theme,
|
||||
child: MediaQuery(
|
||||
data: const MediaQueryData(),
|
||||
child: Align(alignment: Alignment.topLeft, child: child),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
testWidgets('OpenFolderDialog submits the typed path via onOpen', (tester) async {
|
||||
String? opened;
|
||||
await tester.pumpWidget(harness(OpenFolderDialog(
|
||||
onOpen: (p) async => opened = p,
|
||||
onCancel: () {},
|
||||
)));
|
||||
await tester.enterText(find.byType(EditableText), '/some/repo');
|
||||
await tester.tap(find.text('Open'));
|
||||
await tester.pump();
|
||||
expect(opened, '/some/repo');
|
||||
});
|
||||
|
||||
testWidgets('OpenFolderDialog surfaces an error when onOpen throws', (tester) async {
|
||||
await tester.pumpWidget(harness(OpenFolderDialog(
|
||||
onOpen: (_) async => throw StateError('not a repo'),
|
||||
onCancel: () {},
|
||||
)));
|
||||
await tester.enterText(find.byType(EditableText), '/bad');
|
||||
await tester.tap(find.text('Open'));
|
||||
await tester.pump();
|
||||
expect(find.text('Not a git repository'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user