GitHub Copilotを使ったコードリーディングのコツ その1

# はじめに

OSSなどのコードを読んでいる時に、初見のライブラリを使っていたり、普段使わないようなアーキテクチャに沿って書かれたクラスだったりすると一目ではその機能が何をやっているのか理解しにくい場面があります。

この問題を、GitHub Copilotを使って解決するコツを少しだけ紹介しようと思います。

# 対象となるクラス

今回題材として使うのは、`vscode-snippet`というVSCodeの拡張機能のリポジトリです。
その中の、`snippetsStorage.ts`にある`SnippetsStorage`クラスについて見ていきます。

https://github.com/mre/vscode-snippet/blob/master/src/snippetsStorage.ts

# GitHub Copilot LabsのAsk Copilotで試してみる

GitHub Copilot Labsという拡張機能には、`Explain this code`という機能が存在します。

ただ、これを使ってこのクラスを丸ごと説明させようとすると、次のようなエラーが出てしまいました。

```
Error generating Copilot response, please try again. Changes in the prompt or stop sequence might fix this. If this error happens repeatedly check that you are logged into GitHub and have access to both Copilot and Copilot Labs.
```

特定の関数や特定のブロックについての説明には機能してくれますが、大きすぎるとうまく機能してくれないのかもしれません。

# 通常のGitHub Copilotの機能を駆使する

そこで、プロンプトをうまく利用しながら、通常のGitHub Copilotを使ってこのクラスを説明させようと思います。

具体的には、ファイルの末尾に以下のコメントを挿入します。

```ts
...
  private isFolder(element: TreeElement): boolean {
    return element.childIds != null;
  }
}

/*
Write a description about SnippetStorage's each method here.
1. 
*/
```

すると、`1.`と書いた瞬間に各メソッドの名前と説明が自動的に表示されるようになります。

もし、全く関係のないメソッド名と説明を表示しようとしたら、クラスに実際に存在するメソッド名を入力してあげると、GitHub Copilot側で解釈に対する修正を行い正しい説明文を表示してくれるようになります。

最終的に次のようなプロンプトが生成されました。

```ts
/*
Write a description about SnippetStorage's each method here.
1. getFoldersList: This method will return a list of folders that are children of the provided folder id. If no id is provided, it will return the root folders.
2. populateFoldersList: This method will populate the provided tree view with the folders and snippets that are children of the provided folder id. If no id is provided, it will populate the tree view with the root folders.
3. getElement: This method will return the element with the provided id.
4. deleteElement: This method will delete the element with the provided id. If the element is a folder, it will delete all the children of the folder too.
5. renameElement: This method will rename the element with the provided id to the provided name.
6. createFolder: This method will create a folder with the provided name. If the relativeToId is provided, the folder will be created as a child of the provided id. Otherwise, the folder will be created as a root folder.
7. moveElement: This method will move the element with the provided sourceId to the provided targetId. If the targetId is a folder, the element will be moved to the folder. Otherwise, the element will be moved to the parent of the targetId.
8. saveSnippet: This method will save a snippet with the provided content, file extension, label, and parent id.
9. getSnippet: This method will return the content of the snippet with the provided id.
10. save: This method will call the serialize method to get the string representation of our data then call the update method from the context to save it to the global state.
11. load: This method will call the deserialize method to load the data from the global state.
12. loadDefaultElements: This method will load the default elements to the storage. The default elements are the root folder, an example folder, and an example snippet.
13. serialize: This method will convert our data to a string.
14. deserialize: This method will convert the provided string to our data. The type the provided string will be converted to is TreeElement[].
15. isFolder: This method will return true if the provided element is a folder. Otherwise, it will return false.
*/
```

日本語に直すと次のようになります。

| メソッド名 | 概要 |
|:--|:--|
| getFoldersList | このメソッドは、提供されたフォルダIDの子フォルダのリストを返します。IDが提供されない場合は、ルートフォルダを返します。 |
| populateFoldersList | このメソッドは、指定されたフォルダIDの子であるフォルダとスニペットを、指定されたツリービューに入力します。idが提供されない場合、それはルートフォルダでツリービューをポップアップします。 |
| getElement | このメソッドは、提供されたIDの要素を返します。 |
| deleteElement | このメソッドは、提供されたIDの要素を削除します。要素がフォルダの場合、フォルダのすべての子要素も削除されます。 |
| renameElement | このメソッドは、提供されたIDの要素を提供された名前にリネームします。 |
| createFolder | このメソッドは、提供された名前のフォルダを作成します。relativeToIdが提供されている場合、フォルダは提供されたIDの子として作成されます。そうでない場合、フォルダはルートフォルダとして作成されます。 |
| moveElement | このメソッドは、提供されたsourceIdの要素を提供されたtargetIdに移動します。targetIdがフォルダの場合、要素はフォルダに移動されます。そうでない場合、要素はtargetIdの親に移動されます。 |
| saveSnippet | このメソッドは、提供されたコンテンツ、ファイル拡張子、ラベル、および親IDを持つスニペットを保存します。 |
| getSnippet | このメソッドは、提供されたIDのスニペットのコンテンツを返します。 |
| save | このメソッドは、serializeメソッドを呼び出してデータの文字列表現を取得し、コンテキストのupdateメソッドを呼び出してグローバル状態に保存します。 |
| load | このメソッドは、deserializeメソッドを呼び出してグローバル状態からデータをロードします。 |
| loadDefaultElements | このメソッドは、デフォルトの要素をストレージにロードします。デフォルトの要素は、ルートフォルダ、サンプルフォルダ、およびサンプルスニペットです。 |
| serialize | このメソッドは、データを文字列に変換します。 |
| deserialize | このメソッドは、提供された文字列をデータに変換します。提供された文字列が変換される型は、TreeElement[]です。 |
| isFolder | このメソッドは、提供された要素がフォルダである場合はtrueを、そうでない場合はfalseを返します。 |

# おまけ

ちなみに、ChatGPTを使ってVSCode上で各メソッドについて説明を求めてみました。

https://marketplace.visualstudio.com/items?itemName=takagimeow.chatgpt-editor

結果は次のようになりました.

```
/*
Write a description about SnippetStorage's each method here.
- constructor: Initializes the SnippetsStorage object with the provided context and storage key, loads the data from the global state, and loads default elements if no data is found.
- getFoldersList: Returns a list of folders as QuickPickItems.
- populateFoldersList: Populates the provided list with the subfolders of the provided parent element recursively.
- getElement: Returns the element with the provided ID or the root element if no ID is provided.
- deleteElement: Deletes the element with the provided ID, including all its children if it's a folder.
- renameElement: Renames the element with the provided ID with the provided new name.
- createFolder: Creates a new folder with the provided name relative to the element with the provided ID or the root element if no ID is provided.
- moveElement: Moves the element with the provided source ID to the element with the provided target ID or its parent if it's a snippet.
- saveSnippet: Saves a new snippet with the provided content, file extension, label, and parent ID.
- getSnippet: Returns the content of the snippet with the provided ID.
- save: Serializes the data, saves it to the global state, and calls the onSave callback.
- load: Loads the data from the global state.
- loadDefaultElements: Loads default elements if no data is found.
- serialize: Serializes the data to a JSON string.
- deserialize: Deserializes the JSON string to a list of TreeElements and sets the root ID and elements map. 
- isFolder: Returns true if the provided element is a folder.
*/
```

日本語に直すと次のようになります。

| メソッド名 | 概要 |
|:--|:--|
| constructor | コンテキストとストレージキーが提供された場合は、SnippetsStorageオブジェクトを初期化し、グローバル状態からデータを読み込み、データが見つからない場合はデフォルトの要素を読み込みます。 |
| getFoldersList | フォルダのリストをQuickPickItemsとして返します。 |
| populateFoldersList | リストに、親要素のサブフォルダを再帰的に追加します。 |
| getElement | IDが提供された場合は、そのIDの要素を返します。IDが提供されなかった場合は、ルート要素を返します。 |
| deleteElement | IDが提供された要素を削除します。要素がフォルダの場合は、すべての子要素も削除されます。 |
| renameElement | IDが提供された要素の名前を、提供された新しい名前に変更します。 |
| createFolder | IDが提供された場合は、その要素の下に、提供された名前の新しいフォルダを作成します。IDが提供されなかった場合は、ルート要素の下に新しいフォルダを作成します。 |
| moveElement | source IDが提供された要素を、target IDが提供された要素に移動します。要素がスニペットの場合は、親要素に移動されます。 |
| saveSnippet | content、file extension、label、およびparent IDが提供された新しいスニペットを保存します。 |
| getSnippet | IDが提供されたスニペットの内容を返します。 |
| save | データをシリアライズし、グローバル状態に保存し、onSaveコールバックを呼び出します。 |
| load | グローバル状態からデータを読み込みます。 |
| loadDefaultElements | データが見つからない場合は、デフォルトの要素を読み込みます。 |
| serialize | データをJSON文字列にシリアライズします。 |
| deserialize | JSON文字列をTreeElementのリストにデシリアライズし、root IDと要素マップを設定します。 |
| isFolder | 要素がフォルダである場合はtrueを返します。 |

例えば、`getFoldersList`などの概要などは改良の余地が見受けられますが、より具体的な内容を返してくれている場合もあり甲乙つけがたい状況です。

# まとめ

GitHub Copilotを使うとコードリーディングの幅が大きく飛躍します。
プロンプトを工夫することで、全く異なった結果を得ることができるので、読者の皆さんも是非自分なりのプロンプトを見つけ出してみてください。

# 参考にした記事

https://githubnext.com/projects/copilot-labs

https://platform.openai.com/docs/guides/completion/prompt-design

https://help.openai.com/en/articles/5072263-how-do-i-use-stop-sequences

https://openai.com/blog/openai-codex