fix(editor): collapse the workspace split when the last buffer closes (T-459)
EditorRegistry.close() guarded its active-changed emit on `_activeId != null`, so closing the LAST buffer (active clears to null) emitted only editor.closed — never the active-changed(id:null) the editor extension listens for to call closeEditor(). editorOpen stayed true and the top split sat orphaned over the Claude pane. Always emit active-changed when the active buffer is removed, including the cleared-to-null case; the slot renderer already collapses correctly once editorOpen flips false. The existing extension test fabricated the null active-changed event, so it passed despite the registry never emitting it — that gap is why the bug shipped. Add a registry test that drives the real close() path, plus a slot_host widget test asserting the split (drag handle) drops out and the primary pane fills the column. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -89,6 +89,23 @@ void main() {
|
||||
expect(sink.ofKind('editor.active-changed').length, greaterThanOrEqualTo(2));
|
||||
});
|
||||
|
||||
test('closing the last buffer clears active + emits active-changed with id=null (T-459)', () async {
|
||||
final buf = await reg.open('README.md');
|
||||
expect(reg.active, same(buf));
|
||||
sink.events.clear();
|
||||
reg.close(buf.id);
|
||||
// No buffer left — active clears to null.
|
||||
expect(reg.active, isNull);
|
||||
// The collapse signal: active-changed must still fire, carrying a null id,
|
||||
// so the editor split drops out. The bug (T-459) was this emit being
|
||||
// guarded away on the last close, leaving editorOpen stuck true.
|
||||
final activeChanged = sink.ofKind('editor.active-changed');
|
||||
expect(activeChanged, hasLength(1));
|
||||
expect(activeChanged.single.data['id'], isNull);
|
||||
expect(activeChanged.single.data['path'], isNull);
|
||||
expect(sink.ofKind('editor.closed'), hasLength(1));
|
||||
});
|
||||
|
||||
test('opening a non-existent path creates an empty buffer', () async {
|
||||
final buf = await reg.open('NEW.md');
|
||||
expect(buf.content, isEmpty);
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
/// T-459: the workspace editor top split must collapse cleanly when the
|
||||
/// editor closes — no orphaned drag handle or empty region left over the
|
||||
/// primary (Claude) pane.
|
||||
///
|
||||
/// This covers the render seam: given `arrangement.editorOpen`, the
|
||||
/// `_WorkspaceSlot` either shows the split (editor body + drag handle over the
|
||||
/// primary pane) or collapses to just the primary pane. The event seam that
|
||||
/// flips `editorOpen` on the last buffer close is covered by the registry +
|
||||
/// editor-extension tests.
|
||||
library;
|
||||
|
||||
import 'package:clide/extension/extension.dart';
|
||||
import 'package:clide/kernel/kernel.dart';
|
||||
import 'package:clide/src/shell/slot_host.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../../helpers/kernel_fixture.dart';
|
||||
import '../../helpers/widget_harness.dart';
|
||||
|
||||
void main() {
|
||||
late KernelFixture f;
|
||||
|
||||
setUp(() async {
|
||||
f = await KernelFixture.create();
|
||||
// Stand-in workspace tabs: the real Claude pane / EditorView pull in PTY +
|
||||
// tree-sitter FFI, which the split-collapse contract doesn't need.
|
||||
f.services.panels.contribute(
|
||||
TabContribution(
|
||||
id: 'claude.primary',
|
||||
slot: Slots.workspace,
|
||||
title: 'Claude',
|
||||
priority: 90,
|
||||
build: (_) => const SizedBox(key: ValueKey('claude-body')),
|
||||
),
|
||||
);
|
||||
f.services.panels.contribute(
|
||||
TabContribution(
|
||||
id: 'editor.active',
|
||||
slot: Slots.workspace,
|
||||
title: 'Editor',
|
||||
priority: 80,
|
||||
build: (_) => const SizedBox(key: ValueKey('editor-body')),
|
||||
),
|
||||
);
|
||||
});
|
||||
tearDown(() => f.dispose());
|
||||
|
||||
Finder dragHandle() => find.bySemanticsLabel('Editor split');
|
||||
|
||||
testWidgets('editor top split collapses to the primary pane when the editor closes (T-459)', (tester) async {
|
||||
// Editor open + active: the split shows the editor body, the drag handle,
|
||||
// and the primary pane below.
|
||||
f.services.panels.activateTab(Slots.workspace, 'editor.active');
|
||||
f.services.arrangement.openEditor();
|
||||
await tester.pumpWidget(harness(f, const SlotHost(slot: Slots.workspace)));
|
||||
await tester.pump();
|
||||
|
||||
expect(dragHandle(), findsOneWidget, reason: 'split should render its resize handle while the editor is open');
|
||||
expect(find.byKey(const ValueKey('editor-body')), findsOneWidget);
|
||||
expect(find.byKey(const ValueKey('claude-body')), findsOneWidget);
|
||||
|
||||
// Close the editor → the top split must drop out entirely, leaving only the
|
||||
// primary pane (no leftover drag handle / empty region).
|
||||
f.services.arrangement.closeEditor();
|
||||
await tester.pump();
|
||||
|
||||
expect(dragHandle(), findsNothing, reason: 'collapsed split must not leave a drag handle behind');
|
||||
expect(find.byKey(const ValueKey('editor-body')), findsNothing, reason: 'closed editor body must not linger');
|
||||
expect(find.byKey(const ValueKey('claude-body')), findsOneWidget, reason: 'primary pane fills the column');
|
||||
});
|
||||
|
||||
testWidgets('no split when the editor was never opened — primary pane only', (tester) async {
|
||||
f.services.panels.activateTab(Slots.workspace, 'claude.primary');
|
||||
await tester.pumpWidget(harness(f, const SlotHost(slot: Slots.workspace)));
|
||||
await tester.pump();
|
||||
|
||||
expect(dragHandle(), findsNothing);
|
||||
expect(find.byKey(const ValueKey('claude-body')), findsOneWidget);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user