Skip to content
Merged
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
9 changes: 9 additions & 0 deletions lib/layout/controllers/app_layout_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,15 @@ class AppLayoutController(
_platController.focus(AppTabs.releaseNotes);
}

/// The tab on screen in the editor area, or null with none open. What
/// "the active editor" means to File ▸ Close Editor and Revert File.
LeafSnapshot? activeCenterLeaf() {
final group = _platController.snapshot('center_pane');
if (group is! TabGroupSnapshot) return null;
final active = group.activeTab;
return active?.focusedLeaf ?? active?.firstLeaf;
}

/// Whether the settings tab is open *and* the one on screen — the activity
/// bar's settings button lights up on exactly that.
bool isSettingsTabSelected() {
Expand Down
8 changes: 4 additions & 4 deletions lib/shell/menus/file/actions/closed.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:multiview_desktop/multiview_desktop.dart';
import 'package:pinbench_ui/strings.dart';

import '../../../../features/editor/providers/editor_provider.dart';
import '../../../../features/workspace/providers/workspace_files_provider.dart';
import '../../../../layout/controllers/app_layout_controller.dart';
import '../../shared_menu_actions.dart';

PlatformMenuItemGroup closeActions({
Expand All @@ -20,9 +20,9 @@ PlatformMenuItemGroup closeActions({
onSelected: !hasEditorTab
? null
: () async {
final activeTab = ref.read(activeTabProvider);
if (activeTab.isEmpty || activeTab == 'Welcome') return;
await ref.read(workspaceFilesProvider.notifier).revertFile(activeTab);
final path = activeEditorFile(ref.read(appLayoutControllerProvider));
if (path == null) return;
await ref.read(workspaceFilesProvider.notifier).revertFile(path);
},
),
PlatformMenuItem(
Expand Down
29 changes: 22 additions & 7 deletions lib/shell/menus/shared_menu_actions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pinbench_ui/widgets/text_input_dialog.dart';
import 'package:pinbench_ui/strings.dart';

import '../../features/editor/providers/editor_provider.dart';
import '../../features/workspace/providers/workspace_files_provider.dart';
import '../../features/workspace/providers/editor_state_provider.dart';
import '../../core/chrome/chrome_commands.dart';
import '../../core/updates/update_providers.dart';
import '../../layout/controllers/app_layout_controller.dart';
import '../../layout/updates/update_dialog.dart';
Expand Down Expand Up @@ -61,16 +61,31 @@ Future<void> loadPrecompiledHexFile(WorkspaceFiles files) async {
/// Closes the active editor tab (and its canvas tab, if it's a `.cdl`).
/// Backs "Close Editor" on both menus. No-op when nothing (or the welcome
/// tab) is active.
///
/// Asks the layout which tab that is. It used to read `activeTabProvider`,
/// which switching tabs never updates — so it was usually empty, and ⌘W did
/// nothing.
void closeActiveEditorTab(WidgetRef ref) {
final activeTab = ref.read(activeTabProvider);
if (activeTab.isEmpty || activeTab == 'Welcome') return;
ref.read(appLayoutControllerProvider).closeTab(activeTab);
ref.read(editorStateControllerProvider).closeFile(activeTab);
if (activeTab.endsWith('.cdl')) {
ref.read(appLayoutControllerProvider).closeTab('canvas_$activeTab');
final layout = ref.read(appLayoutControllerProvider);
final leaf = layout.activeCenterLeaf();
if (leaf == null) return;
// A file tab carries its path; its editor state goes with it, and so does a
// `.cdl` file's canvas. Anything else — a canvas, the welcome screen,
// settings — is just a tab to close, as ⌘W closes any editor in VS Code.
if (activeEditorFile(layout) case final path?) {
ref.read(editorStateControllerProvider).closeFile(path);
if (path.endsWith('.cdl')) layout.closeTab(AppTabs.canvas(path));
}
layout.closeTab(leaf.id);
}

/// The file open in the active editor tab, or null when that tab is not a
/// file (a canvas, the welcome screen, settings) or there is none.
String? activeEditorFile(AppLayoutController layout) => switch (layout.activeCenterLeaf()?.data) {
final String path when path != 'canvas_view' => path,
_ => null,
};

/// Opens the update dialog, which starts a check as it appears. Backs "Check
/// for Updates…" on both menus.
///
Expand Down
92 changes: 92 additions & 0 deletions test/shell/close_editor_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';

import 'package:pinbench/core/chrome/chrome_commands.dart';
import 'package:pinbench/features/workspace/providers/editor_state_provider.dart';
import 'package:pinbench/layout/controllers/app_layout_controller.dart';
import 'package:pinbench/shell/menus/shared_menu_actions.dart';

/// File ▸ Close Editor (⌘W) closes the tab on screen. It used to read
/// `activeTabProvider`, which switching tabs never updates, so it was usually
/// empty and the shortcut did nothing.
void main() {
late WidgetRef ref;
late ProviderContainer container;

Future<AppLayoutController> pump(WidgetTester tester) async {
container = ProviderContainer();
addTearDown(container.dispose);
await tester.pumpWidget(
UncontrolledProviderScope(
container: container,
child: Consumer(
builder: (context, r, _) {
ref = r;
return const SizedBox();
},
),
),
);
return container.read(appLayoutControllerProvider);
}

testWidgets('closes the active file, and its editor state with it', (tester) async {
final layout = await pump(tester);
container.read(editorStateControllerProvider).openFile('/ws/a.ino', 'void setup() {}');
layout.openEditorTab('/ws/a.ino');

closeActiveEditorTab(ref);

expect(layout.platController.snapshot('/ws/a.ino'), isNull);
expect(container.read(editorStateControllerProvider).openFileControllers, isEmpty);
});

testWidgets('closes the tab that is on screen, not another', (tester) async {
final layout = await pump(tester);
final editors = container.read(editorStateControllerProvider)
..openFile('/ws/a.ino', '')
..openFile('/ws/b.ino', '');
layout
..openEditorTab('/ws/a.ino')
..openEditorTab('/ws/b.ino')
..focusTab('/ws/a.ino');

closeActiveEditorTab(ref);

expect(layout.platController.snapshot('/ws/a.ino'), isNull);
expect(layout.platController.snapshot('/ws/b.ino'), isNotNull);
expect(editors.openFileControllers.keys, ['/ws/b.ino']);
});

testWidgets("a circuit's canvas goes with its file", (tester) async {
final layout = await pump(tester);
container.read(editorStateControllerProvider).openFile('/ws/c.cdl', '');
layout
..openCanvasTab('/ws/c.cdl')
..openEditorTab('/ws/c.cdl');

closeActiveEditorTab(ref);

expect(layout.platController.snapshot('/ws/c.cdl'), isNull);
expect(layout.platController.snapshot(AppTabs.canvas('/ws/c.cdl')), isNull);
});

testWidgets('closes a tab that is not a file, as VS Code does', (tester) async {
final layout = await pump(tester);
layout.openSettingsTab();

closeActiveEditorTab(ref);

expect(layout.platController.snapshot(AppTabs.settings), isNull);
expect(layout.platController.snapshot(AppTabs.welcome), isNotNull);
});

testWidgets('does nothing with no tab open', (tester) async {
final layout = await pump(tester);
layout.closeTab(AppTabs.welcome);
expect(layout.activeCenterLeaf(), isNull);

closeActiveEditorTab(ref);
});
}
Loading