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 { CommentViewPopover } from '@/components/task/comment-view-popover';
|
||||||
import { PanelHeaderBarSplit } from '@/components/task/panel-primitives';
|
import { PanelHeaderBarSplit } from '@/components/task/panel-primitives';
|
||||||
import { useDockviewStore } from '@/lib/state/dockview-store';
|
import { useDockviewStore } from '@/lib/state/dockview-store';
|
||||||
|
import { useCommandPanelOpen } from '@/lib/commands/command-registry';
|
||||||
|
|
||||||
type FileEditorContentProps = {
|
type FileEditorContentProps = {
|
||||||
path: string;
|
path: string;
|
||||||
@@ -153,6 +154,7 @@ export function CodeMirrorCodeEditor({
|
|||||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||||
const mousePositionRef = useRef<{ x: number; y: number }>({ x: 0, y: 0 });
|
const mousePositionRef = useRef<{ x: number; y: number }>({ x: 0, y: 0 });
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const { setOpen: setCommandPanelOpen } = useCommandPanelOpen();
|
||||||
|
|
||||||
// Comment store
|
// Comment store
|
||||||
const addComment = useDiffCommentsStore((state) => state.addComment);
|
const addComment = useDiffCommentsStore((state) => state.addComment);
|
||||||
@@ -396,6 +398,36 @@ export function CodeMirrorCodeEditor({
|
|||||||
return () => wrapper.removeEventListener('keydown', handleKeyDown, true);
|
return () => wrapper.removeEventListener('keydown', handleKeyDown, true);
|
||||||
}, [enableComments, sessionId, currentSelection, floatingButtonPos]);
|
}, [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
|
// Handle Cmd/Ctrl+S keyboard shortcut and Escape
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
|||||||
@@ -228,7 +228,7 @@ export function MonacoCodeEditor({
|
|||||||
|
|
||||||
// Handle editor mount
|
// Handle editor mount
|
||||||
const handleEditorDidMount: OnMount = useCallback(
|
const handleEditorDidMount: OnMount = useCallback(
|
||||||
(editor, _monaco) => {
|
(editor, monaco) => {
|
||||||
editorRef.current = editor;
|
editorRef.current = editor;
|
||||||
setEditorInstance(editor);
|
setEditorInstance(editor);
|
||||||
decorationsRef.current = editor.createDecorationsCollection([]);
|
decorationsRef.current = editor.createDecorationsCollection([]);
|
||||||
@@ -288,8 +288,20 @@ export function MonacoCodeEditor({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
disposablesRef.current.push(glyphDisposable);
|
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
|
// Cleanup disposables
|
||||||
@@ -476,21 +488,6 @@ export function MonacoCodeEditor({
|
|||||||
return () => wrapper.removeEventListener('keydown', handleKeyDown, true);
|
return () => wrapper.removeEventListener('keydown', handleKeyDown, true);
|
||||||
}, [enableComments, sessionId, currentSelection]);
|
}, [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
|
// Escape to close inline forms
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import { startProcess } from '@/lib/api';
|
|||||||
import { createUserShell } from '@/lib/api/domains/user-shell-api';
|
import { createUserShell } from '@/lib/api/domains/user-shell-api';
|
||||||
import { useRepositoryScripts } from '@/hooks/domains/workspace/use-repository-scripts';
|
import { useRepositoryScripts } from '@/hooks/domains/workspace/use-repository-scripts';
|
||||||
import { useLspFileOpener } from '@/hooks/use-lsp-file-opener';
|
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 { useSessionGitStatus } from '@/hooks/domains/session/use-session-git-status';
|
||||||
import { useSessionCommits } from '@/hooks/domains/session/use-session-commits';
|
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
|
// Connect LSP Go-to-Definition navigation to dockview file tabs
|
||||||
useLspFileOpener();
|
useLspFileOpener();
|
||||||
|
|
||||||
|
// Global editor keybinds (tab nav, terminal toggle)
|
||||||
|
useEditorKeybinds();
|
||||||
|
|
||||||
// Keep sessionIdRef in sync for use inside event handlers
|
// Keep sessionIdRef in sync for use inside event handlers
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
sessionIdRef.current = effectiveSessionId;
|
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