## 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貫通キーを先に登録してしまうとキーバインドの設定は反映されなくなる。つまり、最後に設定した方がいいかなと。