この記事は、東京大学工学部電子情報工学科/電気電子工学科の後期実験「大規模ソフトウェアを手探る」のレポートとして作成されました。
定義参照を楽にしたい
VSCodeでコードを書いているとき、おそらく皆さんのほとんどが関数やクラス、変数などを使うと思います。これらを使う際に、それぞれの定義を参照する機会も多いでしょう。
定義を参照したいとき、よく使われるのがGo to Definition
やGo to Type Definition
などの定義元にコードジャンプする機能だと思います。しかし、例えば長いコードにおいてある関数について定義元にコードジャンプすると、参照した後に参照前の位置に戻るためにスクロールする必要があり、面倒です。また、同時に参照することができず、コード内を行ったり来たりしないといけなくなり、大変です。わかりにくいかもしれないので実際に見てみましょう。次のようなコードがあったとします。
def function1(x: str):
print(f'Hello, {x}!')
def function2(x: str):
# Long code begins...
# ...(実際は数十行の空行がある)...
# Long code ends
print(x)
def function3(x: str):
if x == '':
print("string is empty")
else:
print("string is not empty")
def main():
# The function whose definition you want to know
# if you right-click on function1 below and select "Go to Definition"
# you will jump to the beginning of this file in the same tab.
function1("World")
if __name__ == "__main__":
main()
このコードではmain()
の中に、コードの冒頭で定義されたfunction1()
という関数が呼び出されています。function1()
の定義を参照したいとき、function1()
を右クリックすると、context menu
が出ます。
ここでGo to Definition
をクリックすると、次のように定義元のあるコードの冒頭に飛びます。
こうすると、main()
内のfuntion1()
を表示しながら定義元も同時に見ることができず、また、参照前の位置に戻るのも面倒です。
そこで、定義元を参照すると、別タブで画面分割して同時に表示できる機能を実装したいと思ったのですが、ソースコードを探っていくと、この動作を実行できるOpen Definition to Side
という機能が既に存在することがわかりました。しかしこの機能はコマンドパレットからしか実行できず不便なため、この機能をcontext menu
からも呼び出せるようにしたら便利だと考えました。
今回はオープンソースソフトウェアであるVSCodeに、定義元を参照すると、別タブで画面分割して同時に表示できる機能を右クリックで実行できるよう実装しました。
ソースコードを探り、理解する
当初はOpen definition to Side
という機能の存在を知らなかったため、まずはGo to Definition
がどう実装されているかを知り、右クリックで展開されるContext menu
に新しいアクションメニューを追加しようと考えました。Ctrl+F
でGo to Definition
と検索し、探ると次のようなコードが見つかりました。
registerAction2(class GoToDefinitionAction extends DefinitionAction {
static readonly id = 'editor.action.revealDefinition';
constructor() {
super({
openToSide: false,
openInPeek: false,
muteMessage: false
}, {
id: GoToDefinitionAction.id,
title: {
value: nls.localize('actions.goToDecl.label', "Go to Definition"),
original: 'Go to Definition',
mnemonicTitle: nls.localize({ key: 'miGotoDefinition', comment: ['&& denotes a mnemonic'] }, "Go to &&Definition")
},
precondition: ContextKeyExpr.and(
EditorContextKeys.hasDefinitionProvider,
EditorContextKeys.isInWalkThroughSnippet.toNegated()),
keybinding: {
when: EditorContextKeys.editorTextFocus,
primary: goToDefinitionKb,
weight: KeybindingWeight.EditorContrib
},
menu: [{
id: MenuId.EditorContext,
group: 'navigation',
order: 1.1
}, {
id: MenuId.MenubarGoMenu,
group: '4_symbol_nav',
order: 2,
}]
});
CommandsRegistry.registerCommandAlias('editor.action.goToDeclaration', GoToDefinitionAction.id);
}
});
この関数を探しているときに、この下に`Open Definition to Side`を定義する関数がありました。以下に示すのがその部分のコードです。
registerAction2(class OpenDefinitionToSideAction extends DefinitionAction {
static readonly id = 'editor.action.revealDefinitionAside';
constructor() {
super({
openToSide: true,
openInPeek: false,
muteMessage: false
}, {
id: OpenDefinitionToSideAction.id,
title: {
value: nls.localize('actions.goToDeclToSide.label', "Open Definition to the Side"),
original: 'Open Definition to the Side'
},
precondition: ContextKeyExpr.and(
EditorContextKeys.hasDefinitionProvider,
EditorContextKeys.isInWalkThroughSnippet.toNegated()),
keybinding: {
when: EditorContextKeys.editorTextFocus,
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, goToDefinitionKb),
weight: KeybindingWeight.EditorContrib
}
});
CommandsRegistry.registerCommandAlias('editor.action.openDeclarationToTheSide', OpenDefinitionToSideAction.id);
}
});
この部分に注目すると、class OpenDefinitionToSideAction
というクラスを定義して、そのクラスをregisterAction2
という関数に引数として渡していることがわかります。
このregisterAction2
という関数は vs/platform/actions/common/actions で定義されているのをimportしている上に、関数の名前などからこれはActionを表すクラスからショートカットキーや右クリックメニューにアクションを追加するという処理を行っていると考えられます。
すなわち、Actionという共通の動作を表すクラスが存在し、右クリックやコマンドパレットからの実行、ショートカットキーの実行は全てこのクラスを参照し、このクラス内で指定している関数が実行されるようになっているであろうということです。
いざ実装!
今回はOpen Definition to Side
というアクションが、コマンドパレットからは実行できるにも関わらず右クリックのメニューに表示されないことが問題でしたから、Go to Definition
とOpen Definition to Side
を見比べて、適切な宣言や処理を追加すれば良いのではという考えのもと、コードを改変しました。
registerAction2(class OpenDefinitionToSideAction extends DefinitionAction {
static readonly id = 'editor.action.revealDefinitionAside';
constructor() {
super({
openToSide: true,
openInPeek: false,
muteMessage: false
}, {
id: OpenDefinitionToSideAction.id,
title: {
value: nls.localize('actions.goToDeclToSide.label', "Open Definition to Side"),
original: 'Open Definition to Side',
+ mnemonicTitle: nls.localize({ key: 'miOpenDefinitionToSideAction', comment: ['&& denotes a mnemonic'] }, "Open &&Definition to Side")
},
precondition: ContextKeyExpr.and(
EditorContextKeys.hasDefinitionProvider,
EditorContextKeys.isInWalkThroughSnippet.toNegated()),
keybinding: {
when: EditorContextKeys.editorTextFocus,
- primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, goToDefinitionKb),
+ primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyK,
weight: KeybindingWeight.EditorContrib
},
+ menu: [{
+ id: MenuId.EditorContext,
+ group: 'navigation',
+ order: 1.1
+ }, {
+ id: MenuId.MenubarGoMenu,
+ group: '4_symbol_nav',
+ order: 2,
+ }]
});
CommandsRegistry.registerCommandAlias('editor.action.openDeclarationToTheSide', OpenDefinitionToSideAction.id);
}
});
上の実装では、DefinitionActionのコンストラクタに渡すオブジェクトにmenuというメンバを追加し、そこでidやどのグループに表示するか、表示順はどうするかなどの情報を追加しています。
こうすることで、右クリックした時のコンテキストメニューにOpen Definition to Sideが追加されます。
デモンストレーション
では、実際に動かしてみましょう。
右クリックすると、context menu
にOpen Definition to Side
というアクションメニューが追加されており、これをクリックすると画面分割して現在の位置と定義元の位置が同時に表示できていますね!これなら参照しながらわかりやすくコードを書くことができると思います。
ビルド方法について
ビルドのやり方についてはこの記事で説明しています。