はじめに
この文章は東大工学部電気電子工学科・電子情報工学科の3年後期実験の科目の1つである大規模ソフトウェアを手探るの成果報告記事です。実験の内容は「10日間の実験期間で大規模なOSSを選び何らかの機能実装を試みる」というもので、我々の班は普段からお世話になっているVisual Studio Codeに機能実装改善を行いました。以下にその一部始終を記します。
Visual Studio Codeとは
Visual Studio Code(通称:VSCode)は2015年に公開されたMicrosoftが開発している統合開発環境です。軽量ながら多機能でカスタマイズ性も高く、今最も人気なIDEの一つと言っても過言ではないでしょう。
最近(2021年10月下旬)には、VSCodeのWebブラウザ版も公開されました!
ソースコードはGitHub上で公開されており、こちらからコードを確認できます。
VSCodeのビルド方法
VSCodeのリポジトリをローカルに落として、ビルドしてみます。(元のリポジトリのHow-to-Contributeを参照。)
- リポジトリを自分のGitHubにフォークします。
- そのリポジトリをローカルにクローンします。
$ git clone git@github.com:<your github ID>/vscode.git
- ローカルでビルドする。
$ yarn
$ yarn watch
$ ./scripts/code.sh # デバッグモードへ移行
- VSCodeのコードに変更を加えた後は、
./scrips/code.sh
を実行し直します。
以上が完了すると、既存のVSCodeとは別にデバッグ用のVSCodeが現れるはずです。
課題設定
正直「どのOSSの機能改善をするか」を決める課題設定の段階に一番骨を折りました。
なぜなら我々の班でOSSにコントリビュートした経験のあるメンバーはいなかったため、「なんのソフトウェアを選べばいいのか」、そして「どんな機能改善を加えればいいのか」の見通しが全くつかなかったからです。
最終的には、「使ったことのないソフトに機能改善を加えるのは難しいよね」という当然の帰結になり、普段からお世話になっているVSCodeの機能改善をする方向で話がまとまりました。
「VSCodeにこんなのがあったらいいよね」という話を共有し始めると使い慣れているからこそのアイデアがたくさん出たので、「馴染みのあるソフトの機能改善を加える」というのは基本にしてかなり重要な視点だと思います。
既存のVSCodeの不便な点
VSCodeにはCMD + Shift + A
(macのキー配列の場合)で複数行コメントアウトを行うショートカットキーがあります。しかしこのショートカットキーは選択範囲が文の途中から始まっている場合、下画像のようにコメントアウトが途中で切れてしまう問題点があります。
デバッグ時に一時的に大きなまとまりのコードブロックをコメントアウトし、折り畳んでおきたくなる時があります。
そのような時、1行ラインコメントアウトを使う場合は文の途中が選択範囲でも文の先頭からコメントアウトがなされるのですが、肝心な折り畳みができません。(コメントアウトの部分が長くなってコードが煩雑になります。)
一方、複数行コメントアウトを使う場合は、うまく文の初めから終わりまでの選択できれば折り畳めるのですが、そうでない場合上述したように文の途中からコメントアウトが始まり折り畳めません。
大きなまとまりのコードブロックをコメントアウトするとき、その範囲をいちいちカーソルを合わせて選択するのは単純でいて結構めんどくさいです。
我々が改善した機能
上のような不便な点を踏まえ、我々は以下の二つの機能を実装することにしました。
1. 文の途中から複数行コメントアウトを始めてしまっても、ブロック全体がコメントアウトされる機能
2. ブロックのトップレベルにカーソルを当てた状態でショートカットキーを押せばブロック全体がコメントアウトされる機能
言葉だと非常にわかりづらいと思うのですが、具体的には下画像のような機能を実装しました。
- 文の途中から複数行コメントアウトを始めてしまっても、ブロック全体がコメントアウトされる機能。上のGIF画像と比較すると変化がわかりやすい。
2. ブロックのトップレベルにカーソルを当てた状態でショートカットキーを押せばブロック全体がコメントアウトされる機能
これらの機能により、選択範囲をいちいち選択しなくても、また選択範囲を間違えてしまっても、ブロック全体をコメントアウトすることができます
具体的に加えた変更は以下に示します。
具体的な実装方法
まず、複数行コメントアウトを実行しているコードを探しました。コメントアウト系のコードを探していると、どうやらVSCode内では複数行コメントアウトをblockComment
という名前で呼んでいることがわかりました。そこで検索窓でblockComment
で検索をかけると、src/vs/editor/contrib/comment/blockCommentCommand.ts
という名前のファイルが見つかったので、そのファイル内にブレークポイントを置いて、1行ずつ実行しながら、どのようにブロックコメントアウトが実行されているかを理解していきました。
blockCommentCommand.ts
では、BlockComemntCommand
というクラスが定義されており、主に選択範囲を引数にとって、インスタンスを作ります。そのクラスの主要なメソッドは、以下の3つ。
-
_createOperationsForBlockComment
ブロックコメントアウトを行う(_createAddBlockCommentOperations
)か、ブロックコメントアウトを外す(_createRemoveBlockCommentOperations
)かを場合分けしています。 -
_createAddBlockCommentOperations
ブロックコメントアウトを実行しているメソッドで、通常は選択範囲の始まりと終わりに、ブロックコメントアウトの記号(/* や """など)を入れるか、選択範囲がない場合は、カーソルの前後にその記号を入れます。 -
_createRemoveBlockCommentOperations
ブロックコメントアウトを外しているメソッドで、通常は選択範囲の端にブロックコメントアウトの記号があった場合にのみ、その記号を取り除くような処理がされます。
今回はブロックコメントアウトの処理の仕様を変えたいので、_createAddBlockCommentOperations
のメソッド内を書き換えていくことにしました。まず最初に以下に変更前のコードと変更後のコードを示します。
- 変更前
public static _createAddBlockCommentOperations(r: Range, startToken: string, endToken: string, insertSpace: boolean): IIdentifiedSingleEditOperation[] {
let res: IIdentifiedSingleEditOperation[] = [];
if (!Range.isEmpty(r)) {
// Insert block comment start
res.push(EditOperation.insert(new Position(r.startLineNumber, r.startColumn), startToken + (insertSpace ? ' ' : '')));
// Insert block comment end
res.push(EditOperation.insert(new Position(r.endLineNumber, r.endColumn), (insertSpace ? ' ' : '') + endToken));
} else {
// Insert both continuously
res.push(EditOperation.replace(new Range(
r.startLineNumber, r.startColumn,
r.endLineNumber, r.endColumn
), startToken + ' ' + endToken));
}
return res;
}
- 変更後
public static _createAddBlockCommentOperations(r: Range, startToken: string, endToken: string, insertSpace: boolean, startLineMinColumn: number, endLineTextLength: number, model: ITextModel): IIdentifiedSingleEditOperation[] {
let res: IIdentifiedSingleEditOperation[] = [];
if (!Range.isEmpty(r)) {
res.push(EditOperation.insert(new Position(r.startLineNumber, startLineMinColumn), startToken + (insertSpace ? ' ' : '')));
res.push(EditOperation.insert(new Position(r.endLineNumber, endLineTextLength), (insertSpace ? ' ' : '') + endToken));
} else {
const startLineText = model.getLineContent(r.startLineNumber);
const startblockColumn = startLineText.search(/[{]/) + 2;
const cursorPosition = new Position(r.startLineNumber, startblockColumn);
const matchedBrackets = model.matchBracket(cursorPosition);
if (matchedBrackets) {
let newStartBracketPosition: Position;
let newEndBracketPosition: Position;
if (matchedBrackets[0].containsPosition(cursorPosition)) {
newStartBracketPosition = matchedBrackets[0].getStartPosition();
newEndBracketPosition = matchedBrackets[1].getStartPosition();
} else {
newStartBracketPosition = matchedBrackets[1].getStartPosition();
newEndBracketPosition = matchedBrackets[0].getStartPosition();
}
res.push(EditOperation.insert(new Position(newStartBracketPosition.lineNumber, startLineMinColumn), startToken + (insertSpace ? ' ' : '')));
res.push(EditOperation.insert(new Position(newEndBracketPosition.lineNumber, newEndBracketPosition.column + 1), (insertSpace ? ' ' : '') + endToken));
} else {
// Insert both continuously
res.push(EditOperation.replace(new Range(
r.startLineNumber, r.startColumn,
r.endLineNumber, r.endColumn
), startToken + ' ' + endToken));
}
}
return res;
}
ここから実際に二つの機能をどう実装していったかを説明していきます。
1. 文の途中から複数行コメントアウトを始めてしまっても、ブロック全体がコメントアウトされる機能
上の_createAddBlockCommentOperations
というメソッドの引数であるr
という変数に選択範囲が入っており、r
が空でないなら、つまり選択範囲が決められているならif以降のコードが実行されます。ここで、ブロックコメントアウトの記号を入れる位置を行頭と、行末にしてあげることで、意図した挙動を示すようになります。変更としては、以下のようになります。
// 変更前
res.push(EditOperation.insert(new Position(r.startLineNumber, r.startColumn), startToken + (insertSpace ? ' ' : '')));
res.push(EditOperation.insert(new Position(r.endLineNumber, r.endColumn), (insertSpace ? ' ' : '') + endToken));
// 変更後
res.push(EditOperation.insert(new Position(r.startLineNumber, startLineMinColumn), startToken + (insertSpace ? ' ' : '')));
res.push(EditOperation.insert(new Position(r.endLineNumber, endLineTextLength), (insertSpace ? ' ' : '') + endToken));
2. ブロックのトップレベルにカーソルを当てた状態でショートカットキーを押せばブロック全体がコメントアウトされる機能
選択範囲はないので、先の!Range.isEmpty(r)
の条件分岐で、elseの方に移動するため、そこで処理を書きます。まず、コメントアウトしたい範囲を決める必要があります。そのために、SCSSやC言語などでブロックを作るために用いられるブラケット({})を利用します。そのため、ブラケットの始まり({
)を最初に探し、それに対応するブラケット(}
)を見つけることでコメントアウトの範囲を確定させます。以下は、matchBracket
というメソッドを利用して、対応するブラケットのセットを取得しています。
// カーソルがある行の1行テキストを取得
const startLineText = model.getLineContent(r.startLineNumber);
// インデントを考慮して、文字が左から何個目に始まるかを取得
const startblockColumn = startLineText.search(/[{]/) + 2;
// 開始ブラケット(`{`)の位置を取得
const cursorPosition = new Position(r.startLineNumber, startblockColumn);
// 対応するブラケットのセットを取得
const matchedBrackets = model.matchBracket(cursorPosition);
そして、カーソルがある行に開始ブラケットがあるかないかで場合分けを行い、開始ブラケットがない場合は、1行全体をコメントアウトします。開始ブラケットがある場合は、行頭と終了ブラケットの後にコメントアウトの記号を加えます。実装としては、res.push(EditOperation.insert(new Position(行数, 左から何個目か), startToken + (insertSpace ? ' ' : '')));
というコードで、指定したポジションに、/*
という文字列を追加しています。それを2回用いて二つのコメントアウト記号を追加します。具体的なコードは以下のようになります。
if (matchedBrackets) {
// 開始ブラケットの位置
let newStartBracketPosition: Position;
// 終了ブラケットの位置
let newEndBracketPosition: Position;
if (matchedBrackets[0].containsPosition(cursorPosition)) {
newStartBracketPosition = matchedBrackets[0].getStartPosition();
newEndBracketPosition = matchedBrackets[1].getStartPosition();
} else {
newStartBracketPosition = matchedBrackets[1].getStartPosition();
newEndBracketPosition = matchedBrackets[0].getStartPosition();
}
// 行頭に/* を配置します
res.push(EditOperation.insert(new Position(newStartBracketPosition.lineNumber, startLineMinColumn), startToken + (insertSpace ? ' ' : '')));
// 終了ブラケットの後に*/を配置します
res.push(EditOperation.insert(new Position(newEndBracketPosition.lineNumber, newEndBracketPosition.column + 1), (insertSpace ? ' ' : '') + endToken));
} else { // カーソルがある行に開始ブラケットがない時
res.push(EditOperation.replace(new Range(
r.startLineNumber, r.startColumn,
r.endLineNumber, r.endColumn
), startToken + ' ' + endToken));
}
まだ改善できる部分
前章で書いた通り、今回は、コメントアウトするブロックの判定を{}
を探す形式で実装しました。
これにより、コードの書き方によって対応できていない場合があります。
今回の実装で想定したのは以下のような書き方で、この書き方では前述の通りの機能を発揮できます。
int main(){
hoge();
}
しかし、以下のように、ブロックのトップレベルの行と{
が改行されている場合には使うことができません。
int main()
{
hoge();
}
また、以下のように、一行に{}
が二つある場合に、二つ目の{}
をコメントアウトすることができません。
int main(){for(int i = 0; i < 0; i++){hoge();}}
これらは、VSCodeが、{}
の対応は認識しているものの、言語の構文の「ブロック」を認識している訳ではないということに由来します。
また、{}
ではない方法でブロックを表現する言語にも、この実装は対応していません。
ブロックにインデントを用いるPythonでは、VSCodeに既に存在する機能であるIndent Guideの情報を用いて、表示されているIndent Guideの範囲をコメントアウトするという方法で実装できましたが、仕様の詳細が矛盾する点もあり、マージはしていません。
*Indent Guideとは、行番号の右隣に表示される、選択中のインデント範囲をハイライトしてくれる線のこと。
Pull Requestを送ってみる
microsoft/vscodeにissue(#135622)とpull request(#135533)を提出しました。
提出したissueには、botからの返答があり、VSCodeのコミュニティでは、「feature requestは60日以内に20いいねを貰わなければ対応してもらえない」ということが分かりました。
(追記)
先日ついに20以上のいいねをいただくことができました🎉
皆さんありがとうございます!
おわりに
私たち3人にとってこれほど大規模なソフトウェアを手探るというのは初めての経験でした。
コードの意味が理解できない箇所があったり、追加機能を作用させることに頭を悩ませたり、と苦労することも多かったです。しかし、自分が普段使い慣れているソフトウェアに変更を加えることはとても充実感のある作業でした。
また、最終的に一つの形にしてPRを送ることまでできたのもとても良い経験となりました!
今回の経験を足掛かりにこれからもOSS開発にぜひ取り組んでいこうと思います。