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