mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-03 08:24:10 -07:00
undo (#18147)
This commit is contained in:
@@ -343,11 +343,11 @@ please see the dedicated [Custom Commands documentation](./custom-commands.md).
|
|||||||
These shortcuts apply directly to the input prompt for text manipulation.
|
These shortcuts apply directly to the input prompt for text manipulation.
|
||||||
|
|
||||||
- **Undo:**
|
- **Undo:**
|
||||||
- **Keyboard shortcut:** Press **Cmd+z** or **Alt+z** to undo the last action
|
- **Keyboard shortcut:** Press **Alt+z** or **Cmd+z** to undo the last action
|
||||||
in the input prompt.
|
in the input prompt.
|
||||||
|
|
||||||
- **Redo:**
|
- **Redo:**
|
||||||
- **Keyboard shortcut:** Press **Shift+Cmd+Z** or **Shift+Alt+Z** to redo the
|
- **Keyboard shortcut:** Press **Shift+Alt+Z** or **Shift+Cmd+Z** to redo the
|
||||||
last undone action in the input prompt.
|
last undone action in the input prompt.
|
||||||
|
|
||||||
## At commands (`@`)
|
## At commands (`@`)
|
||||||
|
|||||||
@@ -141,6 +141,7 @@ import { LoginWithGoogleRestartDialog } from './auth/LoginWithGoogleRestartDialo
|
|||||||
import { NewAgentsChoice } from './components/NewAgentsNotification.js';
|
import { NewAgentsChoice } from './components/NewAgentsNotification.js';
|
||||||
import { isSlashCommand } from './utils/commandUtils.js';
|
import { isSlashCommand } from './utils/commandUtils.js';
|
||||||
import { useTerminalTheme } from './hooks/useTerminalTheme.js';
|
import { useTerminalTheme } from './hooks/useTerminalTheme.js';
|
||||||
|
import { isITerm2 } from './utils/terminalUtils.js';
|
||||||
|
|
||||||
function isToolExecuting(pendingHistoryItems: HistoryItemWithoutId[]) {
|
function isToolExecuting(pendingHistoryItems: HistoryItemWithoutId[]) {
|
||||||
return pendingHistoryItems.some((item) => {
|
return pendingHistoryItems.some((item) => {
|
||||||
@@ -1472,7 +1473,10 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
|||||||
setShowErrorDetails((prev) => !prev);
|
setShowErrorDetails((prev) => !prev);
|
||||||
return true;
|
return true;
|
||||||
} else if (keyMatchers[Command.SUSPEND_APP](key)) {
|
} else if (keyMatchers[Command.SUSPEND_APP](key)) {
|
||||||
handleWarning('Undo has been moved to Cmd + Z or Alt/Opt + Z');
|
const undoMessage = isITerm2()
|
||||||
|
? 'Undo has been moved to Option + Z'
|
||||||
|
: 'Undo has been moved to Alt/Option + Z or Cmd + Z';
|
||||||
|
handleWarning(undoMessage);
|
||||||
return true;
|
return true;
|
||||||
} else if (keyMatchers[Command.SHOW_FULL_TODOS](key)) {
|
} else if (keyMatchers[Command.SHOW_FULL_TODOS](key)) {
|
||||||
setShowFullTodos((prev) => !prev);
|
setShowFullTodos((prev) => !prev);
|
||||||
|
|||||||
@@ -110,8 +110,8 @@ export const INFORMATIVE_TIPS = [
|
|||||||
'Delete from the cursor to the end of the line with Ctrl+K…',
|
'Delete from the cursor to the end of the line with Ctrl+K…',
|
||||||
'Clear the entire input prompt with a double-press of Esc…',
|
'Clear the entire input prompt with a double-press of Esc…',
|
||||||
'Paste from your clipboard with Ctrl+V…',
|
'Paste from your clipboard with Ctrl+V…',
|
||||||
'Undo text edits in the input with Cmd+Z or Alt+Z…',
|
'Undo text edits in the input with Alt+Z or Cmd+Z…',
|
||||||
'Redo undone text edits with Shift+Cmd+Z or Shift+Alt+Z…',
|
'Redo undone text edits with Shift+Alt+Z or Shift+Cmd+Z…',
|
||||||
'Open the current prompt in an external editor with Ctrl+X…',
|
'Open the current prompt in an external editor with Ctrl+X…',
|
||||||
'In menus, move up/down with k/j or the arrow keys…',
|
'In menus, move up/down with k/j or the arrow keys…',
|
||||||
'In menus, select an item by typing its number…',
|
'In menus, select an item by typing its number…',
|
||||||
|
|||||||
@@ -821,65 +821,72 @@ describe('KeypressContext', () => {
|
|||||||
// Terminals to test
|
// Terminals to test
|
||||||
const terminals = ['iTerm2', 'Ghostty', 'MacTerminal', 'VSCodeTerminal'];
|
const terminals = ['iTerm2', 'Ghostty', 'MacTerminal', 'VSCodeTerminal'];
|
||||||
|
|
||||||
// Key mappings: letter -> [keycode, accented character]
|
// Key mappings: letter -> [keycode, accented character, shift]
|
||||||
const keys: Record<string, [number, string]> = {
|
const keys: Record<string, [number, string, boolean]> = {
|
||||||
b: [98, '\u222B'],
|
b: [98, '\u222B', false],
|
||||||
f: [102, '\u0192'],
|
f: [102, '\u0192', false],
|
||||||
m: [109, '\u00B5'],
|
m: [109, '\u00B5', false],
|
||||||
|
z: [122, '\u03A9', false],
|
||||||
|
Z: [122, '\u00B8', true],
|
||||||
};
|
};
|
||||||
|
|
||||||
it.each(
|
it.each(
|
||||||
terminals.flatMap((terminal) =>
|
terminals.flatMap((terminal) =>
|
||||||
Object.entries(keys).map(([key, [keycode, accentedChar]]) => {
|
Object.entries(keys).map(
|
||||||
if (terminal === 'Ghostty') {
|
([key, [keycode, accentedChar, shiftValue]]) => {
|
||||||
// Ghostty uses kitty protocol sequences
|
if (terminal === 'Ghostty') {
|
||||||
return {
|
// Ghostty uses kitty protocol sequences
|
||||||
terminal,
|
// Modifier 3 is Alt, 4 is Shift+Alt
|
||||||
key,
|
const modifier = shiftValue ? 4 : 3;
|
||||||
chunk: `\x1b[${keycode};3u`,
|
return {
|
||||||
expected: {
|
terminal,
|
||||||
name: key,
|
key,
|
||||||
shift: false,
|
chunk: `\x1b[${keycode};${modifier}u`,
|
||||||
alt: true,
|
expected: {
|
||||||
ctrl: false,
|
name: key.toLowerCase(),
|
||||||
cmd: false,
|
shift: shiftValue,
|
||||||
},
|
alt: true,
|
||||||
};
|
ctrl: false,
|
||||||
} else if (terminal === 'MacTerminal') {
|
cmd: false,
|
||||||
// Mac Terminal sends ESC + letter
|
},
|
||||||
return {
|
};
|
||||||
terminal,
|
} else if (terminal === 'MacTerminal') {
|
||||||
key,
|
// Mac Terminal sends ESC + letter
|
||||||
kitty: false,
|
const chunk = shiftValue
|
||||||
chunk: `\x1b${key}`,
|
? `\x1b${key.toUpperCase()}`
|
||||||
expected: {
|
: `\x1b${key.toLowerCase()}`;
|
||||||
sequence: `\x1b${key}`,
|
return {
|
||||||
name: key,
|
terminal,
|
||||||
shift: false,
|
key,
|
||||||
alt: true,
|
kitty: false,
|
||||||
ctrl: false,
|
chunk,
|
||||||
cmd: false,
|
expected: {
|
||||||
},
|
sequence: chunk,
|
||||||
};
|
name: key.toLowerCase(),
|
||||||
} else {
|
shift: shiftValue,
|
||||||
// iTerm2 and VSCode send accented characters (å, ø, µ)
|
alt: true,
|
||||||
// Note: µ (mu) is sent with alt:false on iTerm2/VSCode but
|
ctrl: false,
|
||||||
// gets converted to m with alt:true
|
cmd: false,
|
||||||
return {
|
},
|
||||||
terminal,
|
};
|
||||||
key,
|
} else {
|
||||||
chunk: accentedChar,
|
// iTerm2 and VSCode send accented characters (å, ø, µ, Ω, ¸)
|
||||||
expected: {
|
return {
|
||||||
name: key,
|
terminal,
|
||||||
shift: false,
|
key,
|
||||||
alt: true, // Always expect alt:true after conversion
|
chunk: accentedChar,
|
||||||
ctrl: false,
|
expected: {
|
||||||
cmd: false,
|
name: key.toLowerCase(),
|
||||||
sequence: accentedChar,
|
shift: shiftValue,
|
||||||
},
|
alt: true, // Always expect alt:true after conversion
|
||||||
};
|
ctrl: false,
|
||||||
}
|
cmd: false,
|
||||||
}),
|
sequence: accentedChar,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)(
|
)(
|
||||||
'should handle Alt+$key in $terminal',
|
'should handle Alt+$key in $terminal',
|
||||||
@@ -1302,4 +1309,57 @@ describe('KeypressContext', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Greek support', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
vi.unstubAllEnvs();
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
{
|
||||||
|
lang: 'en_US.UTF-8',
|
||||||
|
expected: { name: 'z', alt: true, insertable: false },
|
||||||
|
desc: 'non-Greek locale (Option+z)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lang: 'el_GR.UTF-8',
|
||||||
|
expected: { name: '', insertable: true },
|
||||||
|
desc: 'Greek LANG',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lcAll: 'el_GR.UTF-8',
|
||||||
|
expected: { name: '', insertable: true },
|
||||||
|
desc: 'Greek LC_ALL',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lang: 'en_US.UTF-8',
|
||||||
|
lcAll: 'el_GR.UTF-8',
|
||||||
|
expected: { name: '', insertable: true },
|
||||||
|
desc: 'LC_ALL overriding non-Greek LANG',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lang: 'el_GR.UTF-8',
|
||||||
|
char: '\u00B8',
|
||||||
|
expected: { name: 'z', alt: true, shift: true },
|
||||||
|
desc: 'Cedilla (\u00B8) in Greek locale (should be Option+Shift+z)',
|
||||||
|
},
|
||||||
|
])(
|
||||||
|
'should handle $char correctly in $desc',
|
||||||
|
async ({ lang, lcAll, char = '\u03A9', expected }) => {
|
||||||
|
if (lang) vi.stubEnv('LANG', lang);
|
||||||
|
if (lcAll) vi.stubEnv('LC_ALL', lcAll);
|
||||||
|
|
||||||
|
const { keyHandler } = setupKeypressTest();
|
||||||
|
|
||||||
|
act(() => stdin.write(char));
|
||||||
|
|
||||||
|
expect(keyHandler).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
...expected,
|
||||||
|
sequence: char,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -130,6 +130,8 @@ const MAC_ALT_KEY_CHARACTER_MAP: Record<string, string> = {
|
|||||||
'\u222B': 'b', // "∫" back one word
|
'\u222B': 'b', // "∫" back one word
|
||||||
'\u0192': 'f', // "ƒ" forward one word
|
'\u0192': 'f', // "ƒ" forward one word
|
||||||
'\u00B5': 'm', // "µ" toggle markup view
|
'\u00B5': 'm', // "µ" toggle markup view
|
||||||
|
'\u03A9': 'z', // "Ω" Option+z
|
||||||
|
'\u00B8': 'Z', // "¸" Option+Shift+z
|
||||||
};
|
};
|
||||||
|
|
||||||
function nonKeyboardEventFilter(
|
function nonKeyboardEventFilter(
|
||||||
@@ -305,6 +307,10 @@ function createDataListener(keypressHandler: KeypressHandler) {
|
|||||||
function* emitKeys(
|
function* emitKeys(
|
||||||
keypressHandler: KeypressHandler,
|
keypressHandler: KeypressHandler,
|
||||||
): Generator<void, void, string> {
|
): Generator<void, void, string> {
|
||||||
|
const lang = process.env['LANG'] || '';
|
||||||
|
const lcAll = process.env['LC_ALL'] || '';
|
||||||
|
const isGreek = lang.startsWith('el') || lcAll.startsWith('el');
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
let ch = yield;
|
let ch = yield;
|
||||||
let sequence = ch;
|
let sequence = ch;
|
||||||
@@ -574,8 +580,15 @@ function* emitKeys(
|
|||||||
} else if (MAC_ALT_KEY_CHARACTER_MAP[ch]) {
|
} else if (MAC_ALT_KEY_CHARACTER_MAP[ch]) {
|
||||||
// Note: we do this even if we are not on Mac, because mac users may
|
// Note: we do this even if we are not on Mac, because mac users may
|
||||||
// remotely connect to non-Mac systems.
|
// remotely connect to non-Mac systems.
|
||||||
name = MAC_ALT_KEY_CHARACTER_MAP[ch];
|
// We skip this mapping for Greek users to avoid blocking the Omega character.
|
||||||
alt = true;
|
if (isGreek && ch === '\u03A9') {
|
||||||
|
insertable = true;
|
||||||
|
} else {
|
||||||
|
const mapped = MAC_ALT_KEY_CHARACTER_MAP[ch];
|
||||||
|
name = mapped.toLowerCase();
|
||||||
|
shift = mapped !== name;
|
||||||
|
alt = true;
|
||||||
|
}
|
||||||
} else if (sequence === `${ESC}${ESC}`) {
|
} else if (sequence === `${ESC}${ESC}`) {
|
||||||
// Double escape
|
// Double escape
|
||||||
name = 'escape';
|
name = 'escape';
|
||||||
|
|||||||
@@ -2,6 +2,38 @@
|
|||||||
|
|
||||||
exports[`terminalSetup > configureVSCodeStyle > should create new keybindings file if none exists 1`] = `
|
exports[`terminalSetup > configureVSCodeStyle > should create new keybindings file if none exists 1`] = `
|
||||||
[
|
[
|
||||||
|
{
|
||||||
|
"args": {
|
||||||
|
"text": "[122;4u",
|
||||||
|
},
|
||||||
|
"command": "workbench.action.terminal.sendSequence",
|
||||||
|
"key": "shift+alt+z",
|
||||||
|
"when": "terminalFocus",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"args": {
|
||||||
|
"text": "[122;10u",
|
||||||
|
},
|
||||||
|
"command": "workbench.action.terminal.sendSequence",
|
||||||
|
"key": "shift+cmd+z",
|
||||||
|
"when": "terminalFocus",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"args": {
|
||||||
|
"text": "[122;3u",
|
||||||
|
},
|
||||||
|
"command": "workbench.action.terminal.sendSequence",
|
||||||
|
"key": "alt+z",
|
||||||
|
"when": "terminalFocus",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"args": {
|
||||||
|
"text": "[122;9u",
|
||||||
|
},
|
||||||
|
"command": "workbench.action.terminal.sendSequence",
|
||||||
|
"key": "cmd+z",
|
||||||
|
"when": "terminalFocus",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"args": {
|
"args": {
|
||||||
"text": "\\
|
"text": "\\
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ describe('terminalSetup', () => {
|
|||||||
|
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
const writtenContent = JSON.parse(mocks.writeFile.mock.calls[0][1]);
|
const writtenContent = JSON.parse(mocks.writeFile.mock.calls[0][1]);
|
||||||
expect(writtenContent).toHaveLength(2); // Shift+Enter and Ctrl+Enter
|
expect(writtenContent).toHaveLength(6); // Shift+Enter, Ctrl+Enter, Cmd+Z, Alt+Z, Shift+Cmd+Z, Shift+Alt+Z
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not modify if bindings already exist', async () => {
|
it('should not modify if bindings already exist', async () => {
|
||||||
@@ -145,6 +145,26 @@ describe('terminalSetup', () => {
|
|||||||
command: 'workbench.action.terminal.sendSequence',
|
command: 'workbench.action.terminal.sendSequence',
|
||||||
args: { text: VSCODE_SHIFT_ENTER_SEQUENCE },
|
args: { text: VSCODE_SHIFT_ENTER_SEQUENCE },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'cmd+z',
|
||||||
|
command: 'workbench.action.terminal.sendSequence',
|
||||||
|
args: { text: '\u001b[122;9u' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'alt+z',
|
||||||
|
command: 'workbench.action.terminal.sendSequence',
|
||||||
|
args: { text: '\u001b[122;3u' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'shift+cmd+z',
|
||||||
|
command: 'workbench.action.terminal.sendSequence',
|
||||||
|
args: { text: '\u001b[122;10u' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'shift+alt+z',
|
||||||
|
command: 'workbench.action.terminal.sendSequence',
|
||||||
|
args: { text: '\u001b[122;4u' },
|
||||||
|
},
|
||||||
];
|
];
|
||||||
mocks.readFile.mockResolvedValue(JSON.stringify(existingBindings));
|
mocks.readFile.mockResolvedValue(JSON.stringify(existingBindings));
|
||||||
|
|
||||||
|
|||||||
@@ -204,94 +204,105 @@ async function configureVSCodeStyle(
|
|||||||
// File doesn't exist, will create new one
|
// File doesn't exist, will create new one
|
||||||
}
|
}
|
||||||
|
|
||||||
const shiftEnterBinding = {
|
const targetBindings = [
|
||||||
key: 'shift+enter',
|
{
|
||||||
command: 'workbench.action.terminal.sendSequence',
|
key: 'shift+enter',
|
||||||
when: 'terminalFocus',
|
command: 'workbench.action.terminal.sendSequence',
|
||||||
args: { text: VSCODE_SHIFT_ENTER_SEQUENCE },
|
when: 'terminalFocus',
|
||||||
};
|
args: { text: VSCODE_SHIFT_ENTER_SEQUENCE },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'ctrl+enter',
|
||||||
|
command: 'workbench.action.terminal.sendSequence',
|
||||||
|
when: 'terminalFocus',
|
||||||
|
args: { text: VSCODE_SHIFT_ENTER_SEQUENCE },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'cmd+z',
|
||||||
|
command: 'workbench.action.terminal.sendSequence',
|
||||||
|
when: 'terminalFocus',
|
||||||
|
args: { text: '\u001b[122;9u' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'alt+z',
|
||||||
|
command: 'workbench.action.terminal.sendSequence',
|
||||||
|
when: 'terminalFocus',
|
||||||
|
args: { text: '\u001b[122;3u' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'shift+cmd+z',
|
||||||
|
command: 'workbench.action.terminal.sendSequence',
|
||||||
|
when: 'terminalFocus',
|
||||||
|
args: { text: '\u001b[122;10u' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'shift+alt+z',
|
||||||
|
command: 'workbench.action.terminal.sendSequence',
|
||||||
|
when: 'terminalFocus',
|
||||||
|
args: { text: '\u001b[122;4u' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const ctrlEnterBinding = {
|
const results = targetBindings.map((target) => {
|
||||||
key: 'ctrl+enter',
|
const hasOurBinding = keybindings.some((kb) => {
|
||||||
command: 'workbench.action.terminal.sendSequence',
|
const binding = kb as {
|
||||||
when: 'terminalFocus',
|
command?: string;
|
||||||
args: { text: VSCODE_SHIFT_ENTER_SEQUENCE },
|
args?: { text?: string };
|
||||||
};
|
key?: string;
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
binding.key === target.key &&
|
||||||
|
binding.command === target.command &&
|
||||||
|
binding.args?.text === target.args.text
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
// Check if our specific bindings already exist
|
const existingBinding = keybindings.find((kb) => {
|
||||||
const hasOurShiftEnter = keybindings.some((kb) => {
|
const binding = kb as { key?: string };
|
||||||
const binding = kb as {
|
return binding.key === target.key;
|
||||||
command?: string;
|
});
|
||||||
args?: { text?: string };
|
|
||||||
key?: string;
|
return {
|
||||||
|
target,
|
||||||
|
hasOurBinding,
|
||||||
|
conflict: !!existingBinding && !hasOurBinding,
|
||||||
|
conflictMessage: `- ${target.key.charAt(0).toUpperCase() + target.key.slice(1)} binding already exists`,
|
||||||
};
|
};
|
||||||
return (
|
|
||||||
binding.key === 'shift+enter' &&
|
|
||||||
binding.command === 'workbench.action.terminal.sendSequence' &&
|
|
||||||
binding.args?.text === '\\\r\n'
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const hasOurCtrlEnter = keybindings.some((kb) => {
|
if (results.every((r) => r.hasOurBinding)) {
|
||||||
const binding = kb as {
|
|
||||||
command?: string;
|
|
||||||
args?: { text?: string };
|
|
||||||
key?: string;
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
binding.key === 'ctrl+enter' &&
|
|
||||||
binding.command === 'workbench.action.terminal.sendSequence' &&
|
|
||||||
binding.args?.text === '\\\r\n'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (hasOurShiftEnter && hasOurCtrlEnter) {
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: `${terminalName} keybindings already configured.`,
|
message: `${terminalName} keybindings already configured.`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if ANY shift+enter or ctrl+enter bindings already exist (that are NOT ours)
|
const conflicts = results.filter((r) => r.conflict);
|
||||||
const existingShiftEnter = keybindings.find((kb) => {
|
if (conflicts.length > 0) {
|
||||||
const binding = kb as { key?: string };
|
return {
|
||||||
return binding.key === 'shift+enter';
|
success: false,
|
||||||
});
|
message:
|
||||||
|
`Existing keybindings detected. Will not modify to avoid conflicts.\n` +
|
||||||
const existingCtrlEnter = keybindings.find((kb) => {
|
conflicts.map((c) => c.conflictMessage).join('\n') +
|
||||||
const binding = kb as { key?: string };
|
'\n' +
|
||||||
return binding.key === 'ctrl+enter';
|
`Please check and modify manually if needed: ${keybindingsFile}`,
|
||||||
});
|
};
|
||||||
|
|
||||||
if (existingShiftEnter || existingCtrlEnter) {
|
|
||||||
const messages: string[] = [];
|
|
||||||
// Only report conflict if it's not our binding (though we checked above, partial matches might exist)
|
|
||||||
if (existingShiftEnter && !hasOurShiftEnter) {
|
|
||||||
messages.push(`- Shift+Enter binding already exists`);
|
|
||||||
}
|
|
||||||
if (existingCtrlEnter && !hasOurCtrlEnter) {
|
|
||||||
messages.push(`- Ctrl+Enter binding already exists`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (messages.length > 0) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message:
|
|
||||||
`Existing keybindings detected. Will not modify to avoid conflicts.\n` +
|
|
||||||
messages.join('\n') +
|
|
||||||
'\n' +
|
|
||||||
`Please check and modify manually if needed: ${keybindingsFile}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasOurShiftEnter) keybindings.unshift(shiftEnterBinding);
|
for (const { hasOurBinding, target } of results) {
|
||||||
if (!hasOurCtrlEnter) keybindings.unshift(ctrlEnterBinding);
|
if (!hasOurBinding) {
|
||||||
|
keybindings.unshift(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await fs.writeFile(keybindingsFile, JSON.stringify(keybindings, null, 4));
|
await fs.writeFile(keybindingsFile, JSON.stringify(keybindings, null, 4));
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: `Added Shift+Enter and Ctrl+Enter keybindings to ${terminalName}.\nModified: ${keybindingsFile}`,
|
message: `Added ${targetBindings
|
||||||
|
.map((b) => b.key.charAt(0).toUpperCase() + b.key.slice(1))
|
||||||
|
.join(
|
||||||
|
', ',
|
||||||
|
)} keybindings to ${terminalName}.\nModified: ${keybindingsFile}`,
|
||||||
requiresRestart: true,
|
requiresRestart: true,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import { isITerm2, resetITerm2Cache } from './terminalUtils.js';
|
|||||||
describe('terminalUtils', () => {
|
describe('terminalUtils', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.stubEnv('TERM_PROGRAM', '');
|
vi.stubEnv('TERM_PROGRAM', '');
|
||||||
vi.stubEnv('ITERM_SESSION_ID', '');
|
|
||||||
resetITerm2Cache();
|
resetITerm2Cache();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -24,11 +23,6 @@ describe('terminalUtils', () => {
|
|||||||
expect(isITerm2()).toBe(true);
|
expect(isITerm2()).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should detect iTerm2 via ITERM_SESSION_ID', () => {
|
|
||||||
vi.stubEnv('ITERM_SESSION_ID', 'w0t0p0:6789...');
|
|
||||||
expect(isITerm2()).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return false if not iTerm2', () => {
|
it('should return false if not iTerm2', () => {
|
||||||
vi.stubEnv('TERM_PROGRAM', 'vscode');
|
vi.stubEnv('TERM_PROGRAM', 'vscode');
|
||||||
expect(isITerm2()).toBe(false);
|
expect(isITerm2()).toBe(false);
|
||||||
|
|||||||
@@ -31,9 +31,7 @@ export function isITerm2(): boolean {
|
|||||||
return cachedIsITerm2;
|
return cachedIsITerm2;
|
||||||
}
|
}
|
||||||
|
|
||||||
cachedIsITerm2 =
|
cachedIsITerm2 = process.env['TERM_PROGRAM'] === 'iTerm.app';
|
||||||
process.env['TERM_PROGRAM'] === 'iTerm.app' ||
|
|
||||||
!!process.env['ITERM_SESSION_ID'];
|
|
||||||
|
|
||||||
return cachedIsITerm2;
|
return cachedIsITerm2;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user