Excel互換とSheets準拠のキーマップを共存させる
Canonical Catalog・Execution Capability・Runtime Bindingを分離し、Microsoft公式の外部inventoryでcoverageを監査する。
Google Sheetsをキーボード中心で操作するChrome拡張 Spex を作っています。
Spexには、Excelに近いキー操作を使う excel profileと、Google Sheetsの現在のメニュー構造に合わせる sheets profileがあります。
開発途中で、Excel側のショートカットカタログをかなり増やしました。
そのとき一見すると自然な設計をしていました。
Shortcut Catalog
↓
KeyTips
↓
Keyboard Controller
↓
Command
Catalogに登録されているなら、KeyTipsへ出す。
キーが完全一致したらCommandを実行する。
しかし、これが間違いでした。
ある時、KeyTipsにはキーが見えているのに、対応するSheets操作が存在しないという状態が出ました。
ユーザー視点ではかなり悪いバグです。
画面が「このキーを押せます」と案内しているのに、押した先に実行できるものがないからです。
Excelに存在することと、Google Sheetsで実行できることは別だった
SpexではExcel互換性を監査するため、canonicalなショートカット定義を持っています。
例えば概念的には、
Alt + F + H
Alt + H + O + I
Alt + N + S + Z + C
Alt + W + P
のようなExcel側のpathをcatalogへ残したい。
しかし、それら全部にGoogle Sheets上の同等機能があるとは限りません。
さらに、同等機能があっても、Spexが安全に実行・検証できるとは限りません。
つまり、次の3つは別物でした。
1. Excel canonical mappingとして存在する
2. Google Sheets上で対応するsemantic commandがある
3. Spexが現在安全に実行できる
最初の設計では、この3つをほぼ一つとして扱っていました。
Canonical Catalogを消すのも違う
では、実行できないショートカットをcatalogから消せばよいかというと、それも違います。
Excel互換性を考える上では、
Excelにはこのpathがあるが、Sheetsでは対応できない
という事実も重要だからです。
Catalogは仕様・監査のSource of Truthとして残したい。
ただし、そのままruntimeへ流してはいけない。
そこで、現在は概念を分離しています。
Canonical Catalog
↓
Execution Capability
↓
Runtime Binding
↓
KeyTips / Keyboard / Shortcut Reference
Canonical Catalog
Excel側のmappingを保持する。
「存在するかどうか」の正本。
Execution Capability
現在のSpexが、そのsemantic commandをGoogle Sheets上で安全に扱えるか。
例えば、
supported
unsupported
のような能力判定を持つ。
Runtime Binding
実際にユーザーへ提供するshortcut。
unsupportedなら、canonical catalogに残っていてもruntime bindingから外します。
「存在するけど使えない」をUIへ出さない
この変更後、Excel runtime shortcutsは概念的に次のように作られます。
const runtimeShortcuts = canonicalShortcuts.filter(
shortcut => getExecutionCapability(shortcut.command) !== "unsupported"
);
これによって、canonical path自体は失いません。
一方で、
- KeyTips
- KeyboardController
- Shortcut Reference
には実行可能なsubsetだけを渡します。
この分離は、機能対応表をきれいにするためではありません。
UIが嘘をつかないための設計です。
さらに、Excel準拠とSheets準拠で Alt+S が衝突した
もう一つ問題がありました。
Spexには二つのKeyMap profileがあります。
Excel profile
Excel-style shortcut engineがAlt直後を所有します。
Sheets固有のlive registryへ入るgatewayとして Alt+S を使います。
Alt
↓
Excel shortcut engine
↓
Alt + S
↓
Sheets Live Registry
Sheets profile
Google Sheetsの現在のUI構造を直接Alt配下へ出します。
この場合、Alt+S 自体がSheets側のrootになり得ます。
Alt
↓
Sheets Live Registry
↓
S = Sheet root
つまり、同じphysical sequenceである Alt+S が、profileによって別の意味を持ちます。
最初はこれを「duplicate shortcut」として扱いかけました。
しかし、考えてみれば衝突ではありません。
同時に有効にならないからです。
重複判定はglobalではなくprofile内で行う
ここで必要だったのは、各profileにAlt root ownerを一つだけ持たせることでした。
excel profile
Alt owner = excel-shortcuts
sheets profile
Alt owner = sheets-navigator
同じkeydownを両方のengineへ配送しません。
したがって、
excel: Alt+S = Sheets Registry gateway
sheets: Alt+S = Sheet root
は合法です。
重複チェックもglobal catalog全体ではなく、同時にactiveになるbinding集合の中で行います。
この考え方を入れたことで、Excel準拠とSheets準拠を無理に一つの巨大shortcut treeへ混ぜなくて済みました。
直した後、別の場所からKeyTipsがまた嘘をついた
ここで終わらなかったのが面白いところです。
runtime bindingを分離した後も、一部のKeyTips生成経路がcanonical catalogを直接参照していました。
そのため、KeyboardController側では実行不能shortcutを除外していても、KeyTipsだけ古い集合を表示できる状態が残りました。
Git履歴でも、
fix: separate keymap profiles from command capability
↓
fix: scope keytips to runtime catalog
↓
test: exhaust runtime keytip prefixes
という順で修正しています。
ここで得た教訓はかなり単純です。
Filterした集合を作っただけでは不十分。すべてのuser-facing consumerが同じruntime集合を見ていることまで検証する。
Invariant Testを追加した
この種のバグは、個別exampleだけだと再発します。
そのため現在は、runtime binding全体を走査するテストを持っています。
最低限、次を確認します。
1. Active shortcutには必ずexecution capabilityがある
for every runtime shortcut:
capability != unsupported
2. unavailable canonical pathはKeyTipsへ出ない
canonicalにはある
runtimeにはない
KeyTipsにもない
3. profile内ではsequenceが一意
excel active bindings -> unique
sheets active bindings -> unique
4. cross-profile overlapは許可
同時activeではないので、同じsequenceが別profileに存在してもよい。
5. prefixを網羅する
leafだけではなく、途中prefixが次のキー集合を正しく返すかを総当たりします。
これはKeyTipsのために重要です。
最終commandだけ正しくても、途中で存在しないキーを案内したらUXは壊れるからです。
Shortcut DefinitionはUIではない
今回の設計変更で一番大きかった認識はこれでした。
最初は、
shortcut catalog = ユーザーが使えるショートカット一覧
という感覚がありました。
今は、
Catalog = 知識
Capability = 現在できること
Binding = 今ユーザーへ約束すること
と分けています。
この3つは似ていますが、責務が違います。
特に、外部Webアプリの上で動く拡張機能では、host側の変更によってcapabilityだけが突然落ちることがあります。
そのときcanonical knowledgeまで消す必要はありません。
一方、capabilityが落ちたのにUIが古い約束を続けるのは危険です。
Coverageの分母をSpex自身にしない
ここまでの分離で、runtimeに存在するshortcutが実行可能であることは検証できるようになりました。
ただし、もう一つ別の問題があります。
Spexが知っているshortcutを100%分類できても、Excel全体のcoverageが100%とは限らない。
内部catalogだけを分母にすると、まだ登録していないExcel commandは最初から集計対象に入りません。
そこで現在は、内部canonical catalogとは別に、Microsoft公式のWindows desktop向け資料から外部inventoryを作っています。
2026-09-19時点の監査済みinventoryは46 sequenceです。
external Excel inventory: 46
active 28
unsupported 10
missing-binding 0
missing-command 8
一方、Spex内部のmodern canonical catalogは47 sequenceあります。この二つは同じ集合ではありません。
Spex canonical catalog
= Spexが現在知っているExcel mapping
External Excel inventory
= Spex実装とは独立して監査したExcel側の分母
外部inventoryではcoverage状態を active / unsupported / missing-binding / missing-command の4つに分けています。missing-binding はSpex Command自体は存在するがmodern Excel bindingがない状態、missing-command は対応するSpex Command自体がまだない状態です。
unsupported と missing をinventoryから削除しないことが重要です。実装済み項目だけで分母を作ると、機能を追加するたびにcoverageが常に100%に近づく自己参照的な指標になります。外部分母を持つことで、coverage reportをどこが未実装なのかを検出する監査データとして使えます。
Ctrl系shortcutはさらに別inventoryにする
Ribbon KeyTipと、Ctrl+C や Ctrl+B のようなdirect shortcutも同じcoverageへ混ぜません。Google Sheets自身が同じshortcutを持っている場合、Spexが横取りして再実装する必要がないからです。
現在のdirect shortcut seed 44件は次のように分類しています。
native-same 24
native-equivalent 8
different-key 4
conflict 8
native-same はSpexの実装実績としてKeyTip coverageへ加算しません。Sheets nativeへそのまま渡している操作と、Spexが実装したExcel互換commandを同じ数字にすると、何を実装したcoverageなのか分からなくなるためです。
Release gateも単一percentageにしない
最終的な実行結果も executed / invoked / unsupported / failed を区別しています。DOM eventを送れた、menu itemを押した、といった状態は invoked であって、Google Sheets上の結果まで確認できた executed とは別です。
Release AcceptanceではProduction Command inventory、Terminal outcome contract、Native-handoff continuity、Recovery / non-interferenceを独立して検証します。
shortcut coverageを一つのpercentageへ集約しないのは、入力経路、実行能力、hostへの引き継ぎ、観測可能な結果がそれぞれ別のfailure modeを持つからです。
一般化すると
これはショートカットだけの問題ではないと思います。
例えば、
- feature catalog
- plugin registry
- command palette
- API capability
- role-based action menu
でも、
定義されている
と
現在実行できる
と
ユーザーへ表示する
を同じ集合として扱うと壊れやすいです。
SpexではKeyTipsが目に見えて嘘をついたことで、この境界をはっきり分けることになりました。
Spex
この設計はGoogle Sheets向けChrome拡張Spexで使っています。
- Product: https://spex.kikuta.dev/ja
- Chrome Web Store: https://chromewebstore.google.com/detail/gahnolkboklnjhnaahjdkingoejbepcc