Skip to content

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 ​

  1. Key Combinations: Use standard modifier keys and avoid conflicts
  2. Context Conditions: Use specific when conditions to avoid conflicts
  3. Platform Compatibility: Provide platform-specific bindings when needed
  4. User Customization: Allow users to override default keybindings
  5. Documentation: Document all custom keybindings clearly
  6. Accessibility: Ensure keybindings are accessible and discoverable
  7. Performance: Optimize key event handling for responsiveness
  8. Conflict Resolution: Implement clear conflict resolution strategies

Your Ultimate AI-Powered IDE Learning Guide