Skip to content

Keybindings API

Keybindings APIは、開発環境内でキーボードショートカット、カスタムキーの組み合わせ、入力処理を管理するための包括的な機能を提供します。

概要

Keybindings APIでは以下のことができます:

  • カスタムキーボードショートカットの登録
  • デフォルトキーバインディングの上書き
  • 複雑なキーの組み合わせの処理
  • コンテキスト依存のショートカットの管理
  • キーバインディングの競合解決の提供
  • プラットフォーム固有のバインディングのサポート
  • キーバインディングの記録と再生の実装
  • 国際キーボードレイアウトの処理

基本的な使用方法

キーバインディングの登録

typescript
import { TraeAPI } from '@trae/api';

// キーバインディングマネージャーの実装
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 {
    // デフォルトキーバインディングの登録
    const defaultBindings: KeybindingDefinition[] = [
      {
        key: 'ctrl+s',
        command: 'workbench.action.files.save',
        when: 'editorTextFocus',
        description: 'ファイルを保存'
      },
      {
        key: 'ctrl+shift+s',
        command: 'workbench.action.files.saveAs',
        when: 'editorTextFocus',
        description: '名前を付けてファイルを保存'
      },
      {
        key: 'ctrl+o',
        command: 'workbench.action.files.openFile',
        description: 'ファイルを開く'
      },
      {
        key: 'ctrl+n',
        command: 'workbench.action.files.newUntitledFile',
        description: '新しいファイル'
      },
      {
        key: 'ctrl+w',
        command: 'workbench.action.closeActiveEditor',
        when: 'editorIsOpen',
        description: 'エディターを閉じる'
      },
      {
        key: 'ctrl+shift+w',
        command: 'workbench.action.closeAllEditors',
        description: 'すべてのエディターを閉じる'
      },
      {
        key: 'ctrl+z',
        command: 'undo',
        when: 'editorTextFocus && !editorReadonly',
        description: '元に戻す'
      },
      {
        key: 'ctrl+y',
        command: 'redo',
        when: 'editorTextFocus && !editorReadonly',
        description: 'やり直し'
      },
      {
        key: 'ctrl+x',
        command: 'editor.action.clipboardCutAction',
        when: 'editorTextFocus && !editorReadonly',
        description: '切り取り'
      },
      {
        key: 'ctrl+c',
        command: 'editor.action.clipboardCopyAction',
        when: 'editorTextFocus',
        description: 'コピー'
      },
      {
        key: 'ctrl+v',
        command: 'editor.action.clipboardPasteAction',
        when: 'editorTextFocus && !editorReadonly',
        description: '貼り付け'
      },
      {
        key: 'ctrl+a',
        command: 'editor.action.selectAll',
        when: 'editorTextFocus',
        description: 'すべて選択'
      },
      {
        key: 'ctrl+f',
        command: 'actions.find',
        when: 'editorFocus || editorIsOpen',
        description: '検索'
      },
      {
        key: 'ctrl+h',
        command: 'editor.action.startFindReplaceAction',
        when: 'editorFocus || editorIsOpen',
        description: '置換'
      },
      {
        key: 'ctrl+shift+f',
        command: 'workbench.action.findInFiles',
        description: 'ファイル内検索'
      },
      {
        key: 'ctrl+shift+h',
        command: 'workbench.action.replaceInFiles',
        description: 'ファイル内置換'
      },
      {
        key: 'ctrl+g',
        command: 'workbench.action.gotoLine',
        when: 'editorFocus',
        description: '行に移動'
      },
      {
        key: 'ctrl+shift+p',
        command: 'workbench.action.showCommands',
        description: 'コマンドパレットを表示'
      },
      {
        key: 'ctrl+p',
        command: 'workbench.action.quickOpen',
        description: 'クイックオープン'
      },
      {
        key: 'ctrl+shift+e',
        command: 'workbench.view.explorer',
        description: 'エクスプローラーを表示'
      },
      {
        key: 'ctrl+shift+g',
        command: 'workbench.view.scm',
        description: 'ソース管理を表示'
      },
      {
        key: 'ctrl+shift+d',
        command: 'workbench.view.debug',
        description: 'デバッグを表示'
      },
      {
        key: 'ctrl+shift+x',
        command: 'workbench.view.extensions',
        description: '拡張機能を表示'
      },
      {
        key: 'ctrl+`',
        command: 'workbench.action.terminal.toggleTerminal',
        description: 'ターミナルの切り替え'
      },
      {
        key: 'ctrl+shift+`',
        command: 'workbench.action.terminal.new',
        description: '新しいターミナル'
      },
      {
        key: 'f5',
        command: 'workbench.action.debug.start',
        when: 'debuggersAvailable && !inDebugMode',
        description: 'デバッグ開始'
      },
      {
        key: 'shift+f5',
        command: 'workbench.action.debug.stop',
        when: 'inDebugMode',
        description: 'デバッグ停止'
      },
      {
        key: 'f9',
        command: 'editor.debug.action.toggleBreakpoint',
        when: 'debuggersAvailable && editorTextFocus',
        description: 'ブレークポイントの切り替え'
      },
      {
        key: 'f10',
        command: 'workbench.action.debug.stepOver',
        when: 'debugState == "stopped"',
        description: 'ステップオーバー'
      },
      {
        key: 'f11',
        command: 'workbench.action.debug.stepInto',
        when: 'debugState == "stopped"',
        description: 'ステップイン'
      },
      {
        key: 'shift+f11',
        command: 'workbench.action.debug.stepOut',
        when: 'debugState == "stopped"',
        description: 'ステップアウト'
      },
      {
        key: 'ctrl+k ctrl+c',
        command: 'editor.action.addCommentLine',
        when: 'editorTextFocus && !editorReadonly',
        description: '行コメントを追加'
      },
      {
        key: 'ctrl+k ctrl+u',
        command: 'editor.action.removeCommentLine',
        when: 'editorTextFocus && !editorReadonly',
        description: '行コメントを削除'
      },
      {
        key: 'ctrl+/',
        command: 'editor.action.commentLine',
        when: 'editorTextFocus && !editorReadonly',
        description: '行コメントの切り替え'
      },
      {
        key: 'shift+alt+a',
        command: 'editor.action.blockComment',
        when: 'editorTextFocus && !editorReadonly',
        description: 'ブロックコメントの切り替え'
      },
      {
        key: 'ctrl+shift+k',
        command: 'editor.action.deleteLines',
        when: 'editorTextFocus && !editorReadonly',
        description: '行を削除'
      },
      {
        key: 'ctrl+enter',
        command: 'editor.action.insertLineAfter',
        when: 'editorTextFocus && !editorReadonly',
        description: '下に行を挿入'
      },
      {
        key: 'ctrl+shift+enter',
        command: 'editor.action.insertLineBefore',
        when: 'editorTextFocus && !editorReadonly',
        description: '上に行を挿入'
      },
      {
        key: 'alt+up',
        command: 'editor.action.moveLinesUpAction',
        when: 'editorTextFocus && !editorReadonly',
        description: '行を上に移動'
      },
      {
        key: 'alt+down',
        command: 'editor.action.moveLinesDownAction',
        when: 'editorTextFocus && !editorReadonly',
        description: '行を下に移動'
      },
      {
        key: 'shift+alt+up',
        command: 'editor.action.copyLinesUpAction',
        when: 'editorTextFocus && !editorReadonly',
        description: '行を上にコピー'
      },
      {
        key: 'shift+alt+down',
        command: 'editor.action.copyLinesDownAction',
        when: 'editorTextFocus && !editorReadonly',
        description: '行を下にコピー'
      },
      {
        key: 'ctrl+d',
        command: 'editor.action.addSelectionToNextFindMatch',
        when: 'editorFocus',
        description: '次の一致に選択を追加'
      },
      {
        key: 'ctrl+k ctrl+d',
        command: 'editor.action.moveSelectionToNextFindMatch',
        when: 'editorFocus',
        description: '最後の選択を次の一致に移動'
      },
      {
        key: 'ctrl+u',
        command: 'cursorUndo',
        when: 'editorTextFocus',
        description: 'カーソルを元に戻す'
      },
      {
        key: 'shift+alt+i',
        command: 'editor.action.insertCursorAtEndOfEachLineSelected',
        when: 'editorTextFocus',
        description: '選択された各行の末尾にカーソルを挿入'
      },
      {
        key: 'ctrl+shift+l',
        command: 'editor.action.selectHighlights',
        when: 'editorFocus',
        description: '一致するすべてを選択'
      },
      {
        key: 'ctrl+f2',
        command: 'editor.action.changeAll',
        when: 'editorTextFocus && !editorReadonly',
        description: 'すべての出現箇所を変更'
      },
      {
        key: 'ctrl+i',
        command: 'editor.action.triggerSuggest',
        when: 'editorHasCompletionItemProvider && editorTextFocus && !editorReadonly',
        description: '候補をトリガー'
      },
      {
        key: 'ctrl+space',
        command: 'editor.action.triggerSuggest',
        when: 'editorHasCompletionItemProvider && editorTextFocus && !editorReadonly',
        description: '候補をトリガー'
      },
      {
        key: 'ctrl+shift+space',
        command: 'editor.action.triggerParameterHints',
        when: 'editorHasSignatureHelpProvider && editorTextFocus',
        description: 'パラメーターヒントをトリガー'
      },
      {
        key: 'shift+alt+f',
        command: 'editor.action.formatDocument',
        when: 'editorHasDocumentFormattingProvider && editorTextFocus && !editorReadonly',
        description: 'ドキュメントをフォーマット'
      },
      {
        key: 'ctrl+k ctrl+f',
        command: 'editor.action.formatSelection',
        when: 'editorHasDocumentSelectionFormattingProvider && editorTextFocus && !editorReadonly',
        description: '選択範囲をフォーマット'
      },
      {
        key: 'f12',
        command: 'editor.action.revealDefinition',
        when: 'editorHasDefinitionProvider && editorTextFocus',
        description: '定義に移動'
      },
      {
        key: 'alt+f12',
        command: 'editor.action.peekDefinition',
        when: 'editorHasDefinitionProvider && editorTextFocus',
        description: '定義をピーク'
      },
      {
        key: 'ctrl+k f12',
        command: 'editor.action.revealDefinitionAside',
        when: 'editorHasDefinitionProvider && editorTextFocus',
        description: '定義を横に開く'
      },
      {
        key: 'ctrl+f12',
        command: 'editor.action.goToImplementation',
        when: 'editorHasImplementationProvider && editorTextFocus',
        description: '実装に移動'
      },
      {
        key: 'ctrl+shift+f12',
        command: 'editor.action.peekImplementation',
        when: 'editorHasImplementationProvider && editorTextFocus',
        description: '実装をピーク'
      },
      {
        key: 'shift+f12',
        command: 'editor.action.goToReferences',
        when: 'editorHasReferenceProvider && editorTextFocus',
        description: '参照に移動'
      },
      {
        key: 'ctrl+k ctrl+q',
        command: 'editor.action.quickFix',
        when: 'editorHasCodeActionsProvider && editorTextFocus',
        description: 'クイックフィックス'
      },
      {
        key: 'f2',
        command: 'editor.action.rename',
        when: 'editorHasRenameProvider && editorTextFocus',
        description: 'シンボルの名前変更'
      },
      {
        key: 'ctrl+k ctrl+x',
        command: 'editor.action.trimTrailingWhitespace',
        when: 'editorTextFocus && !editorReadonly',
        description: '末尾の空白を削除'
      },
      {
        key: 'ctrl+k m',
        command: 'workbench.action.editor.changeLanguageMode',
        when: 'editorTextFocus',
        description: '言語モードを変更'
      },
      {
        key: 'ctrl+k r',
        command: 'workbench.action.files.revealActiveFileInWindows',
        when: 'editorFocus',
        description: 'エクスプローラーでアクティブファイルを表示'
      },
      {
        key: 'ctrl+k o',
        command: 'workbench.action.files.showOpenedFileInNewWindow',
        when: 'editorFocus',
        description: '新しいウィンドウで開いたファイルを表示'
      },
      {
        key: 'ctrl+k v',
        command: 'workbench.action.markdown.openPreviewToTheSide',
        when: 'editorFocus && editorLangId == "markdown"',
        description: 'Markdownプレビューを横に開く'
      }
    ];

    // デフォルトバインディングの登録
    defaultBindings.forEach(binding => {
      this.registerKeybinding(binding, 'default');
    });

    console.log(`${defaultBindings.length}個のデフォルトキーバインディングを登録しました`);
  }

  private setupEventListeners(): void {
    // キーボードイベントのリスニング
    document.addEventListener('keydown', this.handleKeyDown.bind(this), true);
    document.addEventListener('keyup', this.handleKeyUp.bind(this), true);
    
    // コンテキスト変更のリスニング
    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 || '');
    });

    // デバッグ状態変更のリスニング
    TraeAPI.debug.onDidChangeActiveDebugSession(session => {
      this.updateContext('inDebugMode', !!session);
      this.updateContext('debugState', session?.state || 'inactive');
    });

    // 拡張機能変更のリスニング
    TraeAPI.extensions.onDidChange(() => {
      this.updateContext('debuggersAvailable', this.hasDebuggersAvailable());
    });
  }

  private async loadUserKeybindings(): Promise<void> {
    try {
      // 設定からユーザーキーバインディングを読み込み
      const userBindings = TraeAPI.workspace.getConfiguration('keybindings').get<KeybindingDefinition[]>('user', []);
      
      userBindings.forEach(binding => {
        this.registerKeybinding(binding, 'user');
      });
      
      console.log(`${userBindings.length}個のユーザーキーバインディングを読み込みました`);
    } catch (error) {
      console.error('ユーザーキーバインディングの読み込みに失敗しました:', error);
    }
  }

  // コアキーバインディング操作
  registerKeybinding(definition: KeybindingDefinition, source: KeybindingSource = 'extension'): string {
    try {
      const id = this.generateKeybindingId(definition);
      const entry: KeybindingEntry = {
        id,
        definition,
        source,
        enabled: true,
        registeredAt: Date.now()
      };

      // 競合をチェック
      const conflicts = this.findConflicts(definition);
      if (conflicts.length > 0) {
        const resolution = this.conflictResolver.resolve(entry, conflicts);
        if (!resolution.allow) {
          throw new Error(`キーバインディングの競合: ${resolution.reason}`);
        }
        
        // 競合解決の処理
        if (resolution.disableConflicting) {
          conflicts.forEach(conflict => {
            this.disableKeybinding(conflict.id);
          });
        }
      }

      // キーの組み合わせを解析・検証
      const keyCombo = this.parseKeyCombo(definition.key);
      if (!keyCombo) {
        throw new Error(`無効なキーの組み合わせ: ${definition.key}`);
      }

      entry.keyCombo = keyCombo;
      this.keybindings.set(id, entry);

      console.log(`キーバインディングを登録しました: ${definition.key} -> ${definition.command}`);
      return id;
    } catch (error) {
      console.error(`キーバインディング ${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(`キーバインディングを登録解除しました: ${entry.definition.key}`);
      return true;
    } catch (error) {
      console.error(`キーバインディング ${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;
      }

      // 更新された定義を作成
      const updatedDefinition = { ...entry.definition, ...newDefinition };
      
      // 変更された場合は新しいキーの組み合わせを検証
      if (newDefinition.key && newDefinition.key !== entry.definition.key) {
        const keyCombo = this.parseKeyCombo(newDefinition.key);
        if (!keyCombo) {
          throw new Error(`無効なキーの組み合わせ: ${newDefinition.key}`);
        }
        entry.keyCombo = keyCombo;
      }

      entry.definition = updatedDefinition;
      console.log(`キーバインディングを更新しました: ${id}`);
      return true;
    } catch (error) {
      console.error(`キーバインディング ${id} の更新に失敗しました:`, error);
      return false;
    }
  }

  // キーの組み合わせ解析
  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(`キーの組み合わせ ${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) {
          // 複数の非修飾キーは許可されない
          return null;
        }
        key = normalizedPart;
      }
    }

    if (!key) {
      return null;
    }

    return {
      key,
      modifiers: modifiers.sort(), // 一貫した比較のためソート
      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);
  }

  // イベント処理
  private handleKeyDown(event: KeyboardEvent): void {
    try {
      // 記録中の場合はスキップ
      if (this.recordingSession) {
        this.recordingSession.recordKey(event);
        return;
      }

      // 現在のキーコードを作成
      const currentChord = this.createKeyChordFromEvent(event);
      if (!currentChord) {
        return;
      }

      // 一致するキーバインディングを検索
      const matches = this.findMatchingKeybindings(currentChord);
      
      if (matches.length === 0) {
        return;
      }

      // 単一コードの一致を処理
      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;
        }
      }

      // シーケンスの一致を処理
      const sequenceMatches = matches.filter(match => match.keyCombo.isSequence);
      if (sequenceMatches.length > 0) {
        this.handleKeySequence(currentChord, sequenceMatches, event);
      }
    } catch (error) {
      console.error('キーダウンの処理エラー:', error);
    }
  }

  private handleKeyUp(event: KeyboardEvent): void {
    // 必要に応じてキーアップイベントを処理
  }

  private createKeyChordFromEvent(event: KeyboardEvent): KeyChord | null {
    const key = this.normalizeKeyName(event.key);
    
    // 修飾キーのみをスキップ
    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 {
    // 自然にシフトを使用するキー(大文字、記号)
    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;
      }

      // 最初のコードが一致するかチェック
      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;
    }

    // 優先度でソート: 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;
      }
      
      // 同じ優先度の場合、より最近のものを優先
      return b.registeredAt - a.registeredAt;
    });

    return matches[0];
  }

  private handleKeySequence(chord: KeyChord, matches: KeybindingEntry[], event: KeyboardEvent): void {
    // キーシーケンス処理の実装
    // 部分的な一致を追跡し、後続のキーを待機する
    console.log('キーシーケンスを処理中:', chord, matches);
  }

  // コンテキスト評価
  private evaluateWhenCondition(when?: string): boolean {
    if (!when) {
      return true;
    }

    try {
      return this.parseAndEvaluateCondition(when);
    } catch (error) {
      console.error(`when条件 "${when}" の評価エラー:`, error);
      return false;
    }
  }

  private parseAndEvaluateCondition(condition: string): boolean {
    // シンプルな条件パーサー
    // サポート: &&, ||, !, 括弧, コンテキストキー
    
    // 空白を削除
    condition = condition.replace(/\s+/g, '');
    
    // 括弧を処理
    while (condition.includes('(')) {
      const start = condition.lastIndexOf('(');
      const end = condition.indexOf(')', start);
      
      if (end === -1) {
        throw new Error('括弧が一致しません');
      }
      
      const subCondition = condition.substring(start + 1, end);
      const result = this.parseAndEvaluateCondition(subCondition);
      condition = condition.substring(0, start) + result.toString() + condition.substring(end + 1);
    }
    
    // OR演算子を処理
    if (condition.includes('||')) {
      const parts = condition.split('||');
      return parts.some(part => this.parseAndEvaluateCondition(part));
    }
    
    // AND演算子を処理
    if (condition.includes('&&')) {
      const parts = condition.split('&&');
      return parts.every(part => this.parseAndEvaluateCondition(part));
    }
    
    // NOT演算子を処理
    if (condition.startsWith('!')) {
      return !this.parseAndEvaluateCondition(condition.substring(1));
    }
    
    // ブール値リテラルを処理
    if (condition === 'true') return true;
    if (condition === 'false') return false;
    
    // コンテキストキーを処理
    return this.getContextValue(condition);
  }

  private getContextValue(key: string): boolean {
    const contextKey = this.contextKeys.get(key);
    if (contextKey) {
      return contextKey.get();
    }
    
    // デフォルトコンテキスト値
    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);
    }
  }

  // コマンド実行
  private async executeKeybinding(entry: KeybindingEntry): Promise<void> {
    try {
      console.log(`キーバインディングを実行中: ${entry.definition.key} -> ${entry.definition.command}`);
      
      // コマンドを実行
      await TraeAPI.commands.executeCommand(entry.definition.command, entry.definition.args);
      
      // 使用統計を更新
      entry.lastUsed = Date.now();
      entry.usageCount = (entry.usageCount || 0) + 1;
    } catch (error) {
      console.error(`キーバインディングコマンド ${entry.definition.command} の実行に失敗しました:`, error);
      TraeAPI.window.showErrorMessage(`コマンドの実行に失敗しました: ${entry.definition.command}`);
    }
  }

  // 競合解決
  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) {
        // コンテキストが重複するかチェック
        if (this.contextsOverlap(entry.definition.when, definition.when)) {
          conflicts.push(entry);
        }
      }
    }
    
    return conflicts;
  }

  private contextsOverlap(when1?: string, when2?: string): boolean {
    // 簡略化されたコンテキスト重複検出
    // 実際の実装では、これはより洗練されたものになる
    if (!when1 && !when2) {
      return true; // 両方ともグローバル
    }
    
    if (!when1 || !when2) {
      return true; // 一方がグローバル、重複の可能性
    }
    
    return when1 === when2; // 同じコンテキスト
  }

  private hasDebuggersAvailable(): boolean {
    return TraeAPI.extensions.all.some(ext => 
      ext.packageJSON.contributes?.debuggers?.length > 0
    );
  }

  // キーバインディング記録
  startRecording(): RecordingSession {
    if (this.recordingSession) {
      this.stopRecording();
    }
    
    this.recordingSession = new RecordingSession();
    console.log('キーバインディング記録を開始しました');
    return this.recordingSession;
  }

  stopRecording(): KeySequence | null {
    if (!this.recordingSession) {
      return null;
    }
    
    const sequence = this.recordingSession.getSequence();
    this.recordingSession = null;
    
    console.log('キーバインディング記録を停止しました');
    return sequence;
  }

  isRecording(): boolean {
    return !!this.recordingSession;
  }

  // ユーティリティメソッド
  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 {
      // 既存のユーザーキーバインディングをクリア
      const userBindings = this.getKeybindingsBySource('user');
      userBindings.forEach(entry => {
        this.unregisterKeybinding(entry.id);
      });
      
      // 新しいキーバインディングをインポート
      exportData.keybindings.forEach(definition => {
        this.registerKeybinding(definition, 'user');
      });
      
      console.log(`${exportData.keybindings.length}個のキーバインディングをインポートしました`);
      return true;
    } catch (error) {
      console.error('キーバインディングのインポートに失敗しました:', 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();
    }
  }
}

// サポートクラス
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(`コンテキストキー ${this.key} のリスナーエラー:`, error);
      }
    });
  }
}

class ConflictResolver {
  resolve(newBinding: KeybindingEntry, conflicts: KeybindingEntry[]): ConflictResolution {
    // シンプルな競合解決戦略
    // 実際の実装では、これはより洗練されたものになる
    
    const highPriorityConflicts = conflicts.filter(conflict => 
      conflict.source === 'user' || conflict.source === 'extension'
    );
    
    if (highPriorityConflicts.length > 0) {
      return {
        allow: false,
        reason: '既存の高優先度キーバインディングと競合',
        disableConflicting: false
      };
    }
    
    return {
      allow: true,
      reason: '高優先度の競合なし',
      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 = [];
  }
}

// インターフェース
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[];
}

// キーバインディングマネージャーを初期化
const keybindingManager = new KeybindingManager();

プラットフォーム固有のキーバインディング

typescript
// プラットフォーム検出とキーマッピング
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 {
    // 共通のクロスプラットフォームマッピング
    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; // フォールバック
    }
  }

  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;
}

// プラットフォームキーバインディングを初期化
const platformKeybindings = new PlatformKeybindings();

API リファレンス

コアインターフェース

typescript
interface KeybindingsAPI {
  // 登録
  registerKeybinding(definition: KeybindingDefinition, source?: KeybindingSource): string;
  unregisterKeybinding(id: string): boolean;
  updateKeybinding(id: string, newDefinition: Partial<KeybindingDefinition>): boolean;
  
  // 管理
  enableKeybinding(id: string): boolean;
  disableKeybinding(id: string): boolean;
  
  // クエリ
  getAllKeybindings(): KeybindingEntry[];
  getKeybindingsByCommand(command: string): KeybindingEntry[];
  getKeybindingsBySource(source: KeybindingSource): KeybindingEntry[];
  searchKeybindings(query: string): KeybindingEntry[];
  
  // コンテキスト
  updateContext(key: string, value: boolean): void;
  
  // 記録
  startRecording(): RecordingSession;
  stopRecording(): KeySequence | null;
  isRecording(): boolean;
  
  // インポート/エクスポート
  exportKeybindings(): KeybindingExport;
  importKeybindings(exportData: KeybindingExport): Promise<boolean>;
  
  // プラットフォーム
  getKeybindingForPlatform(command: string): string | undefined;
  getCurrentPlatform(): string;
}

ベストプラクティス

  1. キーの組み合わせ: 標準的な修飾キーを使用し、競合を避ける
  2. コンテキスト条件: 競合を避けるために具体的なwhen条件を使用
  3. プラットフォーム互換性: 必要に応じてプラットフォーム固有のバインディングを提供
  4. ユーザーカスタマイズ: ユーザーがデフォルトキーバインディングを上書きできるようにする
  5. ドキュメント: すべてのカスタムキーバインディングを明確に文書化
  6. アクセシビリティ: キーバインディングがアクセシブルで発見可能であることを確認
  7. パフォーマンス: 応答性のためにキーイベント処理を最適化
  8. 競合解決: 明確な競合解決戦略を実装

関連API

  • Commands API - コマンド実行用
  • UI API - キーバインディングUIコンポーネント用

究極の AI 駆動 IDE 学習ガイド