Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ and this project adheres to

### Fixed

- Cmd/Ctrl+E now closes the IDE while the code editor has focus. Monaco claimed
the combination for "Use Selection for Find", so the shortcut that opened the
IDE could not close it again. Cmd/Ctrl+F still opens Monaco's find widget.
[#4959](https://github.com/OpenFn/lightning/issues/4959)
- The global assistant no longer offers to paste a reply's code block into
whichever job you have open. It applies its own changes and shows them as
diffs, so those blocks are data it quoted back or work it has already done.
Expand Down
22 changes: 17 additions & 5 deletions assets/js/collaborative-editor/components/ide/FullScreenIDE.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -701,8 +701,12 @@ export function FullScreenIDE({
});
}, [onCredentialSaved, currentJob, updateJob, requestCredentials]);

const isShortcutEnabled =
!isConfigureModalOpen && !isAdaptorPickerOpen && !isCredentialModalOpen;

// Escape steps out of the editor before it closes the IDE.
useKeyboardShortcut(
'Escape, Control+e, Meta+e',
'Escape',
() => {
const activeElement = document.activeElement;
const isMonacoFocused = activeElement?.closest('.monaco-editor');
Expand All @@ -714,10 +718,18 @@ export function FullScreenIDE({
}
},
50, // IDE priority
{
enabled:
!isConfigureModalOpen && !isAdaptorPickerOpen && !isCredentialModalOpen,
}
{ enabled: isShortcutEnabled }
);

// Mod+E always closes the IDE, including from inside Monaco, so that it
// mirrors the Mod+E that opened it.
useKeyboardShortcut(
'Control+e, Meta+e',
() => {
onClose();
},
50, // IDE priority
{ enabled: isShortcutEnabled }
);

// Save docs panel collapsed state to localStorage
Expand Down
16 changes: 16 additions & 0 deletions assets/js/monaco/keyboard-overrides.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type { Monaco } from './index';
* - Cmd/Ctrl+Enter: Dispatches to window for run/retry actions
* - Cmd/Ctrl+Shift+Enter: Dispatches to window for force-run actions
* - Cmd/Ctrl+K: Dispatches to window for AI chat shortcut
* - Cmd/Ctrl+E: Dispatches to window for the IDE open/close shortcut
*
* Usage:
* ```typescript
Expand Down Expand Up @@ -77,4 +78,19 @@ export function addKeyboardShortcutOverrides(
});
window.dispatchEvent(event);
});

// Override Monaco's Cmd/Ctrl+E ("Use Selection for Find") so the IDE toggle
// still works while the editor has focus. Cmd/Ctrl+F is left alone, so the
// find widget remains reachable.
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyE, () => {
const event = new KeyboardEvent('keydown', {
key: 'e',
code: 'KeyE',
metaKey: isMac,
ctrlKey: !isMac,
bubbles: true,
cancelable: true,
});
window.dispatchEvent(event);
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*
* Tests keyboard shortcuts for the FullScreenIDE component:
* - Escape: Smart behavior (blur Monaco first, then close IDE)
* - Mod+E: Close the IDE, including while Monaco has focus
* - Mod+Enter: Run or retry (prioritizes retry when available)
* - Mod+Shift+Enter: Force new run (ignores retry)
*
Expand Down Expand Up @@ -595,6 +596,57 @@ describe('FullScreenIDE Keyboard Shortcuts', () => {
});
});

describe('Mod+E - Close IDE', () => {
test('closes IDE when Monaco is not focused (Mac)', async () => {
const user = userEvent.setup();
setupMockUseRunRetry();
const onClose = vi.fn();
renderFullScreenIDE({ onClose });

await waitFor(() =>
expect(screen.getByTestId('collaborative-monaco')).toBeInTheDocument()
);

await user.keyboard('{Meta>}e{/Meta}');

await waitFor(() => expect(onClose).toHaveBeenCalled());
});

test('closes IDE while Monaco has focus (Mac)', async () => {
const user = userEvent.setup();
setupMockUseRunRetry();
const onClose = vi.fn();
renderFullScreenIDE({ onClose });

await waitFor(() =>
expect(screen.getByTestId('collaborative-monaco')).toBeInTheDocument()
);

focusElement(screen.getByTestId('monaco-contenteditable'));

await user.keyboard('{Meta>}e{/Meta}');

await waitFor(() => expect(onClose).toHaveBeenCalled());
});

test('closes IDE while Monaco has focus (Windows)', async () => {
const user = userEvent.setup();
setupMockUseRunRetry();
const onClose = vi.fn();
renderFullScreenIDE({ onClose });

await waitFor(() =>
expect(screen.getByTestId('collaborative-monaco')).toBeInTheDocument()
);

focusElement(screen.getByTestId('monaco-contenteditable'));

await user.keyboard('{Control>}e{/Control}');

await waitFor(() => expect(onClose).toHaveBeenCalled());
});
});

describe('Mod+Enter - Run or Retry', () => {
// Note: Run shortcuts only work in create-run or run-viewer states
test('calls handleRun when in create-run state (Mac)', async () => {
Expand Down
103 changes: 103 additions & 0 deletions assets/test/monaco/keyboard-overrides.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/**
* Monaco keyboard override tests
*
* Monaco claims a handful of Cmd/Ctrl combos for its own commands. These
* overrides re-dispatch them on `window` so the application's keyboard system
* can handle them instead.
*/

import type { editor } from 'monaco-editor';
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';

import type { Monaco } from '../../js/monaco';
import { addKeyboardShortcutOverrides } from '../../js/monaco/keyboard-overrides';

// Arbitrary but distinct values; only their combination matters here.
const KeyMod = { CtrlCmd: 2048, Shift: 1024 } as const;
const KeyCode = { Enter: 3, KeyE: 35, KeyF: 36, KeyK: 41 } as const;

function setupEditor() {
const commands = new Map<number, () => void>();

const editorStub = {
addCommand: (keybinding: number, handler: () => void) => {
commands.set(keybinding, handler);
},
} as unknown as editor.IStandaloneCodeEditor;

const monacoStub = { KeyMod, KeyCode } as unknown as Monaco;

addKeyboardShortcutOverrides(editorStub, monacoStub);

return commands;
}

function setUserAgent(userAgent: string) {
Object.defineProperty(navigator, 'userAgent', {
value: userAgent,
configurable: true,
});
}

const originalUserAgent = navigator.userAgent;

describe('addKeyboardShortcutOverrides', () => {
let dispatched: KeyboardEvent[];
let listener: (event: Event) => void;

beforeEach(() => {
dispatched = [];
listener = event => {
dispatched.push(event as KeyboardEvent);
};
window.addEventListener('keydown', listener);
});

afterEach(() => {
window.removeEventListener('keydown', listener);
setUserAgent(originalUserAgent);
vi.restoreAllMocks();
});

test('registers an override for Mod+E', () => {
const commands = setupEditor();

expect(commands.has(KeyMod.CtrlCmd | KeyCode.KeyE)).toBe(true);
});

test('Mod+E dispatches a Cmd+E keydown on Mac', () => {
setUserAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)');
const commands = setupEditor();

commands.get(KeyMod.CtrlCmd | KeyCode.KeyE)?.();

expect(dispatched).toHaveLength(1);
expect(dispatched[0]).toMatchObject({
key: 'e',
code: 'KeyE',
metaKey: true,
ctrlKey: false,
});
});

test('Mod+E dispatches a Ctrl+E keydown off Mac', () => {
setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64)');
const commands = setupEditor();

commands.get(KeyMod.CtrlCmd | KeyCode.KeyE)?.();

expect(dispatched).toHaveLength(1);
expect(dispatched[0]).toMatchObject({
key: 'e',
code: 'KeyE',
metaKey: false,
ctrlKey: true,
});
});

test('leaves Mod+F to Monaco so the find widget still opens', () => {
const commands = setupEditor();

expect(commands.has(KeyMod.CtrlCmd | KeyCode.KeyF)).toBe(false);
});
});