VSCodeに”関数のはじめにジャンプする"機能を追加しようと試行錯誤
TL;DR
- これはEEIC学部実験の記録です
- OSSであるVisual Studio Codeを機能拡張してみようという試みです
- "関数のはじめにジャンプする"機能の実装はできました
はじめに
このエントリは、東京大学工学部電気電子工学科・電子情報工学科の学部実験のうちの一つ、 大規模ソフトウェアを手探るの振り返り記事です。
もともとVisual Studio Code(以下VSCode)のLive Share機能に興味があって、面白いことができないかと集まったメンバーですが、 10日間×4時間+αの実験期間に、紆余曲折を経てタイトルのような機能を追加実装しました。
以上のことや、3人で執筆していることなども念頭に、読んでいただければ幸いです。
課題の設定まで
Visual Studio Codeについて
Visual Studio CodeはMicrosoft社が中心となって開発しているテキストエディタで、現在最も人気のある開発環境の一つに挙げられています。
デフォルトで多機能でありながらも、比較的軽量で、Windows・MacOS・Linux(β版)にも対応しています。
統合開発環境Visual Studioからエディタ部分を切り出したもので、OSSの一つでありGitHubにコードが公開されています。
主にTypeScriptによって記述されています。
Live Shareの断念
今年5月にPublic Previewとなり、話題になったLive Share機能。 しかし、今回の実験で扱う題材としては、以下の理由から取りやめとなりました
- Live Share機能は実験開始時(2018/09)あるいは執筆時点(2018/11)で、拡張機能(Extensions)の一つであること
- 拡張機能をいじることは、実験趣旨の”大規模ソフトウェアを手探る”にそぐわなさそう
- デバックが大変そう
Issueから課題の設定
普段からVSCodeを利用していることもあり、また開発も活発なこともあって、VSCodeを”手探る”方向で行こうということになりました。
そこで、課題を模索する、すなわちどんな機能追加・改善をしていきたいかを考える中で、GitHub上でのIssueに着目しました。
約4000のIssueがOpenになっており、 is:issue is:open no:assignee
といった絞り込みでヒットしたIssueのうち、手ごろそうなものをピックアップしていきました。
以上を経て次のIssueに取り組んでみようということになりました。
簡単にいうと「今いる関数の最初に飛ぶことができたら便利じゃないですか?」のような形です。
VSCodeには"定義に飛ぶ"機能がもともとあるので、それを利用したら行けるんじゃないかなという憶測もあり、実装可能と予想しました。
VSCodeのデバッグ方法
上で述べたとおり、VSCodeはcontributerに対してデバッグ手法を手厚くサポートしています。VSCodeを用いてVSCodeのデバッグができます。
基本的には このページ にご丁寧に全部書いてあるので従うだけです。
typescriptのパッケージマネージャYarnを用いて yarn watch
とすると単にビルドするだけでなくて、変更を監視してその部分だけ自動で再ビルドしてくれてすぐにデバッグできます。めちゃくちゃ便利。
デバッグするときにはドロップダウンメニューから Launch VS Code
を選択して実行するだけ。実行後も Ctrl + Shift + I
で開くDeveloper ToolsによってGoogle ChromeでF12を押して出てくるのと同様の開発者ツールが登場して快適にデバッグができます。素晴らしいね。
ビルドについてのメモ
私の環境(Ubuntu18.04)ではGCC, makeの他に sudo apt-get install -y libx11-dev libxkbfile-dev libsecret-1-dev fakeroot rpm
と、
Wikiに記載されているすべてのパッケージをインストールする必要がありました。
着手前の調査
Breadcrumbs List(階層リスト)との出会い
いきさつ
現在やろうとしていることを、Kurogoma氏と話しているなかで持ち出すと、
似たような機能もうあるよ
と教えていただき、調べてみるとBreadcrumbs Listという機能がJuly 2018 (ver 1.26) Updateにて導入されたことを知りました。
Breadcrumbs Listとは
日本語に直訳すると「パンくずリスト」。Weblio辞書によると
パンくずリストとは、Webサイトにおける現在ページの位置を、Webサイトの階層を示すことによってガイドする表示のことである。
[View]メニューの Toggle Beradcrumbs
をクリックするとonになります。
便利なのでぜひ使ってほしい!!! (宣伝)
Breadcrumbs Listの構造
開いたファイルやディレクトリのrootからの各階層が Item
型を要素とする配列に格納されています。
Item型の element
というメンバにおいて、各要素の情報が管理されており、その型は
- ファイルやディレクトリ: FileElement
- クラス, 関数, 変数などファイル内の要素: OutlineElement
となっています。
OutlineElement型のメンバ symbol.kind
でその要素がクラスなのか関数なのかその他なのか....が数字で管理されているので、それを参照することで要素の種類を判別することができます。例えばclassは4, arrayは17というように。
src/vs/monaco.d.ts
に enum SymbolKind
とenum型配列として定義されているので興味のある人は覗いていてみては。
Go to Definitionという別の方法
今まで述べてきたBreadcrumbs Listを拡張していく、という手法の他に、 Go to Definition機能を拡張していくといく手法も考えました。
ただ、Breadcrumbs Listに必要な情報が揃っている(だろう)ことと、 実装したい内容はBreadcrumbs Listの延長であることが自然だろうことから、 今回では採用しませんでした
実装
実装の目標
似たような機能が既に実装済みだったのですが、 求めている動作(関数の始めにジャンプする)を実現するには複数の手順を要したので、
ワンアクションにより関数のはじめにジャンプする
を目標としました
実装のながれ
大きく分けて3つの実装を行いました。
- ファイル階層と現在位置の取得
- 所属する関数の階層の取得
- 取得した階層へ画面のフォーカスを移動
1. ファイル階層と現在位置の取得
ここはBreadcrumbs List(n度目)のメニューを流用しました。
もともと、ショートカットキーにより、
Breadcrumbs Picker
と呼ばれている(っぽい)プルダウンメニューを開き、
現在いる箇所にフォーカスされた状態でのPickerメニューを開くものがありました。
ここで現在いる箇所とは、変数宣言しているところであれば変数であったり、 あるいは現在カーソルが合わせてある所の関数であったりします。 これを流用する、つまりPickerメニューでフォーカスされたところを取得すればいいことになります。
また、現在いる箇所は BreadcrumbsWidget
クラスの getItems()
メソッドを実行すれば、
ファイル内の階層形式でとりあえず取得できました。
2. 所属する関数の階層の取得
前項で取得したItem
の element
では、fileやdirectoryは FileElement
型、
関数・変数・クラスなどは OutlineElement
型といって別れているものを一括で扱っています。そして、これは木構造になっています。
よってキャストしてあげて OutlineElement
型で扱ってあげます。
すると以下のようにプロパティをたどれば、必要な情報にアクセスできます。
OutlinElement └ element ├ parents └ symbol ├ kind(int) ├ name(strings) └ range(range Obj) └ startLineNumber(int)
element.symbol.kind
プロパティでは要素の種類を、対応する整数値で持ちます。monaco.d.ts
のSymbolKind
(前述)- ここを見て、参照しているものが関数・メソッドか否かを判断
element.symbol.range.startLineNumber
プロパティでカーソルを飛ばす先の行数を取得- 現在いる場所が関数ではない場合、
element.parent
プロパティで親要素を参照する。 - 無名関数を判別するため、
element.symbol.name
が<function>
になっていないかを判定
3. 取得した階層へ画面のフォーカスを移動
当初は、目的とする行数を引数にとって、該当箇所に飛んでくれるような関数が実装されているのではないか?ということを見越して調査をしていました。 しかし、そのような大元の関数は、我々の弄ろうとしているレイヤーからは直接叩くことができないということがだんだんわかってきました。
ここで途方に暮れていたところ、別のキーバインディングの処理を見ると、KeybindingsRegistry.registerCommandAndKeybindingRule
メソッドの引数のクラスのhandlerメソッドの返り値が以下のようなものであることがわかりました。
return editors.openEditor({ resource: OutlineModel.get(element).textModel.uri, options: { selection: Range.collapseToStart(element.symbol.selectionRange) } }, SIDE_GROUP);
これはBreadcrumbsPickerが表示された状態でCtrl+Enter
で発火するイベントだったようなので、これを実行してみると、エディターが2つに分割され、新たに作られた右側の部分でカーソルがPickerでハイライトされていた関数の位置にあることがわかりました。
これをコピー&ペーストして、エディターが2分割された上でその関数の位置に飛ぶ機能は実装されました。
しかし、エディターが分割される意味はないので、今の画面のまま飛べるようにしたいところです。
ここで、このSIDE_GROUP
の定義を表示すると、以下のようなものが出てきました。
export const ACTIVE_GROUP = -1; export type ACTIVE_GROUP_TYPE = typeof ACTIVE_GROUP; export const SIDE_GROUP = -2; export type SIDE_GROUP_TYPE = typeof SIDE_GROUP;
ここで、SIDE_GROUP
とACTIVE_GROUP
の2つが存在しているようだということがわかりました。
今までの挙動は、SIDE_GROUPを引数にとるとエディターが分割されていました。ということは、ACTIVE_GROUPを引数にとれば、現在カーソルを置いているグループが変わらずに飛んでくれるのでは…?ということを考えました。
そこで、追記したメソッドの返り値を以下のようにしました。
return editors.openEditor({ resource: OutlineModel.get(element).textModel.uri, options: { selection: Range.collapseToStart(element.symbol.selectionRange) } }, ACTIVE_GROUP);
このようにすると…
Ctrl+Shift+,
で関数の頭にカーソルが合うようになりました!
めでたしめでたし。
感想
まず、色々なものが抽象化されているなということに気がつきました。
例えば、何らかのインスタンスをgetするときにも、accessor.get
というメソッドが用意されていて、これを通じて行うようになってしまいました。こうすることで、複数乱立してはいけないようなインスタンスにアクセスするAPIを確保できていました。
また、関数のスコープが明確に意識されているのも印象的でした。各レイヤーでそれぞれどの範囲のことまでできるようになっているのかが(コードを読んでみなければわからないものの)厳格に規定されていて、そしてスコープを巧みに用いることで、レイヤー間を越境するような書き方をすることがそもそも不可能になっていました。
このようにすることで、膨大なコード量でもエラーが極力起きないようにできているということがわかりました。
ただ、人の設計した超抽象化されたコードはやっぱり読みにくい!!
その関数・メソッドがどの範囲のことまで手を出していいのかということが、理詰めでコードを読もうとしてもいかんせんわかりにくい。高レイヤーの部分から直接html要素のDOMを弄るということはできないわけですが、じゃあどこまでの部分だったら直接DOMを弄れるのか/どこのレイヤーがこの関数を使えるのかということが大いに不明瞭です。