feat: add more file editor keybindings (#242)

This commit is contained in:
Carlos Florêncio
2026-02-17 22:12:30 +00:00
committed by GitHub
parent f49158aeb2
commit 77cac23395
4 changed files with 157 additions and 17 deletions
@@ -20,6 +20,7 @@ import { EditorCommentPopover } from '@/components/task/editor-comment-popover';
import { CommentViewPopover } from '@/components/task/comment-view-popover';
import { PanelHeaderBarSplit } from '@/components/task/panel-primitives';
import { useDockviewStore } from '@/lib/state/dockview-store';
import { useCommandPanelOpen } from '@/lib/commands/command-registry';
type FileEditorContentProps = {
path: string;
@@ -153,6 +154,7 @@ export function CodeMirrorCodeEditor({
const wrapperRef = useRef<HTMLDivElement>(null);
const mousePositionRef = useRef<{ x: number; y: number }>({ x: 0, y: 0 });
const { toast } = useToast();
const { setOpen: setCommandPanelOpen } = useCommandPanelOpen();
// Comment store
const addComment = useDiffCommentsStore((state) => state.addComment);
@@ -396,6 +398,36 @@ export function CodeMirrorCodeEditor({
return () => wrapper.removeEventListener('keydown', handleKeyDown, true);
}, [enableComments, sessionId, currentSelection, floatingButtonPos]);
// Cmd/Ctrl+K to open command panel — capture phase intercepts before CodeMirror
useEffect(() => {
const wrapper = wrapperRef.current;
if (!wrapper) return;
const handler = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
e.preventDefault();
e.stopPropagation();
setCommandPanelOpen(true);
}
};
wrapper.addEventListener('keydown', handler, true);
return () => wrapper.removeEventListener('keydown', handler, true);
}, [setCommandPanelOpen]);
// Alt+Z to toggle word wrap — capture phase
useEffect(() => {
const wrapper = wrapperRef.current;
if (!wrapper) return;
const handler = (e: KeyboardEvent) => {
if (e.altKey && !e.metaKey && !e.ctrlKey && e.code === 'KeyZ') {
e.preventDefault();
e.stopPropagation();
setWrapEnabled((prev) => !prev);
}
};
wrapper.addEventListener('keydown', handler, true);
return () => wrapper.removeEventListener('keydown', handler, true);
}, []);
// Handle Cmd/Ctrl+S keyboard shortcut and Escape
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
@@ -228,7 +228,7 @@ export function MonacoCodeEditor({
// Handle editor mount
const handleEditorDidMount: OnMount = useCallback(
(editor, _monaco) => {
(editor, monaco) => {
editorRef.current = editor;
setEditorInstance(editor);
decorationsRef.current = editor.createDecorationsCollection([]);
@@ -288,8 +288,20 @@ export function MonacoCodeEditor({
}
});
disposablesRef.current.push(glyphDisposable);
// Override Monaco's built-in Cmd+K chord to open command panel
editor.addCommand(
monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyK,
() => { setCommandPanelOpen(true); }
);
// Alt+Z to toggle word wrap
editor.addCommand(
monaco.KeyMod.Alt | monaco.KeyCode.KeyZ,
() => { setWrapEnabled((prev) => !prev); }
);
},
[path, enableComments, sessionId]
[path, enableComments, sessionId, setCommandPanelOpen, setWrapEnabled]
);
// Cleanup disposables
@@ -476,21 +488,6 @@ export function MonacoCodeEditor({
return () => wrapper.removeEventListener('keydown', handleKeyDown, true);
}, [enableComments, sessionId, currentSelection]);
// Cmd+K for command panel — capture phase prevents Monaco from swallowing the shortcut
useEffect(() => {
const wrapper = wrapperRef.current;
if (!wrapper) return;
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
e.preventDefault();
e.stopPropagation();
setCommandPanelOpen(true);
}
};
wrapper.addEventListener('keydown', handleKeyDown, true);
return () => wrapper.removeEventListener('keydown', handleKeyDown, true);
}, [setCommandPanelOpen]);
// Escape to close inline forms
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
@@ -43,6 +43,7 @@ import { startProcess } from '@/lib/api';
import { createUserShell } from '@/lib/api/domains/user-shell-api';
import { useRepositoryScripts } from '@/hooks/domains/workspace/use-repository-scripts';
import { useLspFileOpener } from '@/hooks/use-lsp-file-opener';
import { useEditorKeybinds } from '@/hooks/use-editor-keybinds';
import { useSessionGitStatus } from '@/hooks/domains/session/use-session-git-status';
import { useSessionCommits } from '@/hooks/domains/session/use-session-commits';
@@ -614,6 +615,9 @@ export const DockviewDesktopLayout = memo(function DockviewDesktopLayout({
// Connect LSP Go-to-Definition navigation to dockview file tabs
useLspFileOpener();
// Global editor keybinds (tab nav, terminal toggle)
useEditorKeybinds();
// Keep sessionIdRef in sync for use inside event handlers
useEffect(() => {
sessionIdRef.current = effectiveSessionId;
+107
View File
@@ -0,0 +1,107 @@
'use client';
import { useEffect, useRef } from 'react';
import { useDockviewStore } from '@/lib/state/dockview-store';
import { useAppStoreApi } from '@/components/state-provider';
import { createUserShell } from '@/lib/api/domains/user-shell-api';
import type { DockviewApi } from 'dockview-react';
function handleTabNavigation(e: KeyboardEvent, api: DockviewApi) {
const activePanel = api.activePanel;
if (!activePanel) return;
const panels = activePanel.group.panels;
if (panels.length <= 1) return;
const currentIndex = panels.findIndex((p) => p.id === activePanel.id);
if (currentIndex === -1) return;
e.preventDefault();
e.stopPropagation();
const direction = e.code === 'BracketLeft' ? -1 : 1;
const nextIndex = (currentIndex + direction + panels.length) % panels.length;
panels[nextIndex].api.setActive();
}
function handleTerminalToggle(
e: KeyboardEvent,
api: DockviewApi,
previousPanelIdRef: React.MutableRefObject<string | null>,
getSessionId: () => string | null,
) {
e.preventDefault();
e.stopPropagation();
const activePanel = api.activePanel;
const isTerminalFocused = activePanel?.id.startsWith('terminal-') ?? false;
if (isTerminalFocused) {
const prevId = previousPanelIdRef.current;
const target = prevId ? api.getPanel(prevId) : api.getPanel('chat');
if (target) target.api.setActive();
previousPanelIdRef.current = null;
return;
}
if (activePanel) {
previousPanelIdRef.current = activePanel.id;
}
const terminalPanel = api.panels.find((p) => p.id.startsWith('terminal-'));
if (terminalPanel) {
terminalPanel.api.setActive();
return;
}
const sessionId = getSessionId();
if (!sessionId) return;
createUserShell(sessionId)
.then((result) => {
useDockviewStore.getState().addTerminalPanel(result.terminalId);
})
.catch((err) => { console.warn('Failed to create terminal shell:', err); });
}
/**
* Global editor keybinds for dockview:
* - Cmd/Ctrl+Shift+[ / ] — navigate prev/next tab in active group
* - Ctrl+` — toggle terminal focus
*/
export function useEditorKeybinds() {
const previousPanelIdRef = useRef<string | null>(null);
const appStore = useAppStoreApi();
useEffect(() => {
const handler = (e: KeyboardEvent) => {
const api = useDockviewStore.getState().api;
if (!api) return;
const isTabNav =
(e.metaKey || e.ctrlKey) &&
e.shiftKey &&
(e.code === 'BracketLeft' || e.code === 'BracketRight');
if (isTabNav) {
handleTabNavigation(e, api);
return;
}
const isTerminalToggle =
e.ctrlKey && !e.metaKey && !e.shiftKey && e.code === 'Backquote';
if (isTerminalToggle) {
handleTerminalToggle(
e,
api,
previousPanelIdRef,
() => appStore.getState().tasks.activeSessionId,
);
}
};
window.addEventListener('keydown', handler);
return () => window.removeEventListener('keydown', handler);
}, [appStore]);
}