はじめに
「そうなんだ〜1」、nikkieです。
以前、Markdownの「歩夢」を「歩夢🎀」に置き換えるという、どうかしているVS Code拡張を自作しました。
この実装では、VSCode Conference Japan 2021 ハンズオンの基礎編のテキストを大変参考にしています。
このたび、基礎編のテキストの実装の理解が一歩進んだのでアウトプットします。
目次
- はじめに
- 目次
- CodeLensを設定する箇所を正規表現で探す(「ドキュメントを編集しよう」より)
- document.getText()でファイルの全行を取得する
- 疑問:CodeLensは何個表示されるんだろう?
- 回答:文字列先頭が1つ以上の#で始まればCodeLensは1個、始まっていなければ0個
- 読み解き/^#+\s/g
- mフラグを追加すると、#で始まる各行にマッチ!
- 終わりに
CodeLensを設定する箇所を正規表現で探す(「ドキュメントを編集しよう」より)
本節では、ドキュメント上に表示されるコードレンズのボタンを押して、ドキュメントを編集する拡張機能を作ります。
ドキュメント上にCodeLens(コードレンズ)のボタンを表示するために正規表現を使っています。
該当するコードは以下です:
この記事では、「正規表現で、見出しを探す」コメント以降のコードを深堀っていきます。
document.getText()
でファイルの全行を取得する
以下ではファイルの内容を全部取得します。
const text = document.getText();
Get the text of this document.
https://code.visualstudio.com/api/references/vscode-api#TextDocument
ファイルの全行が取得されていて、そこには改行文字も含みます。
text
が指す文字列は、以下のテンプレートリテラル2のようなイメージです。
const text = `# s ほげほげ ## t ふがふが`;
疑問:CodeLensは何個表示されるんだろう?
Python大好きnikkieは、JavaScript(TypeScript)のコードがパッとは読めません。
ハンズオンのコードは正規表現にマッチする数と同じ数のCodeLensができると理解しています。
ただ、上で出したMarkdownのように、見出しが2行ある(#
と##
がある)とき、「正規表現にN個マッチして、CodeLensはN個表示される」のNはいくつになるんでしょう?
正規表現は、正規表現リテラル3/^#+\s/g
で表されています。
この正規表現リテラルからRegExp
インスタンスが初期化され、exec
メソッドを使ったwhile文があります。
考え込むより手を動かしてみようと、ブラウザの開発者モードを使いました。
以下の関数を定義し、いくつかのtext
を渡してみます。
function matchTitleRegex(text) { const titleRegex = /^#+\s/g; const regex = new RegExp(titleRegex); let matches; while ((matches = regex.exec(text)) !== null) { console.log(`Found ${matches[0]}. Index of matches: ${matches.index}. Next starts at ${regex.lastIndex}`) } console.log("End"); }
回答:文字列先頭が1つ以上の#で始まればCodeLensは1個、始まっていなければ0個
「こうじゃないかな」という仮説を持って読んでいた方は、一致しましたか?
動きを確認し、理由を考えていきましょう。
文字列先頭に#の並びで始まる行がある場合
先ほどのtext
を渡してみます。
const text = `# s ほげほげ ## t ふがふが`;
console.log
による出力は以下です。
matchTitleRegex(text); Found # . Index of matches: 0. Next starts at 2 End
- 1箇所マッチした
- 0-1文字目がマッチした(文字列先頭)
- 2文字目以降はマッチしない
見出しのレベルを入れ替えても、1箇所マッチは変わりません。
const text2 = `## t ほげほげ # s ふがふが`;
matchTitleRegex(text2); Found ## . Index of matches: 0. Next starts at 3 End
#
で始まるタイトルがいくつあったとしても、文字列先頭のタイトルだけにマッチするように実装しているのですね。
文字列先頭に#の並びで始まる行がない場合
const text3 = `ほげほげ # s ふがふが`;
matchTitleRegex(text3); End
マッチしませんでした!
正規表現にマッチしないので、text3
の内容のMarkdownファイルにはCodeLensは1つも表示されません。
読み解き/^#+\s/g
JavaScriptの正規表現のドキュメントを確認しながら読み解きます。
^
^
はアサーションの1つです。
アサーション - JavaScript | MDN
^ 入力の先頭に一致します。(略) 例えば /^A/ は "an A" の 'A' には一致しませんが、"An E" の 'A' には一致します。
#+
+
は数量詞の1つ。
数量詞 - JavaScript | MDN
x+
直前のアイテム "x" の 1 回以上の繰り返しに一致します。
#+
で#
の1回以上の繰り返しですね。
小まとめ:^#+
ここまでをまとめると、^#+
は「文字列の先頭にある#
の1回以上の繰り返し」です。
\s
\s
は文字クラスの1つ。
\s ホワイトスペース 1 文字に一致します。例えば空白、タブ、改ページ、改行、その他の Unicode 空白文字などです。
小まとめ:^#+\s
「文字列の先頭にある#
の1回以上の繰り返しとそれに続く空白文字1つ」
g
末尾のg
はフラグです。
「フラグを用いた高度な検索」より
正規表現には、グローバル検索や大文字小文字を区別しない検索などの機能を実現する 6 種類のオプションフラグがあります。
g
グローバル検索を行います。
つまり/^#+\s/g
とは
「文字列の先頭にある#
の1回以上の繰り返しとそれに続く空白文字1つ」のグローバル検索です。
グローバル検索してもアサーション^
によって文字列の先頭としかマッチしないので、上で見たようにマッチは1回か0回です。
- マッチ1回:文字列先頭でマッチした
- マッチ0回:先頭でマッチしなかった
mフラグを追加すると、#で始まる各行にマッチ!
g
フラグの他にm
フラグがあります。(フラグを用いた高度な検索)
m 複数行の検索です。
アサーション^
について、引用で(略)とした部分、以下のように書かれています!
複数行フラグが true にセットされている場合は、改行文字の直後にも一致します。
つまり、各行の先頭とマッチするかを見ていくことになります。
動きが変わることを確認しましょう。
mフラグ追加の動作確認
function matchTitleRegexV2(text) { const titleRegex = /^#+\s/gm; // g -> gm に変えました const regex = new RegExp(titleRegex); let matches; while ((matches = regex.exec(text)) !== null) { console.log(`Found ${matches[0]}. Index of matches: ${matches.index}. Next starts at ${regex.lastIndex}`) } console.log("End"); }
const text4 = `ほげほげ # s ふがふが ## t`;
matchTitleRegexV2(text4); Found # . Index of matches: 6. Next starts at 8 Found ## . Index of matches: 17. Next starts at 20 End
インデックス6が3行目の#
、インデックス17が7行目の##
(の左側)です。
各行にマッチしていますね!(CodeLensをタイトルの各行に表示できます🙌)
終わりに
ファイルの中で正規表現にマッチする箇所全てにCodeLensを表示する拡張の実装を読み解きました。
実装自体は文字列先頭とのマッチで実装しているので、グローバル検索の結果、マッチは1または0回です。
正規表現を読み解いていき、mフラグと^
アサーションで各行の先頭とマッチさせられることまで見ました。
どうかしている拡張実装時はそこまで精読せずに雰囲気で使いましたが、1つ1つ読み解いていると気付きがありますね!
今回お世話になったのはMDNのドキュメントですが、ドキュメントに当たる前にざっくり理解するため、以下の書籍にも当たりました。
-
前回のVS Code拡張ネタエントリから引き続き…
↩できる女の「さしすせそ」
— てんわん (@Tenshi_Shitsuzi) 2022年10月8日
さ『さすが〜』
し『しらなかった〜』
す『すご〜い』
せ『せつ菜ちゃんの方が大事なの!?』
そ『そうなんだ〜』 - 変数の値も埋め込めます↩
- 「スラッシュで囲まれたパターンからなる正規表現リテラルを使用します。」 ref: 正規表現 - JavaScript | MDN↩