import 'package:clide/clide.dart'; import 'package:clide/extension/extension.dart'; import 'package:clide/kernel/kernel.dart'; class DefaultLayoutExtension extends ClideExtension { @override String get id => 'builtin.default-layout'; @override String get title => 'Default layout'; @override String get version => '0.2.0'; LayoutPresetContribution? _preset; ClideExtensionContext? _ctx; @override List get contributions => [ _preset ?? classicPreset(), CommandContribution(id: 'layout.reset', command: 'layout.reset', title: 'Layout: Reset to Classic', run: _reset), CommandContribution(id: 'palette.toggle', command: 'palette.toggle', title: 'Command Palette', defaultBinding: 'ctrl+shift+p', run: _togglePalette), // Collapse toggles (D-051, D-054) CommandContribution( id: 'sidebar.collapse', command: 'sidebar.collapse', title: 'Toggle Sidebar Collapse', defaultBinding: 'ctrl+shift+1', run: _collapseSidebar, ), CommandContribution( id: 'context.collapse', command: 'context.collapse', title: 'Toggle Context Panel Collapse', defaultBinding: 'ctrl+shift+3', run: _collapseContext, ), // Panel focus (D-054) CommandContribution(id: 'panel.focus.left', command: 'panel.focus.left', title: 'Focus Left Panel', defaultBinding: 'ctrl+1', run: _focusLeft), CommandContribution(id: 'panel.focus.middle', command: 'panel.focus.middle', title: 'Focus Middle Panel', defaultBinding: 'ctrl+2', run: _focusMiddle), CommandContribution(id: 'panel.focus.right', command: 'panel.focus.right', title: 'Focus Right Panel', defaultBinding: 'ctrl+3', run: _focusRight), // Focus mode (D-052, D-054) CommandContribution(id: 'panel.focusMode', command: 'panel.focusMode', title: 'Toggle Focus Mode', defaultBinding: 'ctrl+.', run: _toggleFocusMode), CommandContribution( id: 'panel.focusMode.exit', command: 'panel.focusMode.exit', title: 'Exit Focus Mode', defaultBinding: 'escape', // Stand down in Vim insert/visual mode so Esc returns to normal // mode instead of closing the editor (T-257). Symmetric with // vim.yaml's `vim.mode.normal` (escape when vim.insert||vim.visual). bindingWhen: '!vim.insert && !vim.visual', run: _exitFocusMode, ), // Editor split (D-049, D-054) CommandContribution(id: 'editor.open', command: 'editor.open', title: 'Open Editor', defaultBinding: 'ctrl+e', run: _openEditor), CommandContribution(id: 'editor.close', command: 'editor.close', title: 'Close Editor', defaultBinding: 'ctrl+w', run: _closeEditor), // Workspace tab cycling (T-405). Preset-neutral ctrl+pagedown/up across every // preset; the vim preset additionally binds gt/gT to these (T-405 part 2, // once a global multi-chord matcher lands — see T-404). CommandContribution( id: 'workspace.tab.next', command: 'workspace.tab.next', title: 'Next Workspace Tab', defaultBinding: 'ctrl+pagedown', run: _nextWorkspaceTab, ), CommandContribution( id: 'workspace.tab.previous', command: 'workspace.tab.previous', title: 'Previous Workspace Tab', defaultBinding: 'ctrl+pageup', run: _prevWorkspaceTab, ), // Sidebar section switching (D-054): alt+1 through alt+5 for (var i = 0; i < 5; i++) CommandContribution( id: 'sidebar.section.${i + 1}', command: 'sidebar.section.${i + 1}', title: 'Sidebar: Section ${i + 1}', defaultBinding: 'alt+${i + 1}', run: (args) => _switchSidebarSection(i), ), ]; @override Future activate(ClideExtensionContext ctx) async { _ctx = ctx; _preset = classicPreset(); ctx.arrangement.registerSlotsInto(ctx.panels, _preset!); ctx.arrangement.applyPreset(_preset!); _restoreLayout(ctx); ctx.arrangement.addListener(() => _persistLayout(ctx)); ctx.panels.addListener(() => _persistActiveTabs(ctx)); } void _restoreLayout(ClideExtensionContext ctx) { final s = ctx.settings; final sidebarOrder = s.get('project.layout.sidebar.order'); if (sidebarOrder != null) { ctx.panels.setTabOrder(Slots.sidebar, sidebarOrder.cast()); } final contextOrder = s.get('project.layout.context.order'); if (contextOrder != null) { ctx.panels.setTabOrder(Slots.contextPanel, contextOrder.cast()); } final sidebarCollapsed = s.get(_kSidebarCollapsed); if (sidebarCollapsed != null) { ctx.arrangement.setCollapsed(Slots.sidebar, sidebarCollapsed); } final contextCollapsed = s.get(_kContextCollapsed); if (contextCollapsed != null) { ctx.arrangement.setCollapsed(Slots.contextPanel, contextCollapsed); } final sidebarSize = s.get(_kSidebarSize); if (sidebarSize != null) ctx.arrangement.setSize(Slots.sidebar, sidebarSize); final contextSize = s.get(_kContextSize); if (contextSize != null) ctx.arrangement.setSize(Slots.contextPanel, contextSize); final dockVisible = s.get(_kDockVisible); if (dockVisible != null) ctx.arrangement.setVisible(Slots.dock, dockVisible); final dockSize = s.get(_kDockSize); if (dockSize != null) ctx.arrangement.setSize(Slots.dock, dockSize); final editorRatio = s.get(_kEditorRatio); if (editorRatio != null) ctx.arrangement.setEditorRatio(editorRatio); final activeLeft = s.get(_kActiveLeft); if (activeLeft != null) ctx.panels.activateTab(Slots.sidebar, activeLeft); final activeRight = s.get(_kActiveRight); if (activeRight != null) ctx.panels.activateTab(Slots.contextPanel, activeRight); } void _persistLayout(ClideExtensionContext ctx) { if (ctx.settings.projectDir == null) return; final s = ctx.settings; final a = ctx.arrangement; s.set(_kSidebarCollapsed, a.isCollapsed(Slots.sidebar)); s.set(_kContextCollapsed, a.isCollapsed(Slots.contextPanel)); s.set(_kSidebarSize, a.sizeOf(Slots.sidebar)); s.set(_kContextSize, a.sizeOf(Slots.contextPanel)); s.set(_kEditorRatio, a.editorRatio); s.set(_kDockVisible, a.isVisible(Slots.dock)); s.set(_kDockSize, a.sizeOf(Slots.dock)); } void _persistActiveTabs(ClideExtensionContext ctx) { if (ctx.settings.projectDir == null) return; final s = ctx.settings; final left = ctx.panels.activeTabIn(Slots.sidebar); if (left != null) s.set(_kActiveLeft, left); final right = ctx.panels.activeTabIn(Slots.contextPanel); if (right != null) s.set(_kActiveRight, right); } static const _kSidebarCollapsed = 'project.layout.sidebar.collapsed'; static const _kContextCollapsed = 'project.layout.context.collapsed'; static const _kSidebarSize = 'project.layout.sidebar.size'; static const _kContextSize = 'project.layout.context.size'; static const _kEditorRatio = 'project.layout.editor.ratio'; static const _kActiveLeft = 'project.layout.sidebar.activeTab'; static const _kActiveRight = 'project.layout.context.activeTab'; static const _kDockVisible = 'project.layout.dock.visible'; static const _kDockSize = 'project.layout.dock.size'; Future _reset(List args) async { final preset = _preset; final ctx = _ctx; if (preset == null || ctx == null) return _notActivated(); ctx.arrangement.applyPreset(preset); return IpcResponse.ok(id: '', data: {'preset': preset.id}); } Future _togglePalette(List args) async { final ctx = _ctx; if (ctx == null) return _notActivated(); ctx.palette.toggle(); return IpcResponse.ok(id: '', data: {'open': ctx.palette.isOpen}); } Future _collapseSidebar(List args) async { final ctx = _ctx; if (ctx == null) return _notActivated(); ctx.arrangement.toggleCollapsed(Slots.sidebar); final collapsed = ctx.arrangement.isCollapsed(Slots.sidebar); return IpcResponse.ok(id: '', data: {'collapsed': collapsed}); } Future _collapseContext(List args) async { final ctx = _ctx; if (ctx == null) return _notActivated(); ctx.arrangement.toggleCollapsed(Slots.contextPanel); final collapsed = ctx.arrangement.isCollapsed(Slots.contextPanel); return IpcResponse.ok(id: '', data: {'collapsed': collapsed}); } Future _focusLeft(List args) async { final ctx = _ctx; if (ctx == null) return _notActivated(); if (ctx.arrangement.isCollapsed(Slots.sidebar)) { ctx.arrangement.setCollapsed(Slots.sidebar, false); } final active = ctx.panels.activeTabIn(Slots.sidebar); if (active != null) { ctx.focus.setActive(slot: Slots.sidebar, contributionId: active); } return IpcResponse.ok(id: '', data: {'focused': 'sidebar'}); } Future _focusMiddle(List args) async { final ctx = _ctx; if (ctx == null) return _notActivated(); final active = ctx.panels.activeTabIn(Slots.workspace); if (active != null) { ctx.focus.setActive(slot: Slots.workspace, contributionId: active); } return IpcResponse.ok(id: '', data: {'focused': 'workspace'}); } Future _nextWorkspaceTab(List args) => _cycleWorkspaceTab(forward: true); Future _prevWorkspaceTab(List args) => _cycleWorkspaceTab(forward: false); /// Cycle the workspace tab strip with wraparound (T-405). A no-op when there /// are fewer than two tabs. Activating a tab also focuses the workspace slot /// so the newly-shown pane takes keyboard focus. Future _cycleWorkspaceTab({required bool forward}) async { final ctx = _ctx; if (ctx == null) return _notActivated(); final tabs = ctx.panels.tabsFor(Slots.workspace); if (tabs.length < 2) return IpcResponse.ok(id: '', data: const {'cycled': false}); final active = ctx.panels.activeTabIn(Slots.workspace); final cur = tabs.indexWhere((t) => t.id == active); final start = cur < 0 ? 0 : cur; final next = (start + (forward ? 1 : -1) + tabs.length) % tabs.length; final nextId = tabs[next].id; ctx.panels.activateTab(Slots.workspace, nextId); ctx.focus.setActive(slot: Slots.workspace, contributionId: nextId); return IpcResponse.ok(id: '', data: {'active': nextId}); } Future _focusRight(List args) async { final ctx = _ctx; if (ctx == null) return _notActivated(); if (ctx.arrangement.isCollapsed(Slots.contextPanel)) { ctx.arrangement.setCollapsed(Slots.contextPanel, false); } final active = ctx.panels.activeTabIn(Slots.contextPanel); if (active != null) { ctx.focus.setActive(slot: Slots.contextPanel, contributionId: active); } return IpcResponse.ok(id: '', data: {'focused': 'context'}); } Future _toggleFocusMode(List args) async { final ctx = _ctx; if (ctx == null) return _notActivated(); final activeSlot = ctx.focus.activeSlot ?? Slots.workspace; ctx.arrangement.toggleFocusMode(activeSlot); return IpcResponse.ok(id: '', data: {'focusMode': ctx.arrangement.isInFocusMode}); } Future _exitFocusMode(List args) async { final ctx = _ctx; if (ctx == null) return _notActivated(); if (ctx.arrangement.isInFocusMode) { ctx.arrangement.exitFocusMode(); return IpcResponse.ok(id: '', data: {'focusMode': false}); } if (ctx.arrangement.editorOpen) { ctx.arrangement.closeEditor(); return IpcResponse.ok(id: '', data: {'editorOpen': false}); } if (ctx.palette.isOpen) { ctx.palette.toggle(); return IpcResponse.ok(id: '', data: {'palette': false}); } return IpcResponse.ok(id: '', data: {}); } Future _openEditor(List args) async { final ctx = _ctx; if (ctx == null) return _notActivated(); ctx.arrangement.openEditor(); return IpcResponse.ok(id: '', data: {'editorOpen': true}); } Future _closeEditor(List args) async { final ctx = _ctx; if (ctx == null) return _notActivated(); if (ctx.arrangement.editorOpen) { ctx.arrangement.closeEditor(); return IpcResponse.ok(id: '', data: {'editorOpen': false}); } return IpcResponse.ok(id: '', data: {}); } Future _switchSidebarSection(int index) async { final ctx = _ctx; if (ctx == null) return _notActivated(); if (ctx.arrangement.isCollapsed(Slots.sidebar)) { ctx.arrangement.setCollapsed(Slots.sidebar, false); } final tabs = ctx.panels.tabsFor(Slots.sidebar); if (index < tabs.length) { ctx.panels.activateTab(Slots.sidebar, tabs[index].id); return IpcResponse.ok(id: '', data: {'section': tabs[index].id}); } return IpcResponse.ok(id: '', data: {}); } static IpcResponse _notActivated() => IpcResponse.err( id: '', error: IpcError(code: IpcExitCode.toolError, kind: IpcErrorKind.toolError, message: 'not activated'), ); }