## Issue https://github.com/tadashi-aikawa/obsidian-various-complements-plugin/issues/245 ## 修正のポイント ### 現行の重要な関数 - `registerKeyAsIgnored`はあるキーを貫通させるためのキーバインド - たとえばEnterを`registerKeyAsIgnored`すると、エディタ上で改行されたりする - 無効化 (`scope.unregister`) との違いは、無効化はエディタに貫通しない - `selectNext`は次の候補を選択、`selectPrevious`は前の候補を選択 ### 現行の処理 現行処理は設定ごとに割り当てをしている感じ。 1. すべてのキーを無効化 2. 候補決定キーの割り当て - Enterに設定されていなければ、Enterを貫通 - Tabに設定されていなければ、Tabを貫通 - 何かしらキーが設定されていれば、そのキーでバインドする 3. `propagateESC`の設定にしたがって`ESC`の挙動を変更する 4. サイクルキーの割り当て - 上下サイクルキーの無効化設定? - 無効化されているなら、貫通させる - 無効化されていないなら、`selectNext`と`selectPrevious`を割り当て - 追加サイクルキーが設定されているなら - TABで設定されているなら、TABの登録を無効化 - 追加サイクルキーに、`selectNext`と`selectPrevious`を割り当て 5. オープンソースファイルキーの割り当て - 設定されているなら割り当て 6. 共通prefixコンプリートキーの割り当て - 設定されているなら割り当て ```ts // selectSuggestionKeys const selectSuggestionKey = SelectSuggestionKey.fromName( this.settings.selectSuggestionKeys ); if (selectSuggestionKey !== SelectSuggestionKey.ENTER) { registerKeyAsIgnored( SelectSuggestionKey.ENTER.keyBind.modifiers, SelectSuggestionKey.ENTER.keyBind.key ); } if (selectSuggestionKey !== SelectSuggestionKey.TAB) { registerKeyAsIgnored( SelectSuggestionKey.TAB.keyBind.modifiers, SelectSuggestionKey.TAB.keyBind.key ); } if (selectSuggestionKey !== SelectSuggestionKey.None) { this.keymapEventHandler.push( this.scope.register( selectSuggestionKey.keyBind.modifiers, selectSuggestionKey.keyBind.key, (evt, ctx) => { if (!evt.isComposing) { if (this.selectionLock) { this.close(); return true; } else { this.suggestions.useSelectedItem({}); return false; } } } ) ); } // propagateESC this.scope.keys.find((x) => x.key === "Escape")!.func = () => { this.close(); return this.settings.propagateEsc; }; // cycleThroughSuggestionsKeys const selectNext = (evt: KeyboardEvent) => { if (this.settings.noAutoFocusUntilCycle && this.selectionLock) { this.setSelectionLock(false); } else { this.suggestions.setSelectedItem( this.suggestions.selectedItem + 1, evt ); } return false; }; const selectPrevious = (evt: KeyboardEvent) => { if (this.settings.noAutoFocusUntilCycle && this.selectionLock) { this.setSelectionLock(false); } else { this.suggestions.setSelectedItem( this.suggestions.selectedItem - 1, evt ); } return false; }; const cycleThroughSuggestionsKeys = CycleThroughSuggestionsKeys.fromName( this.settings.additionalCycleThroughSuggestionsKeys ); if (this.settings.disableUpDownKeysForCycleThroughSuggestionsKeys) { this.keymapEventHandler.push( this.scope.register([], "ArrowDown", () => { this.close(); return true; }), this.scope.register([], "ArrowUp", () => { this.close(); return true; }) ); } else { this.keymapEventHandler.push( this.scope.register([], "ArrowDown", selectNext), this.scope.register([], "ArrowUp", selectPrevious) ); } if (cycleThroughSuggestionsKeys !== CycleThroughSuggestionsKeys.NONE) { if (cycleThroughSuggestionsKeys === CycleThroughSuggestionsKeys.TAB) { this.scope.unregister( this.scope.keys.find((x) => x.modifiers === "" && x.key === "Tab")! ); } this.keymapEventHandler.push( this.scope.register( cycleThroughSuggestionsKeys.nextKey.modifiers, cycleThroughSuggestionsKeys.nextKey.key, selectNext ), this.scope.register( cycleThroughSuggestionsKeys.previousKey.modifiers, cycleThroughSuggestionsKeys.previousKey.key, selectPrevious ) ); } const openSourceFileKey = OpenSourceFileKeys.fromName( this.settings.openSourceFileKey ); if (openSourceFileKey !== OpenSourceFileKeys.NONE) { this.keymapEventHandler.push( this.scope.register( openSourceFileKey.keyBind.modifiers, openSourceFileKey.keyBind.key, () => { const item = this.suggestions.values[this.suggestions.selectedItem]; if ( item.type !== "currentVault" && item.type !== "internalLink" && item.type !== "frontMatter" ) { return false; } const markdownFile = this.appHelper.getMarkdownFileByPath( item.createdPath ); if (!markdownFile) { // noinspection ObjectAllocationIgnored new Notice(`Can't open ${item.createdPath}`); return false; } this.appHelper.openMarkdownFile(markdownFile, true); return false; } ) ); } if (this.settings.useCommonPrefixCompletionOfSuggestion) { this.scope.unregister( this.scope.keys.find((x) => x.modifiers === "" && x.key === "Tab")! ); this.keymapEventHandler.push( this.scope.register([], "Tab", () => { if (!this.context) { return; } const editor = this.context.editor; const currentPhrase = editor.getRange( { ...this.context.start, ch: this.contextStartCh, }, this.context.end ); const tokens = this.tokenizer.recursiveTokenize(currentPhrase); const commonPrefixWithToken = tokens .map((t) => ({ token: t, commonPrefix: findCommonPrefix( this.suggestions.values .map((x) => excludeEmoji(x.value)) .filter((x) => x.toLowerCase().startsWith(t.word.toLowerCase()) ) ), })) .find((x) => x.commonPrefix != null); if ( !commonPrefixWithToken || currentPhrase === commonPrefixWithToken.commonPrefix ) { return false; } editor.replaceRange( commonPrefixWithToken.commonPrefix!, { ...this.context.start, ch: this.contextStartCh + commonPrefixWithToken.token.offset, }, this.context.end ); return true; }) ); } ``` ```ts // key customization selectSuggestionKeys: "Enter", additionalCycleThroughSuggestionsKeys: "None", disableUpDownKeysForCycleThroughSuggestionsKeys: false, openSourceFileKey: "None", propagateEsc: false, ``` ```ts new Setting(containerEl) .setName("Disable the up/down keys for cycle through suggestions keys") .addToggle((tc) => { tc.setValue( this.plugin.settings.disableUpDownKeysForCycleThroughSuggestionsKeys ).onChange(async (value) => { this.plugin.settings.disableUpDownKeysForCycleThroughSuggestionsKeys = value; await this.plugin.saveSettings(); }); }); new Setting(containerEl) .setName("Select a suggestion key") .addDropdown((tc) => tc .addOptions(mirrorMap(SelectSuggestionKey.values(), (x) => x.name)) .setValue(this.plugin.settings.selectSuggestionKeys) .onChange(async (value) => { this.plugin.settings.selectSuggestionKeys = value; await this.plugin.saveSettings(); }) ); new Setting(containerEl) .setName("Additional cycle through suggestions keys") .addDropdown((tc) => tc .addOptions( mirrorMap(CycleThroughSuggestionsKeys.values(), (x) => x.name) ) .setValue(this.plugin.settings.additionalCycleThroughSuggestionsKeys) .onChange(async (value) => { this.plugin.settings.additionalCycleThroughSuggestionsKeys = value; await this.plugin.saveSettings(); }) ); new Setting(containerEl).setName("Open source file key").addDropdown((tc) => tc .addOptions(mirrorMap(OpenSourceFileKeys.values(), (x) => x.name)) .setValue(this.plugin.settings.openSourceFileKey) .onChange(async (value) => { this.plugin.settings.openSourceFileKey = value; await this.plugin.saveSettings(); }) ); ``` ### 改修後の処理 [[🦉Another Quick Switcher]]のように以下のようなインターフェースで停止する。 ```ts export type Hotkey = { modifiers: Modifier[]; key: string; hideHotkeyGuide?: boolean; }; export interface Hotkeys { main: { up: Hotkey[]; down: Hotkey[]; } } ``` ここから、機能ごとに0以上のホットキーを取得できるようになる。これを順に設定していけばよい。 なので 1. [x] Enter, Tab, 上下, Home, Endキーを無効化 2. [x] Enter, Tab, 上下, Home, Endキーを貫通 3. [x] `propagateESC`の設定にしたがって`ESC`の挙動を変更する 4. [x] 機能ごとにホットキーを割り当てていく - [x] select - [x] up - [x] down - [x] open - [x] completion [[Obsidianプラグインでscopeに登録したhotkeyは先勝ちになる]]ので、ignored貫通キーを先に登録してしまうとキーバインドの設定は反映されなくなる。つまり、最後に設定した方がいいかなと。