Keybindings API
The Keybindings API provides comprehensive functionality for managing keyboard shortcuts, custom key combinations, and input handling within the development environment.
Overview
The Keybindings API enables you to:
- Register custom keyboard shortcuts
- Override default keybindings
- Handle complex key combinations
- Manage context-sensitive shortcuts
- Provide keybinding conflict resolution
- Support platform-specific bindings
- Implement keybinding recording and playback
- Handle international keyboard layouts
Basic Usage
Keybinding Registration
typescript
import { TraeAPI } from '@trae/api';
// Keybinding manager implementation
class KeybindingManager {
private keybindings: Map<string, KeybindingEntry> = new Map();
private contextKeys: Map<string, ContextKey> = new Map();
private keySequences: Map<string, KeySequence> = new Map();
private conflictResolver: ConflictResolver;
private recordingSession: RecordingSession | null = null;
constructor() {
this.conflictResolver = new ConflictResolver();
this.initializeDefaultKeybindings();
this.setupEventListeners();
this.loadUserKeybindings();
}
private initializeDefaultKeybindings(): void {
// Register default keybindings
const defaultBindings: KeybindingDefinition[] = [
{
key: 'ctrl+s',
command: 'workbench.action.files.save',
when: 'editorTextFocus',
description: 'Save file'
},
{
key: 'ctrl+shift+s',
command: 'workbench.action.files.saveAs',
when: 'editorTextFocus',
description: 'Save file as'
},
{
key: 'ctrl+o',
command: 'workbench.action.files.openFile',
description: 'Open file'
},
{
key: 'ctrl+n',
command: 'workbench.action.files.newUntitledFile',
description: 'New file'
},
{
key: 'ctrl+w',
command: 'workbench.action.closeActiveEditor',
when: 'editorIsOpen',
description: 'Close editor'
},
{
key: 'ctrl+shift+w',
command: 'workbench.action.closeAllEditors',
description: 'Close all editors'
},
{
key: 'ctrl+z',
command: 'undo',
when: 'editorTextFocus && !editorReadonly',
description: 'Undo'
},
{
key: 'ctrl+y',
command: 'redo',
when: 'editorTextFocus && !editorReadonly',
description: 'Redo'
},
{
key: 'ctrl+x',
command: 'editor.action.clipboardCutAction',
when: 'editorTextFocus && !editorReadonly',
description: 'Cut'
},
{
key: 'ctrl+c',
command: 'editor.action.clipboardCopyAction',
when: 'editorTextFocus',
description: 'Copy'
},
{
key: 'ctrl+v',
command: 'editor.action.clipboardPasteAction',
when: 'editorTextFocus && !editorReadonly',
description: 'Paste'
},
{
key: 'ctrl+a',
command: 'editor.action.selectAll',
when: 'editorTextFocus',
description: 'Select all'
},
{
key: 'ctrl+f',
command: 'actions.find',
when: 'editorFocus || editorIsOpen',
description: 'Find'
},
{
key: 'ctrl+h',
command: 'editor.action.startFindReplaceAction',
when: 'editorFocus || editorIsOpen',
description: 'Replace'
},
{
key: 'ctrl+shift+f',
command: 'workbench.action.findInFiles',
description: 'Find in files'
},
{
key: 'ctrl+shift+h',
command: 'workbench.action.replaceInFiles',
description: 'Replace in files'
},
{
key: 'ctrl+g',
command: 'workbench.action.gotoLine',
when: 'editorFocus',
description: 'Go to line'
},
{
key: 'ctrl+shift+p',
command: 'workbench.action.showCommands',
description: 'Show command palette'
},
{
key: 'ctrl+p',
command: 'workbench.action.quickOpen',
description: 'Quick open'
},
{
key: 'ctrl+shift+e',
command: 'workbench.view.explorer',
description: 'Show explorer'
},
{
key: 'ctrl+shift+g',
command: 'workbench.view.scm',
description: 'Show source control'
},
{
key: 'ctrl+shift+d',
command: 'workbench.view.debug',
description: 'Show debug'
},
{
key: 'ctrl+shift+x',
command: 'workbench.view.extensions',
description: 'Show extensions'
},
{
key: 'ctrl+`',
command: 'workbench.action.terminal.toggleTerminal',
description: 'Toggle terminal'
},
{
key: 'ctrl+shift+`',
command: 'workbench.action.terminal.new',
description: 'New terminal'
},
{
key: 'f5',
command: 'workbench.action.debug.start',
when: 'debuggersAvailable && !inDebugMode',
description: 'Start debugging'
},
{
key: 'shift+f5',
command: 'workbench.action.debug.stop',
when: 'inDebugMode',
description: 'Stop debugging'
},
{
key: 'f9',
command: 'editor.debug.action.toggleBreakpoint',
when: 'debuggersAvailable && editorTextFocus',
description: 'Toggle breakpoint'
},
{
key: 'f10',
command: 'workbench.action.debug.stepOver',
when: 'debugState == "stopped"',
description: 'Step over'
},
{
key: 'f11',
command: 'workbench.action.debug.stepInto',
when: 'debugState == "stopped"',
description: 'Step into'
},
{
key: 'shift+f11',
command: 'workbench.action.debug.stepOut',
when: 'debugState == "stopped"',
description: 'Step out'
},
{
key: 'ctrl+k ctrl+c',
command: 'editor.action.addCommentLine',
when: 'editorTextFocus && !editorReadonly',
description: 'Add line comment'
},
{
key: 'ctrl+k ctrl+u',
command: 'editor.action.removeCommentLine',
when: 'editorTextFocus && !editorReadonly',
description: 'Remove line comment'
},
{
key: 'ctrl+/',
command: 'editor.action.commentLine',
when: 'editorTextFocus && !editorReadonly',
description: 'Toggle line comment'
},
{
key: 'shift+alt+a',
command: 'editor.action.blockComment',
when: 'editorTextFocus && !editorReadonly',
description: 'Toggle block comment'
},
{
key: 'ctrl+shift+k',
command: 'editor.action.deleteLines',
when: 'editorTextFocus && !editorReadonly',
description: 'Delete line'
},
{
key: 'ctrl+enter',
command: 'editor.action.insertLineAfter',
when: 'editorTextFocus && !editorReadonly',
description: 'Insert line below'
},
{
key: 'ctrl+shift+enter',
command: 'editor.action.insertLineBefore',
when: 'editorTextFocus && !editorReadonly',
description: 'Insert line above'
},
{
key: 'alt+up',
command: 'editor.action.moveLinesUpAction',
when: 'editorTextFocus && !editorReadonly',
description: 'Move line up'
},
{
key: 'alt+down',
command: 'editor.action.moveLinesDownAction',
when: 'editorTextFocus && !editorReadonly',
description: 'Move line down'
},
{
key: 'shift+alt+up',
command: 'editor.action.copyLinesUpAction',
when: 'editorTextFocus && !editorReadonly',
description: 'Copy line up'
},
{
key: 'shift+alt+down',
command: 'editor.action.copyLinesDownAction',
when: 'editorTextFocus && !editorReadonly',
description: 'Copy line down'
},
{
key: 'ctrl+d',
command: 'editor.action.addSelectionToNextFindMatch',
when: 'editorFocus',
description: 'Add selection to next find match'
},
{
key: 'ctrl+k ctrl+d',
command: 'editor.action.moveSelectionToNextFindMatch',
when: 'editorFocus',
description: 'Move last selection to next find match'
},
{
key: 'ctrl+u',
command: 'cursorUndo',
when: 'editorTextFocus',
description: 'Cursor undo'
},
{
key: 'shift+alt+i',
command: 'editor.action.insertCursorAtEndOfEachLineSelected',
when: 'editorTextFocus',
description: 'Insert cursor at end of each line selected'
},
{
key: 'ctrl+shift+l',
command: 'editor.action.selectHighlights',
when: 'editorFocus',
description: 'Select all occurrences of find match'
},
{
key: 'ctrl+f2',
command: 'editor.action.changeAll',
when: 'editorTextFocus && !editorReadonly',
description: 'Change all occurrences'
},
{
key: 'ctrl+i',
command: 'editor.action.triggerSuggest',
when: 'editorHasCompletionItemProvider && editorTextFocus && !editorReadonly',
description: 'Trigger suggest'
},
{
key: 'ctrl+space',
command: 'editor.action.triggerSuggest',
when: 'editorHasCompletionItemProvider && editorTextFocus && !editorReadonly',
description: 'Trigger suggest'
},
{
key: 'ctrl+shift+space',
command: 'editor.action.triggerParameterHints',
when: 'editorHasSignatureHelpProvider && editorTextFocus',
description: 'Trigger parameter hints'
},
{
key: 'shift+alt+f',
command: 'editor.action.formatDocument',
when: 'editorHasDocumentFormattingProvider && editorTextFocus && !editorReadonly',
description: 'Format document'
},
{
key: 'ctrl+k ctrl+f',
command: 'editor.action.formatSelection',
when: 'editorHasDocumentSelectionFormattingProvider && editorTextFocus && !editorReadonly',
description: 'Format selection'
},
{
key: 'f12',
command: 'editor.action.revealDefinition',
when: 'editorHasDefinitionProvider && editorTextFocus',
description: 'Go to definition'
},
{
key: 'alt+f12',
command: 'editor.action.peekDefinition',
when: 'editorHasDefinitionProvider && editorTextFocus',
description: 'Peek definition'
},
{
key: 'ctrl+k f12',
command: 'editor.action.revealDefinitionAside',
when: 'editorHasDefinitionProvider && editorTextFocus',
description: 'Open definition to the side'
},
{
key: 'ctrl+f12',
command: 'editor.action.goToImplementation',
when: 'editorHasImplementationProvider && editorTextFocus',
description: 'Go to implementation'
},
{
key: 'ctrl+shift+f12',
command: 'editor.action.peekImplementation',
when: 'editorHasImplementationProvider && editorTextFocus',
description: 'Peek implementation'
},
{
key: 'shift+f12',
command: 'editor.action.goToReferences',
when: 'editorHasReferenceProvider && editorTextFocus',
description: 'Go to references'
},
{
key: 'ctrl+k ctrl+q',
command: 'editor.action.quickFix',
when: 'editorHasCodeActionsProvider && editorTextFocus',
description: 'Quick fix'
},
{
key: 'f2',
command: 'editor.action.rename',
when: 'editorHasRenameProvider && editorTextFocus',
description: 'Rename symbol'
},
{
key: 'ctrl+k ctrl+x',
command: 'editor.action.trimTrailingWhitespace',
when: 'editorTextFocus && !editorReadonly',
description: 'Trim trailing whitespace'
},
{
key: 'ctrl+k m',
command: 'workbench.action.editor.changeLanguageMode',
when: 'editorTextFocus',
description: 'Change language mode'
},
{
key: 'ctrl+k r',
command: 'workbench.action.files.revealActiveFileInWindows',
when: 'editorFocus',
description: 'Reveal active file in explorer'
},
{
key: 'ctrl+k o',
command: 'workbench.action.files.showOpenedFileInNewWindow',
when: 'editorFocus',
description: 'Show opened file in new window'
},
{
key: 'ctrl+k v',
command: 'workbench.action.markdown.openPreviewToTheSide',
when: 'editorFocus && editorLangId == "markdown"',
description: 'Open markdown preview to the side'
}
];
// Register default bindings
defaultBindings.forEach(binding => {
this.registerKeybinding(binding, 'default');
});
console.log(`Registered ${defaultBindings.length} default keybindings`);
}
private setupEventListeners(): void {
// Listen for keyboard events
document.addEventListener('keydown', this.handleKeyDown.bind(this), true);
document.addEventListener('keyup', this.handleKeyUp.bind(this), true);
// Listen for context changes
TraeAPI.window.onDidChangeActiveTextEditor(editor => {
this.updateContext('editorTextFocus', !!editor);
this.updateContext('editorIsOpen', !!editor);
this.updateContext('editorReadonly', editor?.document.isReadonly || false);
this.updateContext('editorLangId', editor?.document.languageId || '');
});
// Listen for debug state changes
TraeAPI.debug.onDidChangeActiveDebugSession(session => {
this.updateContext('inDebugMode', !!session);
this.updateContext('debugState', session?.state || 'inactive');
});
// Listen for extension changes
TraeAPI.extensions.onDidChange(() => {
this.updateContext('debuggersAvailable', this.hasDebuggersAvailable());
});
}
private async loadUserKeybindings(): Promise<void> {
try {
// Load user keybindings from settings
const userBindings = TraeAPI.workspace.getConfiguration('keybindings').get<KeybindingDefinition[]>('user', []);
userBindings.forEach(binding => {
this.registerKeybinding(binding, 'user');
});
console.log(`Loaded ${userBindings.length} user keybindings`);
} catch (error) {
console.error('Failed to load user keybindings:', error);
}
}
// Core keybinding operations
registerKeybinding(definition: KeybindingDefinition, source: KeybindingSource = 'extension'): string {
try {
const id = this.generateKeybindingId(definition);
const entry: KeybindingEntry = {
id,
definition,
source,
enabled: true,
registeredAt: Date.now()
};
// Check for conflicts
const conflicts = this.findConflicts(definition);
if (conflicts.length > 0) {
const resolution = this.conflictResolver.resolve(entry, conflicts);
if (!resolution.allow) {
throw new Error(`Keybinding conflict: ${resolution.reason}`);
}
// Handle conflict resolution
if (resolution.disableConflicting) {
conflicts.forEach(conflict => {
this.disableKeybinding(conflict.id);
});
}
}
// Parse and validate key combination
const keyCombo = this.parseKeyCombo(definition.key);
if (!keyCombo) {
throw new Error(`Invalid key combination: ${definition.key}`);
}
entry.keyCombo = keyCombo;
this.keybindings.set(id, entry);
console.log(`Registered keybinding: ${definition.key} -> ${definition.command}`);
return id;
} catch (error) {
console.error(`Failed to register keybinding ${definition.key}:`, error);
throw error;
}
}
unregisterKeybinding(id: string): boolean {
try {
const entry = this.keybindings.get(id);
if (!entry) {
return false;
}
this.keybindings.delete(id);
console.log(`Unregistered keybinding: ${entry.definition.key}`);
return true;
} catch (error) {
console.error(`Failed to unregister keybinding ${id}:`, error);
return false;
}
}
enableKeybinding(id: string): boolean {
const entry = this.keybindings.get(id);
if (entry) {
entry.enabled = true;
return true;
}
return false;
}
disableKeybinding(id: string): boolean {
const entry = this.keybindings.get(id);
if (entry) {
entry.enabled = false;
return true;
}
return false;
}
updateKeybinding(id: string, newDefinition: Partial<KeybindingDefinition>): boolean {
try {
const entry = this.keybindings.get(id);
if (!entry) {
return false;
}
// Create updated definition
const updatedDefinition = { ...entry.definition, ...newDefinition };
// Validate new key combination if changed
if (newDefinition.key && newDefinition.key !== entry.definition.key) {
const keyCombo = this.parseKeyCombo(newDefinition.key);
if (!keyCombo) {
throw new Error(`Invalid key combination: ${newDefinition.key}`);
}
entry.keyCombo = keyCombo;
}
entry.definition = updatedDefinition;
console.log(`Updated keybinding: ${id}`);
return true;
} catch (error) {
console.error(`Failed to update keybinding ${id}:`, error);
return false;
}
}
// Key combination parsing
private parseKeyCombo(keyString: string): KeyCombo | null {
try {
const parts = keyString.toLowerCase().split(/\s+/);
const chords: KeyChord[] = [];
for (const part of parts) {
const chord = this.parseKeyChord(part);
if (!chord) {
return null;
}
chords.push(chord);
}
return {
chords,
isSequence: chords.length > 1
};
} catch (error) {
console.error(`Failed to parse key combination ${keyString}:`, error);
return null;
}
}
private parseKeyChord(chordString: string): KeyChord | null {
const parts = chordString.split('+');
const modifiers: KeyModifier[] = [];
let key = '';
for (const part of parts) {
const normalizedPart = this.normalizeKeyName(part);
if (this.isModifier(normalizedPart)) {
modifiers.push(normalizedPart as KeyModifier);
} else {
if (key) {
// Multiple non-modifier keys not allowed
return null;
}
key = normalizedPart;
}
}
if (!key) {
return null;
}
return {
key,
modifiers: modifiers.sort(), // Sort for consistent comparison
ctrlKey: modifiers.includes('ctrl'),
altKey: modifiers.includes('alt'),
shiftKey: modifiers.includes('shift'),
metaKey: modifiers.includes('meta')
};
}
private normalizeKeyName(key: string): string {
const keyMap: { [key: string]: string } = {
'control': 'ctrl',
'command': 'meta',
'cmd': 'meta',
'option': 'alt',
'escape': 'esc',
'return': 'enter',
'space': ' ',
'plus': '+',
'minus': '-',
'equal': '=',
'backquote': '`',
'backslash': '\\',
'bracketleft': '[',
'bracketright': ']',
'semicolon': ';',
'quote': "'",
'comma': ',',
'period': '.',
'slash': '/'
};
return keyMap[key.toLowerCase()] || key.toLowerCase();
}
private isModifier(key: string): boolean {
return ['ctrl', 'alt', 'shift', 'meta'].includes(key);
}
// Event handling
private handleKeyDown(event: KeyboardEvent): void {
try {
// Skip if recording
if (this.recordingSession) {
this.recordingSession.recordKey(event);
return;
}
// Create current key chord
const currentChord = this.createKeyChordFromEvent(event);
if (!currentChord) {
return;
}
// Find matching keybindings
const matches = this.findMatchingKeybindings(currentChord);
if (matches.length === 0) {
return;
}
// Handle single chord matches
const singleChordMatches = matches.filter(match => !match.keyCombo.isSequence);
if (singleChordMatches.length > 0) {
const bestMatch = this.selectBestMatch(singleChordMatches);
if (bestMatch && this.evaluateWhenCondition(bestMatch.definition.when)) {
event.preventDefault();
event.stopPropagation();
this.executeKeybinding(bestMatch);
return;
}
}
// Handle sequence matches
const sequenceMatches = matches.filter(match => match.keyCombo.isSequence);
if (sequenceMatches.length > 0) {
this.handleKeySequence(currentChord, sequenceMatches, event);
}
} catch (error) {
console.error('Error handling key down:', error);
}
}
private handleKeyUp(event: KeyboardEvent): void {
// Handle key up events if needed
}
private createKeyChordFromEvent(event: KeyboardEvent): KeyChord | null {
const key = this.normalizeKeyName(event.key);
// Skip modifier-only keys
if (this.isModifier(key)) {
return null;
}
const modifiers: KeyModifier[] = [];
if (event.ctrlKey) modifiers.push('ctrl');
if (event.altKey) modifiers.push('alt');
if (event.shiftKey && !this.isShiftableKey(key)) modifiers.push('shift');
if (event.metaKey) modifiers.push('meta');
return {
key,
modifiers: modifiers.sort(),
ctrlKey: event.ctrlKey,
altKey: event.altKey,
shiftKey: event.shiftKey,
metaKey: event.metaKey
};
}
private isShiftableKey(key: string): boolean {
// Keys that naturally use shift (uppercase letters, symbols)
return /^[A-Z!@#$%^&*()_+{}|:"<>?]$/.test(key);
}
private findMatchingKeybindings(chord: KeyChord): KeybindingEntry[] {
const matches: KeybindingEntry[] = [];
for (const entry of this.keybindings.values()) {
if (!entry.enabled || !entry.keyCombo) {
continue;
}
// Check if first chord matches
const firstChord = entry.keyCombo.chords[0];
if (this.chordsMatch(chord, firstChord)) {
matches.push(entry);
}
}
return matches;
}
private chordsMatch(chord1: KeyChord, chord2: KeyChord): boolean {
return (
chord1.key === chord2.key &&
chord1.ctrlKey === chord2.ctrlKey &&
chord1.altKey === chord2.altKey &&
chord1.shiftKey === chord2.shiftKey &&
chord1.metaKey === chord2.metaKey
);
}
private selectBestMatch(matches: KeybindingEntry[]): KeybindingEntry | null {
if (matches.length === 0) {
return null;
}
// Sort by priority: user > extension > default
const priorityOrder = { user: 3, extension: 2, default: 1 };
matches.sort((a, b) => {
const aPriority = priorityOrder[a.source] || 0;
const bPriority = priorityOrder[b.source] || 0;
if (aPriority !== bPriority) {
return bPriority - aPriority;
}
// If same priority, prefer more recent
return b.registeredAt - a.registeredAt;
});
return matches[0];
}
private handleKeySequence(chord: KeyChord, matches: KeybindingEntry[], event: KeyboardEvent): void {
// Implementation for handling key sequences
// This would involve tracking partial matches and waiting for subsequent keys
console.log('Handling key sequence:', chord, matches);
}
// Context evaluation
private evaluateWhenCondition(when?: string): boolean {
if (!when) {
return true;
}
try {
return this.parseAndEvaluateCondition(when);
} catch (error) {
console.error(`Error evaluating when condition "${when}":`, error);
return false;
}
}
private parseAndEvaluateCondition(condition: string): boolean {
// Simple condition parser
// Supports: &&, ||, !, parentheses, context keys
// Remove whitespace
condition = condition.replace(/\s+/g, '');
// Handle parentheses
while (condition.includes('(')) {
const start = condition.lastIndexOf('(');
const end = condition.indexOf(')', start);
if (end === -1) {
throw new Error('Unmatched parentheses');
}
const subCondition = condition.substring(start + 1, end);
const result = this.parseAndEvaluateCondition(subCondition);
condition = condition.substring(0, start) + result.toString() + condition.substring(end + 1);
}
// Handle OR operators
if (condition.includes('||')) {
const parts = condition.split('||');
return parts.some(part => this.parseAndEvaluateCondition(part));
}
// Handle AND operators
if (condition.includes('&&')) {
const parts = condition.split('&&');
return parts.every(part => this.parseAndEvaluateCondition(part));
}
// Handle NOT operator
if (condition.startsWith('!')) {
return !this.parseAndEvaluateCondition(condition.substring(1));
}
// Handle boolean literals
if (condition === 'true') return true;
if (condition === 'false') return false;
// Handle context key
return this.getContextValue(condition);
}
private getContextValue(key: string): boolean {
const contextKey = this.contextKeys.get(key);
if (contextKey) {
return contextKey.get();
}
// Default context values
const defaultValues: { [key: string]: boolean } = {
'editorTextFocus': false,
'editorIsOpen': false,
'editorReadonly': false,
'inDebugMode': false,
'debuggersAvailable': false
};
return defaultValues[key] || false;
}
updateContext(key: string, value: boolean): void {
let contextKey = this.contextKeys.get(key);
if (!contextKey) {
contextKey = new ContextKey(key, value);
this.contextKeys.set(key, contextKey);
} else {
contextKey.set(value);
}
}
// Command execution
private async executeKeybinding(entry: KeybindingEntry): Promise<void> {
try {
console.log(`Executing keybinding: ${entry.definition.key} -> ${entry.definition.command}`);
// Execute the command
await TraeAPI.commands.executeCommand(entry.definition.command, entry.definition.args);
// Update usage statistics
entry.lastUsed = Date.now();
entry.usageCount = (entry.usageCount || 0) + 1;
} catch (error) {
console.error(`Failed to execute keybinding command ${entry.definition.command}:`, error);
TraeAPI.window.showErrorMessage(`Failed to execute command: ${entry.definition.command}`);
}
}
// Conflict resolution
private findConflicts(definition: KeybindingDefinition): KeybindingEntry[] {
const conflicts: KeybindingEntry[] = [];
for (const entry of this.keybindings.values()) {
if (!entry.enabled) {
continue;
}
if (entry.definition.key === definition.key) {
// Check if contexts overlap
if (this.contextsOverlap(entry.definition.when, definition.when)) {
conflicts.push(entry);
}
}
}
return conflicts;
}
private contextsOverlap(when1?: string, when2?: string): boolean {
// Simplified context overlap detection
// In a real implementation, this would be more sophisticated
if (!when1 && !when2) {
return true; // Both global
}
if (!when1 || !when2) {
return true; // One is global, potential overlap
}
return when1 === when2; // Same context
}
private hasDebuggersAvailable(): boolean {
return TraeAPI.extensions.all.some(ext =>
ext.packageJSON.contributes?.debuggers?.length > 0
);
}
// Keybinding recording
startRecording(): RecordingSession {
if (this.recordingSession) {
this.stopRecording();
}
this.recordingSession = new RecordingSession();
console.log('Started keybinding recording');
return this.recordingSession;
}
stopRecording(): KeySequence | null {
if (!this.recordingSession) {
return null;
}
const sequence = this.recordingSession.getSequence();
this.recordingSession = null;
console.log('Stopped keybinding recording');
return sequence;
}
isRecording(): boolean {
return !!this.recordingSession;
}
// Utility methods
getAllKeybindings(): KeybindingEntry[] {
return Array.from(this.keybindings.values());
}
getKeybindingsByCommand(command: string): KeybindingEntry[] {
return this.getAllKeybindings().filter(entry =>
entry.definition.command === command
);
}
getKeybindingsBySource(source: KeybindingSource): KeybindingEntry[] {
return this.getAllKeybindings().filter(entry =>
entry.source === source
);
}
searchKeybindings(query: string): KeybindingEntry[] {
const lowercaseQuery = query.toLowerCase();
return this.getAllKeybindings().filter(entry => {
const definition = entry.definition;
return (
definition.key.toLowerCase().includes(lowercaseQuery) ||
definition.command.toLowerCase().includes(lowercaseQuery) ||
(definition.description && definition.description.toLowerCase().includes(lowercaseQuery))
);
});
}
exportKeybindings(): KeybindingExport {
const userBindings = this.getKeybindingsBySource('user');
return {
version: '1.0.0',
exportedAt: new Date().toISOString(),
keybindings: userBindings.map(entry => entry.definition)
};
}
async importKeybindings(exportData: KeybindingExport): Promise<boolean> {
try {
// Clear existing user keybindings
const userBindings = this.getKeybindingsBySource('user');
userBindings.forEach(entry => {
this.unregisterKeybinding(entry.id);
});
// Import new keybindings
exportData.keybindings.forEach(definition => {
this.registerKeybinding(definition, 'user');
});
console.log(`Imported ${exportData.keybindings.length} keybindings`);
return true;
} catch (error) {
console.error('Failed to import keybindings:', error);
return false;
}
}
private generateKeybindingId(definition: KeybindingDefinition): string {
return `${definition.key}:${definition.command}:${Date.now()}`;
}
dispose(): void {
document.removeEventListener('keydown', this.handleKeyDown.bind(this), true);
document.removeEventListener('keyup', this.handleKeyUp.bind(this), true);
this.keybindings.clear();
this.contextKeys.clear();
this.keySequences.clear();
if (this.recordingSession) {
this.recordingSession.dispose();
}
}
}
// Supporting classes
class ContextKey {
private value: boolean;
private listeners: ((value: boolean) => void)[] = [];
constructor(private key: string, initialValue: boolean = false) {
this.value = initialValue;
}
get(): boolean {
return this.value;
}
set(value: boolean): void {
if (this.value !== value) {
this.value = value;
this.notifyListeners();
}
}
onDidChange(listener: (value: boolean) => void): TraeAPI.Disposable {
this.listeners.push(listener);
return {
dispose: () => {
const index = this.listeners.indexOf(listener);
if (index >= 0) {
this.listeners.splice(index, 1);
}
}
};
}
private notifyListeners(): void {
this.listeners.forEach(listener => {
try {
listener(this.value);
} catch (error) {
console.error(`Error in context key listener for ${this.key}:`, error);
}
});
}
}
class ConflictResolver {
resolve(newBinding: KeybindingEntry, conflicts: KeybindingEntry[]): ConflictResolution {
// Simple conflict resolution strategy
// In a real implementation, this could be more sophisticated
const highPriorityConflicts = conflicts.filter(conflict =>
conflict.source === 'user' || conflict.source === 'extension'
);
if (highPriorityConflicts.length > 0) {
return {
allow: false,
reason: 'Conflicts with existing high-priority keybinding',
disableConflicting: false
};
}
return {
allow: true,
reason: 'No high-priority conflicts',
disableConflicting: true
};
}
}
class RecordingSession {
private recordedKeys: KeyboardEvent[] = [];
private startTime: number;
constructor() {
this.startTime = Date.now();
}
recordKey(event: KeyboardEvent): void {
this.recordedKeys.push({
...event,
timeStamp: event.timeStamp - this.startTime
} as KeyboardEvent);
}
getSequence(): KeySequence {
return {
keys: this.recordedKeys.map(event => ({
key: event.key,
ctrlKey: event.ctrlKey,
altKey: event.altKey,
shiftKey: event.shiftKey,
metaKey: event.metaKey,
timestamp: event.timeStamp
})),
duration: Date.now() - this.startTime
};
}
dispose(): void {
this.recordedKeys = [];
}
}
// Interfaces
interface KeybindingDefinition {
key: string;
command: string;
when?: string;
args?: any;
description?: string;
}
interface KeybindingEntry {
id: string;
definition: KeybindingDefinition;
source: KeybindingSource;
enabled: boolean;
registeredAt: number;
lastUsed?: number;
usageCount?: number;
keyCombo?: KeyCombo;
}
type KeybindingSource = 'default' | 'extension' | 'user';
interface KeyCombo {
chords: KeyChord[];
isSequence: boolean;
}
interface KeyChord {
key: string;
modifiers: KeyModifier[];
ctrlKey: boolean;
altKey: boolean;
shiftKey: boolean;
metaKey: boolean;
}
type KeyModifier = 'ctrl' | 'alt' | 'shift' | 'meta';
interface KeySequence {
keys: {
key: string;
ctrlKey: boolean;
altKey: boolean;
shiftKey: boolean;
metaKey: boolean;
timestamp: number;
}[];
duration: number;
}
interface ConflictResolution {
allow: boolean;
reason: string;
disableConflicting: boolean;
}
interface KeybindingExport {
version: string;
exportedAt: string;
keybindings: KeybindingDefinition[];
}
// Initialize keybinding manager
const keybindingManager = new KeybindingManager();Platform-Specific Keybindings
typescript
// Platform detection and key mapping
class PlatformKeybindings {
private platform: 'windows' | 'macos' | 'linux';
private keyMappings: Map<string, PlatformKeyMapping>;
constructor() {
this.platform = this.detectPlatform();
this.keyMappings = new Map();
this.initializePlatformMappings();
}
private detectPlatform(): 'windows' | 'macos' | 'linux' {
const userAgent = navigator.userAgent.toLowerCase();
if (userAgent.includes('mac')) {
return 'macos';
} else if (userAgent.includes('win')) {
return 'windows';
} else {
return 'linux';
}
}
private initializePlatformMappings(): void {
// Common cross-platform mappings
const commonMappings: PlatformKeyMapping[] = [
{
command: 'workbench.action.files.save',
windows: 'ctrl+s',
macos: 'cmd+s',
linux: 'ctrl+s'
},
{
command: 'workbench.action.files.openFile',
windows: 'ctrl+o',
macos: 'cmd+o',
linux: 'ctrl+o'
},
{
command: 'workbench.action.files.newUntitledFile',
windows: 'ctrl+n',
macos: 'cmd+n',
linux: 'ctrl+n'
},
{
command: 'workbench.action.closeActiveEditor',
windows: 'ctrl+w',
macos: 'cmd+w',
linux: 'ctrl+w'
},
{
command: 'undo',
windows: 'ctrl+z',
macos: 'cmd+z',
linux: 'ctrl+z'
},
{
command: 'redo',
windows: 'ctrl+y',
macos: 'cmd+shift+z',
linux: 'ctrl+y'
},
{
command: 'editor.action.clipboardCutAction',
windows: 'ctrl+x',
macos: 'cmd+x',
linux: 'ctrl+x'
},
{
command: 'editor.action.clipboardCopyAction',
windows: 'ctrl+c',
macos: 'cmd+c',
linux: 'ctrl+c'
},
{
command: 'editor.action.clipboardPasteAction',
windows: 'ctrl+v',
macos: 'cmd+v',
linux: 'ctrl+v'
},
{
command: 'editor.action.selectAll',
windows: 'ctrl+a',
macos: 'cmd+a',
linux: 'ctrl+a'
},
{
command: 'actions.find',
windows: 'ctrl+f',
macos: 'cmd+f',
linux: 'ctrl+f'
},
{
command: 'workbench.action.showCommands',
windows: 'ctrl+shift+p',
macos: 'cmd+shift+p',
linux: 'ctrl+shift+p'
},
{
command: 'workbench.action.quickOpen',
windows: 'ctrl+p',
macos: 'cmd+p',
linux: 'ctrl+p'
}
];
commonMappings.forEach(mapping => {
this.keyMappings.set(mapping.command, mapping);
});
}
getKeybindingForPlatform(command: string): string | undefined {
const mapping = this.keyMappings.get(command);
if (!mapping) {
return undefined;
}
switch (this.platform) {
case 'windows':
return mapping.windows;
case 'macos':
return mapping.macos;
case 'linux':
return mapping.linux;
default:
return mapping.windows; // fallback
}
}
getCurrentPlatform(): string {
return this.platform;
}
addPlatformMapping(mapping: PlatformKeyMapping): void {
this.keyMappings.set(mapping.command, mapping);
}
getAllMappings(): PlatformKeyMapping[] {
return Array.from(this.keyMappings.values());
}
}
interface PlatformKeyMapping {
command: string;
windows: string;
macos: string;
linux: string;
}
// Initialize platform keybindings
const platformKeybindings = new PlatformKeybindings();API Reference
Core Interfaces
typescript
interface KeybindingsAPI {
// Registration
registerKeybinding(definition: KeybindingDefinition, source?: KeybindingSource): string;
unregisterKeybinding(id: string): boolean;
updateKeybinding(id: string, newDefinition: Partial<KeybindingDefinition>): boolean;
// Management
enableKeybinding(id: string): boolean;
disableKeybinding(id: string): boolean;
// Query
getAllKeybindings(): KeybindingEntry[];
getKeybindingsByCommand(command: string): KeybindingEntry[];
getKeybindingsBySource(source: KeybindingSource): KeybindingEntry[];
searchKeybindings(query: string): KeybindingEntry[];
// Context
updateContext(key: string, value: boolean): void;
// Recording
startRecording(): RecordingSession;
stopRecording(): KeySequence | null;
isRecording(): boolean;
// Import/Export
exportKeybindings(): KeybindingExport;
importKeybindings(exportData: KeybindingExport): Promise<boolean>;
// Platform
getKeybindingForPlatform(command: string): string | undefined;
getCurrentPlatform(): string;
}Best Practices
- Key Combinations: Use standard modifier keys and avoid conflicts
- Context Conditions: Use specific when conditions to avoid conflicts
- Platform Compatibility: Provide platform-specific bindings when needed
- User Customization: Allow users to override default keybindings
- Documentation: Document all custom keybindings clearly
- Accessibility: Ensure keybindings are accessible and discoverable
- Performance: Optimize key event handling for responsiveness
- Conflict Resolution: Implement clear conflict resolution strategies
Related APIs
- Commands API - For command execution
- UI API - For keybinding UI components
- Settings API - For keybinding settings
- Extensions API - For extension keybindings