mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-24 22:55:13 +00:00
feat: autogenerate keyboard shortcut docs (#12944)
This commit is contained in:
@@ -1,84 +1,132 @@
|
|||||||
# Gemini CLI Keyboard Shortcuts
|
# Gemini CLI Keyboard Shortcuts
|
||||||
|
|
||||||
This document lists the available keyboard shortcuts within Gemini CLI.
|
Gemini CLI ships with a set of default keyboard shortcuts for editing input,
|
||||||
|
navigating history, and controlling the UI. Use this reference to learn the
|
||||||
|
available combinations.
|
||||||
|
|
||||||
## General
|
<!-- KEYBINDINGS-AUTOGEN:START -->
|
||||||
|
|
||||||
| Shortcut | Description |
|
#### Basic Controls
|
||||||
| ----------- | --------------------------------------------------------------------------------------------------------------------- |
|
|
||||||
| `Esc` | Close dialogs and suggestions. |
|
|
||||||
| `Ctrl+C` | Cancel the ongoing request and clear the input. Press twice to exit the application. |
|
|
||||||
| `Ctrl+D` | Exit the application if the input is empty. Press twice to confirm. |
|
|
||||||
| `Ctrl+L` | Clear the screen. |
|
|
||||||
| `Ctrl+S` | Allows long responses to print fully, disabling truncation. Use your terminal's scrollback to view the entire output. |
|
|
||||||
| `Ctrl+S` | Toggle copy mode (alternate buffer mode only). |
|
|
||||||
| `Ctrl+T` | Toggle the display of the todo list. |
|
|
||||||
| `Ctrl+Y` | Toggle auto-approval (YOLO mode) for all tool calls. |
|
|
||||||
| `Shift+Tab` | Toggle auto-accepting edits approval mode. |
|
|
||||||
| `Option+M` | Toggle Markdown rendering for messages (raw markdown mode). |
|
|
||||||
| `F12` | Toggle the display of the debug console. |
|
|
||||||
|
|
||||||
## Input Prompt
|
| Action | Keys |
|
||||||
|
| -------------------------------------------- | ------- |
|
||||||
|
| Confirm the current selection or choice. | `Enter` |
|
||||||
|
| Dismiss dialogs or cancel the current focus. | `Esc` |
|
||||||
|
|
||||||
| Shortcut | Description |
|
#### Cursor Movement
|
||||||
| -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
|
||||||
| `!` | Toggle shell mode when the input is empty. |
|
|
||||||
| `\` (at end of line) + `Enter` | Insert a newline. |
|
|
||||||
| `Down Arrow` | Navigate down through the input history. |
|
|
||||||
| `Enter` | Submit the current prompt. |
|
|
||||||
| `Meta+Delete` / `Ctrl+Delete` | Delete the word to the right of the cursor. |
|
|
||||||
| `Tab` | Autocomplete the current suggestion if one exists. |
|
|
||||||
| `Up Arrow` | Navigate up through the input history. |
|
|
||||||
| `Ctrl+A` / `Home` | Move the cursor to the beginning of the line. |
|
|
||||||
| `Ctrl+B` / `Left Arrow` | Move the cursor one character to the left. |
|
|
||||||
| `Ctrl+C` | Clear the input prompt |
|
|
||||||
| `Esc` (double press) | Clear the input prompt. |
|
|
||||||
| `Ctrl+D` / `Delete` | Delete the character to the right of the cursor. |
|
|
||||||
| `Ctrl+E` / `End` | Move the cursor to the end of the line. |
|
|
||||||
| `Ctrl+F` / `Right Arrow` | Move the cursor one character to the right. `Ctrl+F` also toggles focus between input and interactive shell if active. |
|
|
||||||
| `Ctrl+H` / `Backspace` | Delete the character to the left of the cursor. |
|
|
||||||
| `Ctrl+K` | Delete from the cursor to the end of the line. |
|
|
||||||
| `Ctrl+Left Arrow` / `Meta+Left Arrow` / `Meta+B` | Move the cursor one word to the left. |
|
|
||||||
| `Ctrl+N` | Navigate down through the input history. |
|
|
||||||
| `Ctrl+P` | Navigate up through the input history. |
|
|
||||||
| `Ctrl+R` | Activate reverse command search history. |
|
|
||||||
| `Ctrl+Right Arrow` / `Meta+Right Arrow` / `Meta+F` | Move the cursor one word to the right. |
|
|
||||||
| `Ctrl+U` | Delete from the cursor to the beginning of the line. |
|
|
||||||
| `Ctrl+V` | Paste clipboard content. If the clipboard contains an image, it will be saved and a reference to it will be inserted in the prompt. |
|
|
||||||
| `Ctrl+W` / `Meta+Backspace` / `Ctrl+Backspace` | Delete the word to the left of the cursor. |
|
|
||||||
| `Ctrl+X` / `Meta+Enter` | Open the current input in an external editor. |
|
|
||||||
| `Ctrl+Z` | Undo last text edit. |
|
|
||||||
| `Ctrl+Shift+Z` | Redo last undone text edit. |
|
|
||||||
|
|
||||||
## Suggestions
|
| Action | Keys |
|
||||||
|
| ----------------------------------------- | ---------------------- |
|
||||||
|
| Move the cursor to the start of the line. | `Ctrl + A`<br />`Home` |
|
||||||
|
| Move the cursor to the end of the line. | `Ctrl + E`<br />`End` |
|
||||||
|
|
||||||
| Shortcut | Description |
|
#### Editing
|
||||||
| ----------------------- | -------------------------------------- |
|
|
||||||
| `Down Arrow` / `Ctrl+N` | Navigate down through the suggestions. |
|
|
||||||
| `Tab` / `Enter` | Accept the selected suggestion. |
|
|
||||||
| `Up Arrow` / `Ctrl+P` | Navigate up through the suggestions. |
|
|
||||||
|
|
||||||
## Radio Button Select
|
| Action | Keys |
|
||||||
|
| ------------------------------------------------ | ----------------------------------------- |
|
||||||
|
| Delete from the cursor to the end of the line. | `Ctrl + K` |
|
||||||
|
| Delete from the cursor to the start of the line. | `Ctrl + U` |
|
||||||
|
| Clear all text in the input field. | `Ctrl + C` |
|
||||||
|
| Delete the previous word. | `Ctrl + Backspace`<br />`Cmd + Backspace` |
|
||||||
|
|
||||||
| Shortcut | Description |
|
#### Screen Control
|
||||||
| ------------------ | ------------------------------------------------------------------------------------------------------------- |
|
|
||||||
| `Down Arrow` / `j` | Move selection down. |
|
|
||||||
| `Enter` | Confirm selection. |
|
|
||||||
| `Up Arrow` / `k` | Move selection up. |
|
|
||||||
| `1-9` | Select an item by its number. |
|
|
||||||
| (multi-digit) | For items with numbers greater than 9, press the digits in quick succession to select the corresponding item. |
|
|
||||||
|
|
||||||
## IDE Integration
|
| Action | Keys |
|
||||||
|
| -------------------------------------------- | ---------- |
|
||||||
|
| Clear the terminal screen and redraw the UI. | `Ctrl + L` |
|
||||||
|
|
||||||
| Shortcut | Description |
|
#### History & Search
|
||||||
| -------- | --------------------------------- |
|
|
||||||
| `Ctrl+G` | See context CLI received from IDE |
|
|
||||||
|
|
||||||
## Meta+key combos on mac
|
| Action | Keys |
|
||||||
|
| -------------------------------------------- | --------------------- |
|
||||||
|
| Show the previous entry in history. | `Ctrl + P (no Shift)` |
|
||||||
|
| Show the next entry in history. | `Ctrl + N (no Shift)` |
|
||||||
|
| Start reverse search through history. | `Ctrl + R` |
|
||||||
|
| Insert the selected reverse-search match. | `Enter (no Ctrl)` |
|
||||||
|
| Accept a suggestion while reverse searching. | `Tab` |
|
||||||
|
|
||||||
On Mac, all Meta+char combos should work normally except for these three which
|
#### Navigation
|
||||||
are mapped to special functionality.
|
|
||||||
|
|
||||||
- `meta+b`: "∫" back one word
|
| Action | Keys |
|
||||||
- `meta+f`: "ƒ" forward one word
|
| -------------------------------- | ------------------------------------------- |
|
||||||
- `meta+m`: "µ" toggle markup view
|
| Move selection up in lists. | `Up Arrow (no Shift)` |
|
||||||
|
| Move selection down in lists. | `Down Arrow (no Shift)` |
|
||||||
|
| Move up within dialog options. | `Up Arrow (no Shift)`<br />`K (no Shift)` |
|
||||||
|
| Move down within dialog options. | `Down Arrow (no Shift)`<br />`J (no Shift)` |
|
||||||
|
|
||||||
|
#### Suggestions & Completions
|
||||||
|
|
||||||
|
| Action | Keys |
|
||||||
|
| --------------------------------------- | -------------------------------------------------- |
|
||||||
|
| Accept the inline suggestion. | `Tab`<br />`Enter (no Ctrl)` |
|
||||||
|
| Move to the previous completion option. | `Up Arrow (no Shift)`<br />`Ctrl + P (no Shift)` |
|
||||||
|
| Move to the next completion option. | `Down Arrow (no Shift)`<br />`Ctrl + N (no Shift)` |
|
||||||
|
| Expand an inline suggestion. | `Right Arrow` |
|
||||||
|
| Collapse an inline suggestion. | `Left Arrow` |
|
||||||
|
|
||||||
|
#### Text Input
|
||||||
|
|
||||||
|
| Action | Keys |
|
||||||
|
| ------------------------------------ | ------------------------------------------------------------------------------------------- |
|
||||||
|
| Submit the current prompt. | `Enter (no Ctrl, no Shift, no Cmd, not Paste)` |
|
||||||
|
| Insert a newline without submitting. | `Ctrl + Enter`<br />`Cmd + Enter`<br />`Paste + Enter`<br />`Shift + Enter`<br />`Ctrl + J` |
|
||||||
|
|
||||||
|
#### External Tools
|
||||||
|
|
||||||
|
| Action | Keys |
|
||||||
|
| ---------------------------------------------- | ---------- |
|
||||||
|
| Open the current prompt in an external editor. | `Ctrl + X` |
|
||||||
|
| Paste an image from the clipboard. | `Ctrl + V` |
|
||||||
|
|
||||||
|
#### App Controls
|
||||||
|
|
||||||
|
| Action | Keys |
|
||||||
|
| ----------------------------------------------------------------- | ---------- |
|
||||||
|
| Toggle detailed error information. | `F12` |
|
||||||
|
| Toggle the full TODO list. | `Ctrl + T` |
|
||||||
|
| Toggle IDE context details. | `Ctrl + G` |
|
||||||
|
| Toggle Markdown rendering. | `Cmd + M` |
|
||||||
|
| Toggle copy mode when the terminal is using the alternate buffer. | `Ctrl + S` |
|
||||||
|
| Expand a height-constrained response to show additional lines. | `Ctrl + S` |
|
||||||
|
| Toggle focus between the shell and Gemini input. | `Ctrl + F` |
|
||||||
|
|
||||||
|
#### Session Control
|
||||||
|
|
||||||
|
| Action | Keys |
|
||||||
|
| -------------------------------------------- | ---------- |
|
||||||
|
| Cancel the current request or quit the CLI. | `Ctrl + C` |
|
||||||
|
| Exit the CLI when the input buffer is empty. | `Ctrl + D` |
|
||||||
|
|
||||||
|
<!-- KEYBINDINGS-AUTOGEN:END -->
|
||||||
|
|
||||||
|
## Additional Context-Specific Shortcuts
|
||||||
|
|
||||||
|
- `Ctrl+Y`: Toggle YOLO (auto-approval) mode for tool calls.
|
||||||
|
- `Shift+Tab`: Toggle Auto Edit (auto-accept edits) mode.
|
||||||
|
- `Option+M` (macOS): Entering `µ` with Option+M also toggles Markdown
|
||||||
|
rendering, matching `Cmd+M`.
|
||||||
|
- `!` on an empty prompt: Enter or exit shell mode.
|
||||||
|
- `\` (at end of a line) + `Enter`: Insert a newline without leaving single-line
|
||||||
|
mode.
|
||||||
|
- `Ctrl+Delete` / `Meta+Delete`: Delete the word to the right of the cursor.
|
||||||
|
- `Ctrl+B` or `Left Arrow`: Move the cursor one character to the left while
|
||||||
|
editing text.
|
||||||
|
- `Ctrl+F` or `Right Arrow`: Move the cursor one character to the right; with an
|
||||||
|
embedded shell attached, `Ctrl+F` still toggles focus.
|
||||||
|
- `Ctrl+D` or `Delete`: Remove the character immediately to the right of the
|
||||||
|
cursor.
|
||||||
|
- `Ctrl+H` or `Backspace`: Remove the character immediately to the left of the
|
||||||
|
cursor.
|
||||||
|
- `Ctrl+Left Arrow` / `Meta+Left Arrow` / `Meta+B`: Move one word to the left.
|
||||||
|
- `Ctrl+Right Arrow` / `Meta+Right Arrow` / `Meta+F`: Move one word to the
|
||||||
|
right.
|
||||||
|
- `Ctrl+W`: Delete the word to the left of the cursor (in addition to
|
||||||
|
`Ctrl+Backspace` / `Cmd+Backspace`).
|
||||||
|
- `Ctrl+Z` / `Ctrl+Shift+Z`: Undo or redo the most recent text edit.
|
||||||
|
- `Meta+Enter`: Open the current input in an external editor (alias for
|
||||||
|
`Ctrl+X`).
|
||||||
|
- `Esc` pressed twice quickly: Clear the current input buffer.
|
||||||
|
- `Up Arrow` / `Down Arrow`: When the cursor is at the top or bottom of a
|
||||||
|
single-line input, navigate backward or forward through prompt history.
|
||||||
|
- `Number keys (1-9, multi-digit)` inside selection dialogs: Jump directly to
|
||||||
|
the numbered radio option and confirm when the full number is entered.
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
"predocs:settings": "npm run build --workspace @google/gemini-cli-core",
|
"predocs:settings": "npm run build --workspace @google/gemini-cli-core",
|
||||||
"schema:settings": "tsx ./scripts/generate-settings-schema.ts",
|
"schema:settings": "tsx ./scripts/generate-settings-schema.ts",
|
||||||
"docs:settings": "tsx ./scripts/generate-settings-doc.ts",
|
"docs:settings": "tsx ./scripts/generate-settings-doc.ts",
|
||||||
|
"docs:keybindings": "tsx ./scripts/generate-keybindings-doc.ts",
|
||||||
"build": "node scripts/build.js",
|
"build": "node scripts/build.js",
|
||||||
"build-and-start": "npm run build && npm run start",
|
"build-and-start": "npm run build && npm run start",
|
||||||
"build:vscode": "node scripts/build_vscode_companion.js",
|
"build:vscode": "node scripts/build_vscode_companion.js",
|
||||||
|
|||||||
@@ -6,7 +6,12 @@
|
|||||||
|
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import type { KeyBindingConfig } from './keyBindings.js';
|
import type { KeyBindingConfig } from './keyBindings.js';
|
||||||
import { Command, defaultKeyBindings } from './keyBindings.js';
|
import {
|
||||||
|
Command,
|
||||||
|
commandCategories,
|
||||||
|
commandDescriptions,
|
||||||
|
defaultKeyBindings,
|
||||||
|
} from './keyBindings.js';
|
||||||
|
|
||||||
describe('keyBindings config', () => {
|
describe('keyBindings config', () => {
|
||||||
describe('defaultKeyBindings', () => {
|
describe('defaultKeyBindings', () => {
|
||||||
@@ -16,6 +21,7 @@ describe('keyBindings config', () => {
|
|||||||
for (const command of commands) {
|
for (const command of commands) {
|
||||||
expect(defaultKeyBindings[command]).toBeDefined();
|
expect(defaultKeyBindings[command]).toBeDefined();
|
||||||
expect(Array.isArray(defaultKeyBindings[command])).toBe(true);
|
expect(Array.isArray(defaultKeyBindings[command])).toBe(true);
|
||||||
|
expect(defaultKeyBindings[command]?.length).toBeGreaterThan(0);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -78,4 +84,35 @@ describe('keyBindings config', () => {
|
|||||||
expect(defaultKeyBindings[Command.END]).toContainEqual({ key: 'end' });
|
expect(defaultKeyBindings[Command.END]).toContainEqual({ key: 'end' });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('command metadata', () => {
|
||||||
|
const commandValues = Object.values(Command);
|
||||||
|
|
||||||
|
it('has a description entry for every command', () => {
|
||||||
|
const describedCommands = Object.keys(commandDescriptions);
|
||||||
|
expect(describedCommands.sort()).toEqual([...commandValues].sort());
|
||||||
|
|
||||||
|
for (const command of commandValues) {
|
||||||
|
expect(typeof commandDescriptions[command]).toBe('string');
|
||||||
|
expect(commandDescriptions[command]?.trim()).not.toHaveLength(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('categorizes each command exactly once', () => {
|
||||||
|
const seen = new Set<Command>();
|
||||||
|
|
||||||
|
for (const category of commandCategories) {
|
||||||
|
expect(typeof category.title).toBe('string');
|
||||||
|
expect(Array.isArray(category.commands)).toBe(true);
|
||||||
|
|
||||||
|
for (const command of category.commands) {
|
||||||
|
expect(commandValues).toContain(command);
|
||||||
|
expect(seen.has(command)).toBe(false);
|
||||||
|
seen.add(command);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(seen.size).toBe(commandValues.length);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -199,3 +199,135 @@ export const defaultKeyBindings: KeyBindingConfig = {
|
|||||||
[Command.EXPAND_SUGGESTION]: [{ key: 'right' }],
|
[Command.EXPAND_SUGGESTION]: [{ key: 'right' }],
|
||||||
[Command.COLLAPSE_SUGGESTION]: [{ key: 'left' }],
|
[Command.COLLAPSE_SUGGESTION]: [{ key: 'left' }],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface CommandCategory {
|
||||||
|
readonly title: string;
|
||||||
|
readonly commands: readonly Command[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Presentation metadata for grouping commands in documentation or UI.
|
||||||
|
*/
|
||||||
|
export const commandCategories: readonly CommandCategory[] = [
|
||||||
|
{
|
||||||
|
title: 'Basic Controls',
|
||||||
|
commands: [Command.RETURN, Command.ESCAPE],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Cursor Movement',
|
||||||
|
commands: [Command.HOME, Command.END],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Editing',
|
||||||
|
commands: [
|
||||||
|
Command.KILL_LINE_RIGHT,
|
||||||
|
Command.KILL_LINE_LEFT,
|
||||||
|
Command.CLEAR_INPUT,
|
||||||
|
Command.DELETE_WORD_BACKWARD,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Screen Control',
|
||||||
|
commands: [Command.CLEAR_SCREEN],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'History & Search',
|
||||||
|
commands: [
|
||||||
|
Command.HISTORY_UP,
|
||||||
|
Command.HISTORY_DOWN,
|
||||||
|
Command.REVERSE_SEARCH,
|
||||||
|
Command.SUBMIT_REVERSE_SEARCH,
|
||||||
|
Command.ACCEPT_SUGGESTION_REVERSE_SEARCH,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Navigation',
|
||||||
|
commands: [
|
||||||
|
Command.NAVIGATION_UP,
|
||||||
|
Command.NAVIGATION_DOWN,
|
||||||
|
Command.DIALOG_NAVIGATION_UP,
|
||||||
|
Command.DIALOG_NAVIGATION_DOWN,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Suggestions & Completions',
|
||||||
|
commands: [
|
||||||
|
Command.ACCEPT_SUGGESTION,
|
||||||
|
Command.COMPLETION_UP,
|
||||||
|
Command.COMPLETION_DOWN,
|
||||||
|
Command.EXPAND_SUGGESTION,
|
||||||
|
Command.COLLAPSE_SUGGESTION,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Text Input',
|
||||||
|
commands: [Command.SUBMIT, Command.NEWLINE],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'External Tools',
|
||||||
|
commands: [Command.OPEN_EXTERNAL_EDITOR, Command.PASTE_CLIPBOARD_IMAGE],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'App Controls',
|
||||||
|
commands: [
|
||||||
|
Command.SHOW_ERROR_DETAILS,
|
||||||
|
Command.SHOW_FULL_TODOS,
|
||||||
|
Command.TOGGLE_IDE_CONTEXT_DETAIL,
|
||||||
|
Command.TOGGLE_MARKDOWN,
|
||||||
|
Command.TOGGLE_COPY_MODE,
|
||||||
|
Command.SHOW_MORE_LINES,
|
||||||
|
Command.TOGGLE_SHELL_INPUT_FOCUS,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Session Control',
|
||||||
|
commands: [Command.QUIT, Command.EXIT],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Human-readable descriptions for each command, used in docs/tooling.
|
||||||
|
*/
|
||||||
|
export const commandDescriptions: Readonly<Record<Command, string>> = {
|
||||||
|
[Command.RETURN]: 'Confirm the current selection or choice.',
|
||||||
|
[Command.ESCAPE]: 'Dismiss dialogs or cancel the current focus.',
|
||||||
|
[Command.HOME]: 'Move the cursor to the start of the line.',
|
||||||
|
[Command.END]: 'Move the cursor to the end of the line.',
|
||||||
|
[Command.KILL_LINE_RIGHT]: 'Delete from the cursor to the end of the line.',
|
||||||
|
[Command.KILL_LINE_LEFT]: 'Delete from the cursor to the start of the line.',
|
||||||
|
[Command.CLEAR_INPUT]: 'Clear all text in the input field.',
|
||||||
|
[Command.DELETE_WORD_BACKWARD]: 'Delete the previous word.',
|
||||||
|
[Command.CLEAR_SCREEN]: 'Clear the terminal screen and redraw the UI.',
|
||||||
|
[Command.HISTORY_UP]: 'Show the previous entry in history.',
|
||||||
|
[Command.HISTORY_DOWN]: 'Show the next entry in history.',
|
||||||
|
[Command.NAVIGATION_UP]: 'Move selection up in lists.',
|
||||||
|
[Command.NAVIGATION_DOWN]: 'Move selection down in lists.',
|
||||||
|
[Command.DIALOG_NAVIGATION_UP]: 'Move up within dialog options.',
|
||||||
|
[Command.DIALOG_NAVIGATION_DOWN]: 'Move down within dialog options.',
|
||||||
|
[Command.ACCEPT_SUGGESTION]: 'Accept the inline suggestion.',
|
||||||
|
[Command.COMPLETION_UP]: 'Move to the previous completion option.',
|
||||||
|
[Command.COMPLETION_DOWN]: 'Move to the next completion option.',
|
||||||
|
[Command.SUBMIT]: 'Submit the current prompt.',
|
||||||
|
[Command.NEWLINE]: 'Insert a newline without submitting.',
|
||||||
|
[Command.OPEN_EXTERNAL_EDITOR]:
|
||||||
|
'Open the current prompt in an external editor.',
|
||||||
|
[Command.PASTE_CLIPBOARD_IMAGE]: 'Paste an image from the clipboard.',
|
||||||
|
[Command.SHOW_ERROR_DETAILS]: 'Toggle detailed error information.',
|
||||||
|
[Command.SHOW_FULL_TODOS]: 'Toggle the full TODO list.',
|
||||||
|
[Command.TOGGLE_IDE_CONTEXT_DETAIL]: 'Toggle IDE context details.',
|
||||||
|
[Command.TOGGLE_MARKDOWN]: 'Toggle Markdown rendering.',
|
||||||
|
[Command.TOGGLE_COPY_MODE]:
|
||||||
|
'Toggle copy mode when the terminal is using the alternate buffer.',
|
||||||
|
[Command.QUIT]: 'Cancel the current request or quit the CLI.',
|
||||||
|
[Command.EXIT]: 'Exit the CLI when the input buffer is empty.',
|
||||||
|
[Command.SHOW_MORE_LINES]:
|
||||||
|
'Expand a height-constrained response to show additional lines.',
|
||||||
|
[Command.REVERSE_SEARCH]: 'Start reverse search through history.',
|
||||||
|
[Command.SUBMIT_REVERSE_SEARCH]: 'Insert the selected reverse-search match.',
|
||||||
|
[Command.ACCEPT_SUGGESTION_REVERSE_SEARCH]:
|
||||||
|
'Accept a suggestion while reverse searching.',
|
||||||
|
[Command.TOGGLE_SHELL_INPUT_FOCUS]:
|
||||||
|
'Toggle focus between the shell and Gemini input.',
|
||||||
|
[Command.EXPAND_SUGGESTION]: 'Expand an inline suggestion.',
|
||||||
|
[Command.COLLAPSE_SUGGESTION]: 'Collapse an inline suggestion.',
|
||||||
|
};
|
||||||
|
|||||||
220
scripts/generate-keybindings-doc.ts
Normal file
220
scripts/generate-keybindings-doc.ts
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import path from 'node:path';
|
||||||
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||||
|
import { readFile, writeFile } from 'node:fs/promises';
|
||||||
|
|
||||||
|
import type { KeyBinding } from '../packages/cli/src/config/keyBindings.js';
|
||||||
|
import {
|
||||||
|
commandCategories,
|
||||||
|
commandDescriptions,
|
||||||
|
defaultKeyBindings,
|
||||||
|
} from '../packages/cli/src/config/keyBindings.js';
|
||||||
|
import {
|
||||||
|
formatWithPrettier,
|
||||||
|
injectBetweenMarkers,
|
||||||
|
normalizeForCompare,
|
||||||
|
} from './utils/autogen.js';
|
||||||
|
|
||||||
|
const START_MARKER = '<!-- KEYBINDINGS-AUTOGEN:START -->';
|
||||||
|
const END_MARKER = '<!-- KEYBINDINGS-AUTOGEN:END -->';
|
||||||
|
const OUTPUT_RELATIVE_PATH = ['docs', 'cli', 'keyboard-shortcuts.md'];
|
||||||
|
|
||||||
|
const KEY_NAME_OVERRIDES: Record<string, string> = {
|
||||||
|
return: 'Enter',
|
||||||
|
escape: 'Esc',
|
||||||
|
tab: 'Tab',
|
||||||
|
backspace: 'Backspace',
|
||||||
|
delete: 'Delete',
|
||||||
|
up: 'Up Arrow',
|
||||||
|
down: 'Down Arrow',
|
||||||
|
left: 'Left Arrow',
|
||||||
|
right: 'Right Arrow',
|
||||||
|
home: 'Home',
|
||||||
|
end: 'End',
|
||||||
|
pageup: 'Page Up',
|
||||||
|
pagedown: 'Page Down',
|
||||||
|
clear: 'Clear',
|
||||||
|
insert: 'Insert',
|
||||||
|
f1: 'F1',
|
||||||
|
f2: 'F2',
|
||||||
|
f3: 'F3',
|
||||||
|
f4: 'F4',
|
||||||
|
f5: 'F5',
|
||||||
|
f6: 'F6',
|
||||||
|
f7: 'F7',
|
||||||
|
f8: 'F8',
|
||||||
|
f9: 'F9',
|
||||||
|
f10: 'F10',
|
||||||
|
f11: 'F11',
|
||||||
|
f12: 'F12',
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface KeybindingDocCommand {
|
||||||
|
description: string;
|
||||||
|
bindings: readonly KeyBinding[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KeybindingDocSection {
|
||||||
|
title: string;
|
||||||
|
commands: readonly KeybindingDocCommand[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function main(argv = process.argv.slice(2)) {
|
||||||
|
const checkOnly = argv.includes('--check');
|
||||||
|
|
||||||
|
const repoRoot = path.resolve(
|
||||||
|
path.dirname(fileURLToPath(import.meta.url)),
|
||||||
|
'..',
|
||||||
|
);
|
||||||
|
const docPath = path.join(repoRoot, ...OUTPUT_RELATIVE_PATH);
|
||||||
|
|
||||||
|
const sections = buildDefaultDocSections();
|
||||||
|
const generatedBlock = renderDocumentation(sections);
|
||||||
|
const currentDoc = await readFile(docPath, 'utf8');
|
||||||
|
const injectedDoc = injectBetweenMarkers({
|
||||||
|
document: currentDoc,
|
||||||
|
startMarker: START_MARKER,
|
||||||
|
endMarker: END_MARKER,
|
||||||
|
newContent: generatedBlock,
|
||||||
|
paddingBefore: '\n\n',
|
||||||
|
paddingAfter: '\n',
|
||||||
|
});
|
||||||
|
const updatedDoc = await formatWithPrettier(injectedDoc, docPath);
|
||||||
|
|
||||||
|
if (normalizeForCompare(updatedDoc) === normalizeForCompare(currentDoc)) {
|
||||||
|
if (!checkOnly) {
|
||||||
|
console.log('Keybinding documentation already up to date.');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checkOnly) {
|
||||||
|
console.error(
|
||||||
|
'Keybinding documentation is out of date. Run `npm run docs:keybindings` to regenerate.',
|
||||||
|
);
|
||||||
|
process.exitCode = 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await writeFile(docPath, updatedDoc, 'utf8');
|
||||||
|
console.log('Keybinding documentation regenerated.');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildDefaultDocSections(): readonly KeybindingDocSection[] {
|
||||||
|
return commandCategories.map((category) => ({
|
||||||
|
title: category.title,
|
||||||
|
commands: category.commands.map((command) => ({
|
||||||
|
description: commandDescriptions[command],
|
||||||
|
bindings: defaultKeyBindings[command],
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderDocumentation(
|
||||||
|
sections: readonly KeybindingDocSection[],
|
||||||
|
): string {
|
||||||
|
const renderedSections = sections.map((section) => {
|
||||||
|
const rows = section.commands.map((command) => {
|
||||||
|
const formattedBindings = formatBindings(command.bindings);
|
||||||
|
const keysCell = formattedBindings.join('<br />');
|
||||||
|
return `| ${command.description} | ${keysCell} |`;
|
||||||
|
});
|
||||||
|
|
||||||
|
return [
|
||||||
|
`#### ${section.title}`,
|
||||||
|
'',
|
||||||
|
'| Action | Keys |',
|
||||||
|
'| --- | --- |',
|
||||||
|
...rows,
|
||||||
|
].join('\n');
|
||||||
|
});
|
||||||
|
|
||||||
|
return renderedSections.join('\n\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatBindings(bindings: readonly KeyBinding[]): string[] {
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const results: string[] = [];
|
||||||
|
|
||||||
|
for (const binding of bindings) {
|
||||||
|
const label = formatBinding(binding);
|
||||||
|
if (label && !seen.has(label)) {
|
||||||
|
seen.add(label);
|
||||||
|
results.push(label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatBinding(binding: KeyBinding): string {
|
||||||
|
const modifiers: string[] = [];
|
||||||
|
if (binding.ctrl) modifiers.push('Ctrl');
|
||||||
|
if (binding.command) modifiers.push('Cmd');
|
||||||
|
if (binding.shift) modifiers.push('Shift');
|
||||||
|
if (binding.paste) modifiers.push('Paste');
|
||||||
|
|
||||||
|
const keyName = binding.key
|
||||||
|
? formatKeyName(binding.key)
|
||||||
|
: binding.sequence
|
||||||
|
? formatSequence(binding.sequence)
|
||||||
|
: '';
|
||||||
|
|
||||||
|
if (!keyName) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const segments = [...modifiers, keyName].filter(Boolean);
|
||||||
|
let combo = segments.join(' + ');
|
||||||
|
|
||||||
|
const restrictions: string[] = [];
|
||||||
|
if (binding.ctrl === false) restrictions.push('no Ctrl');
|
||||||
|
if (binding.shift === false) restrictions.push('no Shift');
|
||||||
|
if (binding.command === false) restrictions.push('no Cmd');
|
||||||
|
if (binding.paste === false) restrictions.push('not Paste');
|
||||||
|
|
||||||
|
if (restrictions.length > 0) {
|
||||||
|
combo = `${combo} (${restrictions.join(', ')})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return combo ? `\`${combo}\`` : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatKeyName(key: string): string {
|
||||||
|
const normalized = key.toLowerCase();
|
||||||
|
if (KEY_NAME_OVERRIDES[normalized]) {
|
||||||
|
return KEY_NAME_OVERRIDES[normalized];
|
||||||
|
}
|
||||||
|
if (key.length === 1) {
|
||||||
|
return key.toUpperCase();
|
||||||
|
}
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatSequence(sequence: string): string {
|
||||||
|
if (sequence.length === 1) {
|
||||||
|
const code = sequence.charCodeAt(0);
|
||||||
|
if (code >= 1 && code <= 26) {
|
||||||
|
return String.fromCharCode(code + 64);
|
||||||
|
}
|
||||||
|
if (code === 10 || code === 13) {
|
||||||
|
return 'Enter';
|
||||||
|
}
|
||||||
|
if (code === 9) {
|
||||||
|
return 'Tab';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return JSON.stringify(sequence);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.argv[1]) {
|
||||||
|
const entryUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
||||||
|
if (entryUrl === import.meta.url) {
|
||||||
|
await main();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
escapeBackticks,
|
escapeBackticks,
|
||||||
formatDefaultValue,
|
formatDefaultValue,
|
||||||
formatWithPrettier,
|
formatWithPrettier,
|
||||||
|
injectBetweenMarkers,
|
||||||
normalizeForCompare,
|
normalizeForCompare,
|
||||||
} from './utils/autogen.js';
|
} from './utils/autogen.js';
|
||||||
|
|
||||||
@@ -52,21 +53,15 @@ export async function main(argv = process.argv.slice(2)) {
|
|||||||
const generatedBlock = renderSections(sections);
|
const generatedBlock = renderSections(sections);
|
||||||
|
|
||||||
const doc = await readFile(docPath, 'utf8');
|
const doc = await readFile(docPath, 'utf8');
|
||||||
const startIndex = doc.indexOf(START_MARKER);
|
const injectedDoc = injectBetweenMarkers({
|
||||||
const endIndex = doc.indexOf(END_MARKER);
|
document: doc,
|
||||||
|
startMarker: START_MARKER,
|
||||||
if (startIndex === -1 || endIndex === -1 || startIndex >= endIndex) {
|
endMarker: END_MARKER,
|
||||||
throw new Error(
|
newContent: generatedBlock,
|
||||||
`Could not locate documentation markers (${START_MARKER}, ${END_MARKER}).`,
|
paddingBefore: '\n',
|
||||||
);
|
paddingAfter: '\n',
|
||||||
}
|
});
|
||||||
|
const formattedDoc = await formatWithPrettier(injectedDoc, docPath);
|
||||||
const before = doc.slice(0, startIndex + START_MARKER.length);
|
|
||||||
const after = doc.slice(endIndex);
|
|
||||||
const formattedDoc = await formatWithPrettier(
|
|
||||||
`${before}\n${generatedBlock}\n${after}`,
|
|
||||||
docPath,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (normalizeForCompare(doc) === normalizeForCompare(formattedDoc)) {
|
if (normalizeForCompare(doc) === normalizeForCompare(formattedDoc)) {
|
||||||
if (!checkOnly) {
|
if (!checkOnly) {
|
||||||
|
|||||||
68
scripts/tests/generate-keybindings-doc.test.ts
Normal file
68
scripts/tests/generate-keybindings-doc.test.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import {
|
||||||
|
main as generateKeybindingDocs,
|
||||||
|
renderDocumentation,
|
||||||
|
type KeybindingDocSection,
|
||||||
|
} from '../generate-keybindings-doc.ts';
|
||||||
|
|
||||||
|
describe('generate-keybindings-doc', () => {
|
||||||
|
it('keeps keyboard shortcut documentation in sync in check mode', async () => {
|
||||||
|
const previousExitCode = process.exitCode;
|
||||||
|
try {
|
||||||
|
process.exitCode = 0;
|
||||||
|
await expect(
|
||||||
|
generateKeybindingDocs(['--check']),
|
||||||
|
).resolves.toBeUndefined();
|
||||||
|
expect(process.exitCode).toBe(0);
|
||||||
|
} finally {
|
||||||
|
process.exitCode = previousExitCode;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders provided sections into markdown tables', () => {
|
||||||
|
const sections: KeybindingDocSection[] = [
|
||||||
|
{
|
||||||
|
title: 'Custom Controls',
|
||||||
|
commands: [
|
||||||
|
{
|
||||||
|
description: 'Trigger custom action.',
|
||||||
|
bindings: [{ key: 'x', ctrl: true }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'Submit with Enter if no modifiers are held.',
|
||||||
|
bindings: [{ key: 'return', ctrl: false, shift: false }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Navigation',
|
||||||
|
commands: [
|
||||||
|
{
|
||||||
|
description: 'Move up through results.',
|
||||||
|
bindings: [
|
||||||
|
{ key: 'up', shift: false },
|
||||||
|
{ key: 'p', ctrl: true, shift: false },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const markdown = renderDocumentation(sections);
|
||||||
|
expect(markdown).toContain('#### Custom Controls');
|
||||||
|
expect(markdown).toContain('Trigger custom action.');
|
||||||
|
expect(markdown).toContain('`Ctrl + X`');
|
||||||
|
expect(markdown).toContain('Submit with Enter if no modifiers are held.');
|
||||||
|
expect(markdown).toContain('`Enter (no Ctrl, no Shift)`');
|
||||||
|
expect(markdown).toContain('#### Navigation');
|
||||||
|
expect(markdown).toContain('Move up through results.');
|
||||||
|
expect(markdown).toContain('`Up Arrow (no Shift)`');
|
||||||
|
expect(markdown).toContain('`Ctrl + P (no Shift)`');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -10,7 +10,12 @@ import { main as generateDocs } from '../generate-settings-doc.ts';
|
|||||||
describe('generate-settings-doc', () => {
|
describe('generate-settings-doc', () => {
|
||||||
it('keeps documentation in sync in check mode', async () => {
|
it('keeps documentation in sync in check mode', async () => {
|
||||||
const previousExitCode = process.exitCode;
|
const previousExitCode = process.exitCode;
|
||||||
await expect(generateDocs(['--check'])).resolves.toBeUndefined();
|
try {
|
||||||
expect(process.exitCode).toBe(previousExitCode);
|
process.exitCode = 0;
|
||||||
|
await expect(generateDocs(['--check'])).resolves.toBeUndefined();
|
||||||
|
expect(process.exitCode).toBe(0);
|
||||||
|
} finally {
|
||||||
|
process.exitCode = previousExitCode;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -81,3 +81,39 @@ export function formatDefaultValue(
|
|||||||
return String(value);
|
return String(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface MarkerInsertionOptions {
|
||||||
|
document: string;
|
||||||
|
startMarker: string;
|
||||||
|
endMarker: string;
|
||||||
|
newContent: string;
|
||||||
|
paddingBefore?: string;
|
||||||
|
paddingAfter?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces the content between two markers with `newContent`, preserving the
|
||||||
|
* original document outside the markers and applying optional padding.
|
||||||
|
*/
|
||||||
|
export function injectBetweenMarkers({
|
||||||
|
document,
|
||||||
|
startMarker,
|
||||||
|
endMarker,
|
||||||
|
newContent,
|
||||||
|
paddingBefore = '\n',
|
||||||
|
paddingAfter = '\n',
|
||||||
|
}: MarkerInsertionOptions): string {
|
||||||
|
const startIndex = document.indexOf(startMarker);
|
||||||
|
const endIndex = document.indexOf(endMarker);
|
||||||
|
|
||||||
|
if (startIndex === -1 || endIndex === -1 || startIndex >= endIndex) {
|
||||||
|
throw new Error(
|
||||||
|
`Could not locate documentation markers (${startMarker}, ${endMarker}).`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const before = document.slice(0, startIndex + startMarker.length);
|
||||||
|
const after = document.slice(endIndex);
|
||||||
|
|
||||||
|
return `${before}${paddingBefore}${newContent}${paddingAfter}${after}`;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user