/** * @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/ui/key/keyBindings.js'; import { commandCategories, commandDescriptions, defaultKeyBindingConfig, Command, getPlatformUndoBindings, getPlatformRedoBindings, } from '../packages/cli/src/ui/key/keyBindings.js'; import { formatWithPrettier, injectBetweenMarkers, normalizeForCompare, } from './utils/autogen.js'; const START_MARKER = ''; const END_MARKER = ''; const OUTPUT_RELATIVE_PATH = ['docs', 'reference', 'keyboard-shortcuts.md']; import { formatKeyBinding } from '../packages/cli/src/ui/key/keybindingUtils.js'; export interface KeybindingDocCommand { command: string; 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) => { // For UNDO and REDO, we want to show all platform variants in the docs if (command === Command.UNDO) { return { command: command, description: commandDescriptions[command], bindings: getMergedPlatformBindings(getPlatformUndoBindings), }; } if (command === Command.REDO) { return { command: command, description: commandDescriptions[command], bindings: getMergedPlatformBindings(getPlatformRedoBindings), }; } return { command: command, description: commandDescriptions[command], bindings: defaultKeyBindingConfig.get(command) ?? [], }; }), })); } function getMergedPlatformBindings( getBindings: (platform: string) => readonly KeyBinding[], ): readonly KeyBinding[] { const win32 = getBindings('win32'); const darwin = getBindings('darwin'); const linux = getBindings('linux'); const all = [...win32, ...darwin, ...linux]; const seen = new Set(); const unique: KeyBinding[] = []; for (const b of all) { const key = `${b.name}-${b.ctrl}-${b.shift}-${b.alt}-${b.cmd}`; if (!seen.has(key)) { seen.add(key); unique.push(b); } } return unique; } 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('
'); return `| \`${command.command}\` | ${command.description} | ${keysCell} |`; }); return [ `#### ${section.title}`, '', '| Command | Action | Keys |', '| --- | --- | --- |', ...rows, ].join('\n'); }); return renderedSections.join('\n\n'); } function formatBindings(bindings: readonly KeyBinding[]): string[] { const seen = new Set(); const results: string[] = []; for (const binding of bindings) { const label = formatKeyBinding(binding, 'default'); if (label && !seen.has(label)) { seen.add(label); results.push(`\`${label}\``); } } return results; } if (process.argv[1]) { const entryUrl = pathToFileURL(path.resolve(process.argv[1])).href; if (entryUrl === import.meta.url) { await main(); } }