Skip to content

Commit 57c42a5

Browse files
feat(cli): add Sublime Text and Emacs Client editors, improve error messages and documentation (google-gemini#21090)
Co-authored-by: Ananth Kini <ananthkini1@gmail.com>
1 parent 8997488 commit 57c42a5

11 files changed

Lines changed: 518 additions & 76 deletions

File tree

docs/reference/configuration.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,19 @@ their corresponding top-level category object in your `settings.json` file.
105105

106106
#### `general`
107107

108-
- **`general.preferredEditor`** (string):
109-
- **Description:** The preferred editor to open files in.
108+
- **`general.preferredEditor`** (enum):
109+
- **Description:** The preferred editor to open files in. Must be one of the
110+
built-in supported identifiers. Use /editor in the CLI to pick
111+
interactively, or leave unset to use $VISUAL/$EDITOR.
110112
- **Default:** `undefined`
113+
- **Values:** `"vscode"`, `"vscodium"`, `"windsurf"`, `"cursor"`, `"zed"`,
114+
`"antigravity"`, `"sublimetext"`, `"lapce"`, `"nova"`, `"bbedit"`, `"vim"`,
115+
`"neovim"`, `"emacs"`, `"hx"`, `"emacsclient"`, `"micro"`
116+
117+
- **`general.openEditorInNewWindow`** (boolean):
118+
- **Description:** Open VS Code-family editors in a new window when editing
119+
files.
120+
- **Default:** `false`
111121

112122
- **`general.vimMode`** (boolean):
113123
- **Description:** Enable Vim keybindings

packages/cli/src/config/settingsSchema.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import {
1313
DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD,
1414
DEFAULT_MODEL_CONFIGS,
15+
EDITOR_OPTIONS,
1516
AuthProviderType,
1617
type MCPServerConfig,
1718
type RequiredMcpServerConfig,
@@ -192,12 +193,27 @@ const SETTINGS_SCHEMA = {
192193
showInDialog: false,
193194
properties: {
194195
preferredEditor: {
195-
type: 'string',
196+
type: 'enum',
196197
label: 'Preferred Editor',
197198
category: 'General',
198199
requiresRestart: false,
199200
default: undefined as string | undefined,
200-
description: 'The preferred editor to open files in.',
201+
description: oneLine`
202+
The preferred editor to open files in. Must be one of the built-in
203+
supported identifiers. Use /editor in the CLI to pick interactively,
204+
or leave unset to use $VISUAL/$EDITOR.
205+
`,
206+
showInDialog: false,
207+
options: EDITOR_OPTIONS,
208+
},
209+
openEditorInNewWindow: {
210+
type: 'boolean',
211+
label: 'Open Editor in New Window',
212+
category: 'General',
213+
requiresRestart: false,
214+
default: false,
215+
description:
216+
'Open VS Code-family editors in a new window when editing files.',
201217
showInDialog: false,
202218
},
203219
vimMode: {

packages/cli/src/ui/AppContainer.tsx

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ import { MouseProvider } from './contexts/MouseContext.js';
4747
import { ScrollProvider } from './contexts/ScrollProvider.js';
4848
import {
4949
type StartupWarning,
50-
type EditorType,
5150
type Config,
5251
type IdeInfo,
5352
type IdeContext,
@@ -68,6 +67,7 @@ import {
6867
ShellExecutionService,
6968
saveApiKey,
7069
debugLogger,
70+
isValidEditorType,
7171
coreEvents,
7272
CoreEvent,
7373
flattenMemory,
@@ -609,11 +609,10 @@ export const AppContainer = (props: AppContainerProps) => {
609609

610610
const staticAreaMaxItemHeight = Math.max(terminalHeight * 4, 100);
611611

612-
const getPreferredEditor = useCallback(
613-
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
614-
() => settings.merged.general.preferredEditor as EditorType,
615-
[settings.merged.general.preferredEditor],
616-
);
612+
const getPreferredEditor = useCallback(() => {
613+
const val = settings.merged.general.preferredEditor;
614+
return isValidEditorType(val) ? val : undefined;
615+
}, [settings.merged.general.preferredEditor]);
617616

618617
const buffer = useTextBuffer({
619618
initialText: '',

packages/cli/src/ui/components/EditorSettingsDialog.tsx

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import {
2222
type EditorType,
2323
isEditorAvailable,
2424
EDITOR_DISPLAY_NAMES,
25-
coreEvents,
2625
} from '@google/gemini-cli-core';
2726
import { useKeypress } from '../hooks/useKeypress.js';
2827

@@ -72,10 +71,6 @@ export function EditorSettingsDialog({
7271
)
7372
: 0;
7473
if (editorIndex === -1) {
75-
coreEvents.emitFeedback(
76-
'error',
77-
`Editor is not supported: ${currentPreference}`,
78-
);
7974
editorIndex = 0;
8075
}
8176

@@ -131,10 +126,7 @@ export function EditorSettingsDialog({
131126
isEditorAvailable(settings.merged.general.preferredEditor)
132127
) {
133128
mergedEditorName =
134-
EDITOR_DISPLAY_NAMES[
135-
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
136-
settings.merged.general.preferredEditor as EditorType
137-
];
129+
EDITOR_DISPLAY_NAMES[settings.merged.general.preferredEditor];
138130
}
139131

140132
return (
@@ -161,6 +153,7 @@ export function EditorSettingsDialog({
161153
onSelect={handleEditorSelect}
162154
isFocused={focusedSection === 'editor'}
163155
key={selectedScope}
156+
maxItemsToShow={editorItems.length}
164157
/>
165158

166159
<Box marginTop={1} flexDirection="column">

packages/cli/src/ui/components/shared/performance.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,17 @@ import { renderHook } from '../../../test-utils/render.js';
99
import { useTextBuffer } from './text-buffer.js';
1010
import { parseInputForHighlighting } from '../../utils/highlight.js';
1111

12+
vi.mock('../../contexts/SettingsContext.js', async (importOriginal) => {
13+
const actual =
14+
await importOriginal<typeof import('../../contexts/SettingsContext.js')>();
15+
return {
16+
...actual,
17+
useSettings: () => ({
18+
merged: { general: { openEditorInNewWindow: false } },
19+
}),
20+
};
21+
});
22+
1223
describe('text-buffer performance', () => {
1324
afterEach(() => {
1425
vi.restoreAllMocks();

packages/cli/src/ui/components/shared/text-buffer.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,17 @@ import { cpLen } from '../../utils/textUtils.js';
4444
import { type Key } from '../../hooks/useKeypress.js';
4545
import { escapePath } from '@google/gemini-cli-core';
4646

47+
vi.mock('../../contexts/SettingsContext.js', async (importOriginal) => {
48+
const actual =
49+
await importOriginal<typeof import('../../contexts/SettingsContext.js')>();
50+
return {
51+
...actual,
52+
useSettings: () => ({
53+
merged: { general: { openEditorInNewWindow: false } },
54+
}),
55+
};
56+
});
57+
4758
const defaultVisualLayout: VisualLayout = {
4859
visualLines: [''],
4960
logicalToVisualMap: [[[0, 0]]],

packages/cli/src/ui/components/shared/text-buffer.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { LRUCache } from 'mnemonist';
1313
import {
1414
coreEvents,
1515
debugLogger,
16+
getErrorMessage,
1617
unescapePath,
1718
type EditorType,
1819
} from '@google/gemini-cli-core';
@@ -30,6 +31,7 @@ import type { VimAction } from './vim-buffer-actions.js';
3031
import { handleVimAction } from './vim-buffer-actions.js';
3132
import { LRU_BUFFER_PERF_CACHE_LIMIT } from '../../constants.js';
3233
import { openFileInEditor } from '../../utils/editorUtils.js';
34+
import { useSettings } from '../../contexts/SettingsContext.js';
3335
import { useKeyMatchers } from '../../hooks/useKeyMatchers.js';
3436

3537
export const LARGE_PASTE_LINE_THRESHOLD = 5;
@@ -2840,6 +2842,7 @@ export function useTextBuffer({
28402842
singleLine = false,
28412843
getPreferredEditor,
28422844
}: UseTextBufferProps): TextBuffer {
2845+
const settings = useSettings();
28432846
const keyMatchers = useKeyMatchers();
28442847
const initialState = useMemo((): TextBufferState => {
28452848
const lines = initialText.split('\n');
@@ -3325,6 +3328,7 @@ export function useTextBuffer({
33253328
stdin,
33263329
setRawMode,
33273330
getPreferredEditor?.(),
3331+
settings.merged.general.openEditorInNewWindow,
33283332
);
33293333

33303334
let newText = fs.readFileSync(filePath, 'utf8');
@@ -3342,11 +3346,7 @@ export function useTextBuffer({
33423346

33433347
dispatch({ type: 'set_text', payload: newText, pushToUndo: false });
33443348
} catch (err) {
3345-
coreEvents.emitFeedback(
3346-
'error',
3347-
'[useTextBuffer] external editor error',
3348-
err,
3349-
);
3349+
coreEvents.emitFeedback('error', getErrorMessage(err), err);
33503350
} finally {
33513351
try {
33523352
fs.unlinkSync(filePath);
@@ -3359,7 +3359,14 @@ export function useTextBuffer({
33593359
/* ignore */
33603360
}
33613361
}
3362-
}, [text, pastedContent, stdin, setRawMode, getPreferredEditor]);
3362+
}, [
3363+
text,
3364+
pastedContent,
3365+
stdin,
3366+
setRawMode,
3367+
getPreferredEditor,
3368+
settings.merged.general.openEditorInNewWindow,
3369+
]);
33633370

33643371
const handleInput = useCallback(
33653372
(key: Key): boolean => {

0 commit comments

Comments
 (0)