【VSCode】定義参照を簡単に！

>この記事は、東京大学工学部電子情報工学科/電気電子工学科の後期実験「大規模ソフトウェアを手探る」のレポートとして作成されました。

# 定義参照を楽にしたい

VSCodeでコードを書いているとき、おそらく皆さんのほとんどが関数やクラス、変数などを使うと思います。これらを使う際に、それぞれの定義を参照する機会も多いでしょう。<br />
定義を参照したいとき、よく使われるのが`Go to Definition`や`Go to Type Definition`などの定義元にコードジャンプする機能だと思います。しかし、例えば長いコードにおいてある関数について定義元にコードジャンプすると、参照した後に参照前の位置に戻るためにスクロールする必要があり、面倒です。また、同時に参照することができず、コード内を行ったり来たりしないといけなくなり、大変です。わかりにくいかもしれないので実際に見てみましょう。次のようなコードがあったとします。

```diff_python
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`が出ます。<br />

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2889067/5cc1c7e4-d36e-2e49-1c6c-898d21148190.png" width="75%">


ここで`Go to Definition`をクリックすると、次のように定義元のあるコードの冒頭に飛びます。<br />

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2889067/a427c923-089c-3550-d37d-58a02a5a6dd1.png" width="75%">

こうすると、`main()`内の`funtion1()`を表示しながら定義元も同時に見ることができず、また、参照前の位置に戻るのも面倒です。

そこで、定義元を参照すると、別タブで画面分割して同時に表示できる機能を実装したいと思ったのですが、ソースコードを探っていくと、この動作を実行できる`Open Definition to Side`という機能が既に存在することがわかりました。しかしこの機能はコマンドパレットからしか実行できず不便なため、この機能を`context menu`からも呼び出せるようにしたら便利だと考えました。

今回はオープンソースソフトウェアであるVSCodeに、定義元を参照すると、別タブで画面分割して同時に表示できる機能を右クリックで実行できるよう実装しました。

# ソースコードを探り、理解する
当初は`Open definition to Side`という機能の存在を知らなかったため、まずは`Go to Definition`がどう実装されているかを知り、右クリックで展開される`Context menu`に新しいアクションメニューを追加しようと考えました。`Ctrl+F`で`Go to Definition`と検索し、探ると次のようなコードが見つかりました。

```diff_typescript
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);
	}
});
```
<br />
この関数を探しているときに、この下に`Open Definition to Side`を定義する関数がありました。以下に示すのがその部分のコードです。
<br />

```diff_typescript
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`を見比べて、適切な宣言や処理を追加すれば良いのではという考えのもと、コードを改変しました。

```diff_typescript
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が追加されます。

# デモンストレーション
では、実際に動かしてみましょう。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2889067/b50bd403-7156-174a-1a7d-ecca3055c5c8.gif">

右クリックすると、`context menu`に`Open Definition to Side`というアクションメニューが追加されており、これをクリックすると画面分割して現在の位置と定義元の位置が同時に表示できていますね！これなら参照しながらわかりやすくコードを書くことができると思います。

# ビルド方法について
ビルドのやり方については[この記事](https://qiita.com/joietaking/private/242371cf48931d481aa6)で説明しています。