diff --git a/src-web/components/Settings/SettingsHotkeys.tsx b/src-web/components/Settings/SettingsHotkeys.tsx
index c1ef58d7..f5fc1280 100644
--- a/src-web/components/Settings/SettingsHotkeys.tsx
+++ b/src-web/components/Settings/SettingsHotkeys.tsx
@@ -1,7 +1,8 @@
import { patchModel, settingsAtom } from '@yaakapp-internal/models';
import classNames from 'classnames';
+import { fuzzyMatch } from 'fuzzbunny';
import { useAtomValue } from 'jotai';
-import { useCallback, useEffect, useState } from 'react';
+import { useCallback, useEffect, useMemo, useState } from 'react';
import {
defaultHotkeys,
formatHotkeyString,
@@ -19,11 +20,19 @@ import { Heading } from '../core/Heading';
import { HotkeyRaw } from '../core/Hotkey';
import { Icon } from '../core/Icon';
import { IconButton } from '../core/IconButton';
+import { PlainInput } from '../core/PlainInput';
import { HStack, VStack } from '../core/Stacks';
import { Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow } from '../core/Table';
const HOLD_KEYS = ['Shift', 'Control', 'Alt', 'Meta'];
-const LAYOUT_INSENSITIVE_KEYS = ['Equal', 'Minus', 'BracketLeft', 'BracketRight', 'Backquote'];
+const LAYOUT_INSENSITIVE_KEYS = [
+ 'Equal',
+ 'Minus',
+ 'BracketLeft',
+ 'BracketRight',
+ 'Backquote',
+ 'Space',
+];
/** Convert a KeyboardEvent to a hotkey string like "Meta+Shift+k" or "Control+Shift+k" */
function eventToHotkeyString(e: KeyboardEvent): string | null {
@@ -58,6 +67,19 @@ function eventToHotkeyString(e: KeyboardEvent): string | null {
export function SettingsHotkeys() {
const settings = useAtomValue(settingsAtom);
const hotkeys = useAtomValue(hotkeysAtom);
+ const [filter, setFilter] = useState('');
+
+ const filteredActions = useMemo(() => {
+ if (!filter.trim()) {
+ return hotkeyActions;
+ }
+ return hotkeyActions.filter((action) => {
+ const scope = getHotkeyScope(action).replace(/_/g, ' ');
+ const label = action.replace(/[_.]/g, ' ');
+ const searchText = `${scope} ${label}`;
+ return fuzzyMatch(searchText, filter) != null;
+ });
+ }, [filter]);
if (settings == null) {
return null;
@@ -71,6 +93,14 @@ export function SettingsHotkeys() {
Click the menu button to add, remove, or reset keyboard shortcuts.
+
@@ -80,8 +110,9 @@ export function SettingsHotkeys() {
-
- {hotkeyActions.map((action) => (
+ {/* key={filter} forces re-render on filter change to fix Safari table rendering bug */}
+
+ {filteredActions.map((action) => (
{
onBlur.current?.();
},
- keydown: (e) => {
+ keydown: (e, view) => {
+ // Check if the hotkey matches the editor.autocomplete action
+ if (eventMatchesHotkey(e, 'editor.autocomplete')) {
+ e.preventDefault();
+ startCompletion(view);
+ return true;
+ }
onKeyDown.current?.(e);
},
paste: (e, v) => {
diff --git a/src-web/components/core/Editor/extensions.ts b/src-web/components/core/Editor/extensions.ts
index a145e27d..2bf9b8b5 100644
--- a/src-web/components/core/Editor/extensions.ts
+++ b/src-web/components/core/Editor/extensions.ts
@@ -184,6 +184,16 @@ export function getLanguageExtension({
});
}
+// Filter out autocomplete start triggers from completionKeymap since we handle it via configurable hotkeys.
+// Keep navigation keys (ArrowUp/Down, Enter, Escape, etc.) but remove startCompletion bindings.
+const filteredCompletionKeymap = completionKeymap.filter((binding) => {
+ const key = binding.key?.toLowerCase() ?? '';
+ const mac = (binding as { mac?: string }).mac?.toLowerCase() ?? '';
+ // Filter out Ctrl-Space and Mac-specific autocomplete triggers (Alt-`, Alt-i)
+ const isStartTrigger = key.includes('space') || mac.includes('alt-') || mac.includes('`');
+ return !isStartTrigger;
+});
+
export const baseExtensions = [
highlightSpecialChars(),
history(),
@@ -192,6 +202,7 @@ export const baseExtensions = [
autocompletion({
tooltipClass: () => 'x-theme-menu',
closeOnBlur: true, // Set to `false` for debugging in devtools without closing it
+ defaultKeymap: false, // We handle the trigger via configurable hotkeys
compareCompletions: (a, b) => {
// Don't sort completions at all, only on boost
return (a.boost ?? 0) - (b.boost ?? 0);
@@ -199,7 +210,7 @@ export const baseExtensions = [
}),
syntaxHighlighting(syntaxHighlightStyle),
syntaxTheme,
- keymap.of([...historyKeymap, ...completionKeymap]),
+ keymap.of([...historyKeymap, ...filteredCompletionKeymap]),
];
export const readonlyExtensions = [
diff --git a/src-web/hooks/useHotKey.ts b/src-web/hooks/useHotKey.ts
index 44f265e7..40e9985c 100644
--- a/src-web/hooks/useHotKey.ts
+++ b/src-web/hooks/useHotKey.ts
@@ -14,6 +14,7 @@ export type HotkeyAction =
| 'app.zoom_out'
| 'app.zoom_reset'
| 'command_palette.toggle'
+ | 'editor.autocomplete'
| 'environment_editor.toggle'
| 'hotkeys.showHelp'
| 'model.create'
@@ -41,6 +42,7 @@ const defaultHotkeysMac: Record = {
'app.zoom_out': ['Meta+Minus'],
'app.zoom_reset': ['Meta+0'],
'command_palette.toggle': ['Meta+k'],
+ 'editor.autocomplete': ['Control+Space'],
'environment_editor.toggle': ['Meta+Shift+e'],
'request.rename': ['Control+Shift+r'],
'request.send': ['Meta+Enter', 'Meta+r'],
@@ -69,6 +71,7 @@ const defaultHotkeysOther: Record = {
'app.zoom_out': ['Control+Minus'],
'app.zoom_reset': ['Control+0'],
'command_palette.toggle': ['Control+k'],
+ 'editor.autocomplete': ['Control+Space'],
'environment_editor.toggle': ['Control+Shift+e'],
'request.rename': ['F2'],
'request.send': ['Control+Enter', 'Control+r'],
@@ -122,6 +125,7 @@ const hotkeyLabels: Record = {
'app.zoom_out': 'Zoom Out',
'app.zoom_reset': 'Zoom to Actual Size',
'command_palette.toggle': 'Toggle Command Palette',
+ 'editor.autocomplete': 'Trigger Autocomplete',
'environment_editor.toggle': 'Edit Environments',
'hotkeys.showHelp': 'Show Keyboard Shortcuts',
'model.create': 'New Request',
@@ -144,7 +148,14 @@ const hotkeyLabels: Record = {
'workspace_settings.show': 'Open Workspace Settings',
};
-const layoutInsensitiveKeys = ['Equal', 'Minus', 'BracketLeft', 'BracketRight', 'Backquote'];
+const layoutInsensitiveKeys = [
+ 'Equal',
+ 'Minus',
+ 'BracketLeft',
+ 'BracketRight',
+ 'Backquote',
+ 'Space',
+];
export const hotkeyActions: HotkeyAction[] = (
Object.keys(defaultHotkeys) as (keyof typeof defaultHotkeys)[]
@@ -265,29 +276,20 @@ function handleKeyDown(e: KeyboardEvent) {
}
const executed: string[] = [];
- const hotkeys = getHotkeys();
- outer: for (const { action, callback, options } of jotaiStore.get(sortedCallbacksAtom)) {
- for (const [hkAction, hkKeys] of Object.entries(hotkeys) as [HotkeyAction, string[]][]) {
- if (hkAction !== action) {
- continue;
- }
- const enable = typeof options.enable === 'function' ? options.enable() : options.enable;
- if (enable === false) {
- continue;
- }
+ for (const { action, callback, options } of jotaiStore.get(sortedCallbacksAtom)) {
+ const enable = typeof options.enable === 'function' ? options.enable() : options.enable;
+ if (enable === false) {
+ continue;
+ }
- for (const hkKey of hkKeys) {
- const keys = hkKey.split('+');
- if (compareKeys(keys, Array.from(currentKeysWithModifiers))) {
- if (!options.allowDefault) {
- e.preventDefault();
- e.stopPropagation();
- }
- callback(e);
- executed.push(`${action} ${options.priority ?? 0}`);
- break outer;
- }
+ if (keysMatchAction(Array.from(currentKeysWithModifiers), action)) {
+ if (!options.allowDefault) {
+ e.preventDefault();
+ e.stopPropagation();
}
+ callback(e);
+ executed.push(`${action} ${options.priority ?? 0}`);
+ break;
}
}
@@ -336,12 +338,16 @@ export function formatHotkeyString(trigger: string): string[] {
labelParts.push('+');
} else if (p === 'Equal') {
labelParts.push('=');
+ } else if (p === 'Space') {
+ labelParts.push('Space');
} else {
labelParts.push(capitalize(p));
}
} else {
if (p === 'Control') {
labelParts.push('Ctrl');
+ } else if (p === 'Space') {
+ labelParts.push('Space');
} else {
labelParts.push(capitalize(p));
}
@@ -376,3 +382,39 @@ function compareKeys(keysA: string[], keysB: string[]) {
.join('::');
return sortedA === sortedB;
}
+
+/** Build the full key combination from a KeyboardEvent including modifiers */
+function getKeysFromEvent(e: KeyboardEvent): string[] {
+ const keys: string[] = [];
+ if (e.altKey) keys.push('Alt');
+ if (e.ctrlKey) keys.push('Control');
+ if (e.metaKey) keys.push('Meta');
+ if (e.shiftKey) keys.push('Shift');
+
+ // Add the actual key (use code for layout-insensitive keys)
+ const keyToAdd = layoutInsensitiveKeys.includes(e.code) ? e.code : e.key;
+ keys.push(keyToAdd);
+
+ return keys;
+}
+
+/** Check if a set of pressed keys matches any hotkey for the given action */
+function keysMatchAction(keys: string[], action: HotkeyAction): boolean {
+ const hotkeys = getHotkeys();
+ const hkKeys = hotkeys[action];
+ if (!hkKeys || hkKeys.length === 0) return false;
+
+ for (const hkKey of hkKeys) {
+ const hotkeyParts = hkKey.split('+');
+ if (compareKeys(hotkeyParts, keys)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+/** Check if a KeyboardEvent matches a hotkey action */
+export function eventMatchesHotkey(e: KeyboardEvent, action: HotkeyAction): boolean {
+ const keys = getKeysFromEvent(e);
+ return keysMatchAction(keys, action);
+}