feat: add more file editor keybindings (#242)
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
Reference in New Issue
Block a user