mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-08-04 14:11:11 -07:00
57c42a5c40
Co-authored-by: Ananth Kini <ananthkini1@gmail.com>
211 lines
6.1 KiB
TypeScript
211 lines
6.1 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2026 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import { spawn, spawnSync } from 'node:child_process';
|
|
import type { ReadStream } from 'node:tty';
|
|
import {
|
|
ALL_EDITORS,
|
|
CoreEvent,
|
|
coreEvents,
|
|
type EditorType,
|
|
getEditorCommand,
|
|
getEditorExtraArgs,
|
|
getEditorWaitFlag,
|
|
isGuiEditor,
|
|
isTerminalEditor,
|
|
isValidEditorType,
|
|
resolveEditorTypeFromCommand,
|
|
} from '@google/gemini-cli-core';
|
|
|
|
/**
|
|
* Command name substrings used to guess whether an unknown $VISUAL/$EDITOR
|
|
* value is a GUI editor. This is a fallback for editors not in the registry;
|
|
* registered editors are detected via resolveEditorTypeFromCommand instead.
|
|
*/
|
|
const HEURISTIC_GUI_COMMANDS = [
|
|
'code',
|
|
'cursor',
|
|
'subl',
|
|
'zed',
|
|
'atom',
|
|
'agy',
|
|
] as const;
|
|
|
|
/**
|
|
* Opens a file in an external editor and waits for it to close.
|
|
* Handles raw mode switching to ensure the editor can interact with the terminal.
|
|
*
|
|
* @param filePath Path to the file to open
|
|
* @param stdin The stdin stream from Ink/Node
|
|
* @param setRawMode Function to toggle raw mode
|
|
* @param preferredEditorType The user's preferred editor from config
|
|
* @param openInNewWindow Whether to open VS Code-family editors in a new window
|
|
*/
|
|
export async function openFileInEditor(
|
|
filePath: string,
|
|
stdin: ReadStream | null | undefined,
|
|
setRawMode: ((mode: boolean) => void) | undefined,
|
|
preferredEditorType?: EditorType,
|
|
openInNewWindow?: boolean,
|
|
): Promise<void> {
|
|
let command: string | undefined = undefined;
|
|
const args = [filePath];
|
|
// Extra args that come before the file path (e.g. -nw for emacsclient)
|
|
const extraArgs: string[] = [];
|
|
|
|
if (preferredEditorType) {
|
|
if (!isValidEditorType(preferredEditorType)) {
|
|
coreEvents.emitFeedback(
|
|
'error',
|
|
`Editor '${preferredEditorType}' is not a recognized editor identifier. ` +
|
|
`Supported editors: ${ALL_EDITORS.join(', ')}. ` +
|
|
`Use /editor to select one, or set the $VISUAL or $EDITOR environment variable.`,
|
|
);
|
|
return;
|
|
}
|
|
command = getEditorCommand(preferredEditorType);
|
|
if (isGuiEditor(preferredEditorType)) {
|
|
args.unshift(getEditorWaitFlag(preferredEditorType));
|
|
}
|
|
extraArgs.push(
|
|
...getEditorExtraArgs(preferredEditorType, {
|
|
newWindow: openInNewWindow,
|
|
}),
|
|
);
|
|
}
|
|
|
|
if (!command) {
|
|
const envCommand = process.env['VISUAL'] ?? process.env['EDITOR'];
|
|
if (envCommand) {
|
|
command = envCommand;
|
|
const [envExecutable = ''] = envCommand.split(' ');
|
|
const resolvedType = resolveEditorTypeFromCommand(envExecutable);
|
|
if (resolvedType) {
|
|
if (
|
|
isGuiEditor(resolvedType) &&
|
|
!envCommand.includes('--wait') &&
|
|
!envCommand.includes('-w')
|
|
) {
|
|
args.unshift(getEditorWaitFlag(resolvedType));
|
|
}
|
|
extraArgs.push(
|
|
...getEditorExtraArgs(resolvedType, { newWindow: openInNewWindow }),
|
|
);
|
|
} else {
|
|
// Heuristic fallback for commands not in the registry
|
|
const lower = envCommand.toLowerCase();
|
|
const isGui = HEURISTIC_GUI_COMMANDS.some((g) => lower.includes(g));
|
|
if (isGui && !lower.includes('--wait') && !lower.includes('-w')) {
|
|
args.unshift(lower.includes('subl') ? '-w' : '--wait');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!command) {
|
|
command = process.platform === 'win32' ? 'notepad' : 'vi';
|
|
}
|
|
|
|
const [executable = '', ...initialArgs] = command.split(' ');
|
|
|
|
// Determine if we should use sync or async based on the command/editor type.
|
|
// If we have a preferredEditorType, we can check if it's a terminal editor.
|
|
// Otherwise, we guess based on the command name.
|
|
const terminalEditors = [
|
|
'vi',
|
|
'vim',
|
|
'nvim',
|
|
'emacs',
|
|
'emacsclient',
|
|
'hx',
|
|
'nano',
|
|
'micro',
|
|
];
|
|
const isTerminal = preferredEditorType
|
|
? isTerminalEditor(preferredEditorType)
|
|
: terminalEditors.some((te) => executable.toLowerCase().includes(te));
|
|
|
|
if (
|
|
isTerminal &&
|
|
(executable.includes('vi') ||
|
|
executable.includes('vim') ||
|
|
executable.includes('nvim'))
|
|
) {
|
|
// Pass -i NONE to prevent E138 'Can't write viminfo file' errors in restricted environments.
|
|
args.unshift('-i', 'NONE');
|
|
}
|
|
|
|
const wasRaw = stdin?.isRaw ?? false;
|
|
setRawMode?.(false);
|
|
|
|
try {
|
|
if (isTerminal) {
|
|
const result = spawnSync(
|
|
executable,
|
|
[...initialArgs, ...extraArgs, ...args],
|
|
{
|
|
stdio: 'inherit',
|
|
shell: process.platform === 'win32',
|
|
},
|
|
);
|
|
if (result.error) {
|
|
const spawnErr = result.error as NodeJS.ErrnoException;
|
|
coreEvents.emitFeedback(
|
|
'error',
|
|
spawnErr.code === 'ENOENT'
|
|
? `Editor command '${executable}' was not found in PATH. Install it or use /editor to choose another editor.`
|
|
: (spawnErr.message ?? String(spawnErr)),
|
|
);
|
|
return;
|
|
}
|
|
if (typeof result.status === 'number' && result.status !== 0) {
|
|
coreEvents.emitFeedback(
|
|
'error',
|
|
`External editor exited with status ${result.status}`,
|
|
);
|
|
return;
|
|
}
|
|
} else {
|
|
await new Promise<void>((resolve) => {
|
|
const child = spawn(
|
|
executable,
|
|
[...initialArgs, ...extraArgs, ...args],
|
|
{
|
|
stdio: 'inherit',
|
|
shell: process.platform === 'win32',
|
|
},
|
|
);
|
|
|
|
child.on('error', (err) => {
|
|
const spawnErr = err as NodeJS.ErrnoException;
|
|
resolve();
|
|
coreEvents.emitFeedback(
|
|
'error',
|
|
spawnErr.code === 'ENOENT'
|
|
? `Editor command '${executable}' was not found in PATH. Install it or use /editor to choose another editor.`
|
|
: (spawnErr.message ?? String(spawnErr)),
|
|
);
|
|
});
|
|
|
|
child.on('close', (status) => {
|
|
resolve();
|
|
if (typeof status === 'number' && status !== 0) {
|
|
coreEvents.emitFeedback(
|
|
'error',
|
|
`External editor exited with status ${status}`,
|
|
);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
} finally {
|
|
if (wasRaw) {
|
|
setRawMode?.(true);
|
|
}
|
|
coreEvents.emit(CoreEvent.ExternalEditorClosed);
|
|
}
|
|
}
|