はじめに
こちらは、東京大学工学部電気電子工学科・電子情報工学科の後期実験のうちの一つ「大規模ソフトウェアを手探る」の成果報告記事です。世の中にはOSS(オープンソースソフトウェア)と呼ばれる、ソースコードが一般に無償公開されているソフトウェアが多数存在します。その中から一つ選び、全容を把握出来るわけがない程大きなソフトウェアをいかに理解し、いかに変更を加えられるかという趣旨の実験です。
VSCodeとは
VSCode(Visual Studio Code)とはMicrosoftが開発しているソースコードエディタで、軽量かつ多機能という特徴を持ち、現在最も多くの人に使われている開発環境の内の一つです。
課題設定
GitHubのissueを見ながら改善案を考えていきました。issueでは
good first issue
とラベルのついたものが最初に取り組むには良いissueということなのですが、この中にめぼしいものがなかったので、
add, button
等で検索していきました。そうするとこんなissueが見つかりました。
この方はLaTeXで数式を書くときに変数のシンボルを入れ替えたい時があり、word-swapping機能が欲しいということでした。これは既存のreplace機能を活用すれば実装できるのではないかと思い、課題としてswap機能の追加に取り組むことにしました。
デバッグ環境構築
公式が出しているcontributerのためのこちらのページを参考に環境構築していきました。
1.必要なツールのインストール
まずは
- git
- yarn
- nodejs(14.x-17.x)
を入れます。他に必要なものはOSによって異なるのですが、linuxの方は
$ sudo apt-get install -y g++ gcc make python2.7 pkg-config libx11-dev libxkbfile-dev libsecret-1-dev
で全て入ると思います。
2.ビルド
$ git clone https://github.com/microsoft/vscode
$ cd vscode
$ yarn
$ yarn watch
を打つとビルドができます。時間はかかりますが、
… [watch-extensions] [00:59:36] Starting watch-extension:html-language-features-server ...
と表示されれば成功です。もし今後yarn watchでlimit number for watchというエラーが出てきたら
[watch-extensions] [01:00:03] Finished compilation with 0 errors after 33816 ms
[watch-client] [01:01:12] Finished compilation with 0 errors after 100775 ms
$ yarn run compile
でもビルドすることはできます。
無事ビルドできたら./scripts/code.shでビルドしたvscodeが立ち上がるはずです。
3.デバッグのやり方
VScode上でやるやり方とChrome Developer Toolsを使ってやるやり方があります。VScode上で行う場合は、run and debugモードに入り、Launch VScodeを実行するとデバッグすることができます。
Chrome Developer Toolsで行う場合は
$ ./scripts/code.sh
でvscodeを立ち上げたあとにCtrl+Shift+iでdebugモードに入れます。あとはsource欄からファイルを開いて、breakpointを置いて実行したりすれば無事デバッグをすることができます。
既存の機能の調査
まず既存の機能の調査を行うことにしました。今回、swap機能は既存のreplace機能に似ているものだと考えました。replace機能というのは以下のような置換を行うVSCodeの機能です。
そこでまず初めにreplace機能がどこで実装されているのか見ていくことにしました。そうすると、replaceCommand.tsといういかにもなファイルを見つけ、この中にbreakpointを設定して既存のreplaceを実行してみたところ、見事動作がそこで止まったのでそこからコードを追いかけることにしました。
replace機能
replace機能のcall stackは以下のような流れになっていました。ここで実はreplace機能はfind機能の仲間で、find~というファイルは全てfind-replace機能に関するものが書かれているのですが、勿論これに気づくのは大分後になってからのお話です。
/src/vs/editor/contib/find/findController.ts(handler)
- キーボード入力と呼び出すModelの関数の紐づけ
↓
/src/vs/editor/contib/find/findController.ts(replace)
- Modelの呼び出し
↓
/src/vs/editor/contrib/find/findModel.ts(replace)
- Commandの呼び出し
- 及びexecute_commandの実行(この関数によって直接的に機能が実行され、またここで定義する名前とコマンドが紐づけられてどこかに登録される)
↓
/src/vs/editor/common/commands/replaceCommand.ts(ReplaceCommand)
- replaceするためのコマンドを作成しているクラスがいくつか書かれている
そうしてreplaceの挙動を追っていくうちにreplaceとreplaceAllでは呼び出すCommandファイルが異なることに気づきます。
見ていくとreplaceAll機能の方が単純な実装となっていたので、こちらから先に手を出すことにします。
replaceAll機能
findModelまでの流れはreplaceと同じで、最後に呼び出すcommandがreplaceAllCommand.tsから呼ばれていました
そこでreplaceAllCommand.tsの中身の実装を見ていくことに
replaceAllCommand.ts
ここではclassが一つしか定義されておらず、そのclassの中にbreakpointを打ってreplaceAllを実行してみると、、
constructorの引数はeditorに関する情報を持つeditorSelection、置換する場所をRangeというクラスの配列にまとめたranges、置換する文字列をStringの配列としてまとめたreplaceStringsになっていました。ここで中身を見ていくと、
上記のような場合ではrangesは[(3個目の場所), (5個目の場所)]、replaceStringsは["apple", "apple"]となっていました。これは予想外で、というのも普通ならappleをstringで渡すだけだと思ってたからです。なのでrangeとstringが一個ずつ対応させられてるのだと思い、ここの渡し方を変更したらswapの挙動は作れるのではないかと考えました。そこで上記のような場合にrangesを2つの単語のrangeをくっつけて[(3個目の場所), (5個目の場所), (1個目の場所), (2個目の場所), (4個目の場所)]、replaceStringsを["apple", "apple", "banana", "banana", "banana"]という形で渡したところ、見事求めていたswapの挙動を示したのでこの方向性で行くことに。
とりあえずの実装としてまだ入力部について見てはいないので、
仕様を上記のfindInputとreplaceInputに文字を入力し、Ctrl+Shift+Enterを押すとswapAllが実行されるように定め、あとは、replaceAllの真似をしてswapAllCommand.tsやfindModel.ts, findController.tsにswapAll関数を実装しました。
UI
swapAll関数の実装が出来たところで今度はUIについて考えていくことにします。現状ではswapしたい文字列をfindInputとreplaceInputに入力しています。しかし、機能を明確にするためにもswap専用のInputが欲しかったので、新たにswapInputを作ることにしました。
既存のUIの調査
同じようにUIの部分を探っていくと、どうやらfindWidget.tsというファイルでUIを設計しているようでした。ここではClassとして作成されたUIの様々なボタンやInputBoxなどを用いてDOMによってUIが作成されています。なのでこれに付け足す形でswapInputを作ることができるだろうと考えます。
UIの作成
コードの空気を読んで関係のありそうなところを一つ一つ変更していくと無事とりあえずのswapInputができました。しかしUIに関してはここから様々な問題が見つかり、何度も変更を加えることに。。。
UIとModelの修正に次ぐ修正
一見うまくいったかのように思えた実装でしたが、様々な意図しない挙動が起きる可能性に気づきます。具体的には
- 元のfindはdefaultでは大文字と小文字を区別しないため、swapするつもりのない文字までswapされる
- wholewordボタンを押さないと、入力された文字列が単語の部分文字列だったとしても単語の一部がswapされる
- findに正規表現入力するとおかしくなる
- swapInputに文字を入れない、もしくはコード内に存在しない文字列が入力されたときに、空文字とのswapという挙動になる
- そもそもswapInputに入れた文字が光らないので、どれがswapされるのか分かりずらい
などの問題が発生すると考えられました。なので、実装過程は省略しますが、結果としては以下のような仕様にしました。
- findInputのMatch Caseが押されている時のみ(この時、大文字小文字は区別して扱われます)swapすることが出来るように
- findInputの正規表現がonになっているときはswapすることが出来ないように
- swapInputにもWhole Wordボタン(文字列が部分文字列でなく完全に一致してるときのみ範囲に入れるか決めるボタン)を作成し、部分文字列のswapを許容するかどうか選べるように
- swapInputにおいてはMatch Caseが内部で常にonになるように(swapInputでは強制的に大文字小文字を区別するように)
- swapInputに入れた文字と一致する文字列が光るように
これらによって意図しないswapが起きることを出来る限り最小限にしました。
ここまでで完成した物の動作は以下のようになります。
これにてswapAllの実装を終えて、本実験は終了しました。
実装できなかったこと
単語をそれぞれ選んで一個ずつswapするという機能も実装したかったので、相当な時間を費やしたんですが結局上手くいかなかったです。失敗した原因としては色々考えられるんですが、主に元の実装が互換性を考えられてなかったというのがあると思います。というのも、元々はfindに文字が入力されるとそれを一個ずつ選べるようになっているんですが、これの実装が他の単語も選ぶということが一切考慮されずに作られていたので、そこを拡張する部分が難しかったです。
感想
VSCodeのコードは非常に膨大で(約160万行)、また非常に抽象化されていたのでコードの意味を理解するのがとても大変でした。しかし非常に互換性や拡張性が高く、オブジェクト指向の強みを垣間見ることができました。結局、最終的な実装として加えた行数は1232行!元のコードの真似が多かったとはいえ、結構頑張った方なのではないでしょうか。実験の感想としては3人ともOSSに触ったことがなかったので、正直最初は完成させることが出来るかとても不安だったのですが、最終的にはある程度形が出来たものを作ることが出来たのでよかったなと思います(色々助けて頂いたTAの方と先生には本当に感謝しています)。とても楽しかったのは勿論なのですが、一回OSSに触れるという経験を得たお陰で大規模ソフトウェアをいじることに対する敷居が低くなって、他のOSSもいじってみたいなという気持ちになりました。
PR送ってみた
せっかくだったのでプルリクエストを送ってみました。
進展があり次第どんどん追記していきたいと思います。
参考にさせていただいた先輩方のレポート
環境構築からデバッグまでの流れ、レポートの書き方に至るまでこちらの記事を参考にさせていただきましたので、感謝を述べたいと思います。
https://eeic2020-doss1.hatenablog.com/entry/2020/11/02/220316