Grep結果をファイル出力する VSCode Extensionを作ってみた

# 作ったもの: [Grep2File] (https://marketplace.visualstudio.com/items?itemName=yuna611d.grep2file)

開いているワークスペース配下のファイルに対して指定した文字列をGrep検索。
その結果をファイルに出力する。
検索結果をExcelに貼り付けたいという(自分のための)用途を前提にしているため、tsvっぽい形で検索結果を出力するようにした。

![demo01.gif](https://qiita-image-store.s3.amazonaws.com/0/215058/ad333e52-7ed5-ce02-23b0-ddc8f457abed.gif)

# 動機

VSCodeはお気に入りのエディタだがGrep検索に不満があった。
自分でソース分析する分には"Ctr(Command)+Shift+F"のGrepで十分なのだが、
自分の仕事環境では検索結果をファイルに出力したい場面がままあった。

ファイルに出力したい理由は、検索結果をExcel等に貼り付けてフィルターしたり色をつけたりして結果を眺めながら情報整理したいため。

VSCodeのTerminalから

```terminal
$ grep -r 'lorem' ./* > ./grep_result.txt
```
とか

```cmd
findstr /S 'lorem' > .\grep_result.txt
```
とかでGrep結果をファイル出力したりawk等と組み合わせて整形してもいいのだが、コマンドをよく忘れてしまうので、できればコマンドを使わずにGrep検索したかった。

---

# 開発の個人的奮闘記

## どういう作りにしよう？

大方針としてぱっと思いついたのが下記だった。

**案1) ターミナルやCmdにGrep系のコマンド投げる**

**案2)ファイルアクセスするAPI使って、自分でGrepっぽい処理を実装**

### 比較してみた

**案1)** 

* ポジティブ
    * Grep処理のコーディング不要
* ネガティブ
    * 実行環境に依存(BashとCmdに対応すれば十分?)
    * ターミナルの出力結果を整形する処理 
        * コマンドで整形して出力する?　Grep結果をエクステンションに取り込んで整形する?
* 必要そうなこと
    * ターミナルにコマンドを投げるAPI
    * Grep完了を検知する仕組み
        * 処理完了したらempty file作ってエクステンションはそれを監視する?
    * 使用しているターミナル種類の検出する仕組み
        * Configurationに設定させる?　OSから判定する?　settings.jsonから判定できる?

**案2)**

* ポジティブ
    * 環境依存しにくい
* ネガティブ
    * 自分でGrep処理のコーディングが必要
* 実装に必要そうなこと
    * ファイルアクセスのAPI (VSCodeでGrep検索できるからAPIはある?)

結局、実行環境について考えるのが面倒になり、自分の勉強も兼ねて案2で進めることにした。

---

## VSCodeの検索はどうしているのだろう?

[pieceTreeBase.ts](https://github.com/Microsoft/vscode/blob/master/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts)と[textModelSearch.ts](https://github.com/Microsoft/vscode/blob/master/src/vs/editor/common/model/textModelSearch.ts)がVSCodeで検索をやっているところっぽかったが、一旦自分で実装してみて困ったらこちらを参考にしてみることにした。


## ファイルアクセスをどうしよう

VSCodeでGrep検索できるのだから[VSCode API](https://code.visualstudio.com/api/references/vscode-api)にそれっぽいものがあるかと思ったが見つからなかった。

どうやってファイルアクセスしようかと思ったが、普通にNode.jsのAPIが使えたのでfsモジュールでファイルアクセスすることにした。

```typescript
// 特に問題なくNode.jsで使用できるモジュールはVSCodeのエクステンション開発でも利用可能だった。
fs.readFileSync(this.FilePath, null);
fs.readFile(this.FilePath, (err) => {});
```

## 文字列検索の方法どうしよう

正規表現検索もできるようにしたかったこともあり、RegExp.testで文字列存在チェックをすることにした。

```typescript
    protected isContainSearchWord(re: RegExp, targetString: string): boolean {
        return re.test(targetString);
    }
```

## なんかバイナリも検索されている...

実装して動かしていたら検索結果に文字化けがかなり出ていた。
よくよく検索結果を見てみるとpdfやpngなどバイナリファイルも検索していた。

VSCodeではどうしているのだろうと思い、[encoding.ts](https://github.com/Microsoft/vscode/blob/master/src/vs/base/node/encoding.ts)を見てみるとbufferの先頭512byteを見てnull文字が含まれていて UTF-16 BE/LEでなければバイナリとみなしているっぽい。

とりあえず、UTF-16 BE/LEの扱いは一旦保留にして、null文字の有無でバイナリ判定をすることにした。他にも制御コードが含まれていたらバイナリとみなす場合もあるという記事もあったので、null文字以外でも制御コードを含む場合にバイナリとみなすことにした。

```typescript
    public get seemsBinary(): boolean {
        const buffer = this.BufferContent;
        const controls = [0,1,2,3,4,5,6,7,8];
        for (let i = 0; i < 512; i++) {
            const c = buffer[i];
            if (controls.indexOf(c) > -1) {
                return true;
            }
        }
        return false;
    }
```

## 出力結果を設定できるようにしたいなぁ

自分にとって必要なフォーマット形式(Excelに貼り付けて分析できる形)には出力できているが、直接ファイルをExcelで開いたり、別のプログラムに受け渡ししたくなったのでプレーンテキスト以外にCSVやTSVで結果ファイルを出力できるようにしてみた。

package.jsonの[Configuration](https://code.visualstudio.com/api/references/contribution-points#contributes.configuration)でUser Setting/Workspace Settingでユーザが設定可能なコンフィグ設定ができるとのことなのでpakcage.jsonにconfigruationを追加した。

**package.json**

```json
        "configuration": {
            "type": "object",
            "title": "Grep2File",
            "properties": {
                "grep2file.outputContentFormat": {
                    "type": "string",
                    "default": "txt",
                    "description": "File format of result file. You can opt from txt, csv or tsv."
                }
          
```
* type: 型
* default: 初期設定

上記configurationはプログラムから下記のようにして取得できる

```typescript
vscode.workspace.getConfiguration('grep2file.outputContentFormat')
```


## 検索結果に色つけたいなぁ

元々やりたいことはファイル出力した結果をコピーしてExcelに貼り付けたかっただけだが、VSCode上で検索結果をさらっと見たくなったので検出されたワードに色をつける処理を追加。

幸い装飾の処理は簡単にできるようにAPIが準備されていた。
装飾は、[Text Editor](https://code.visualstudio.com/api/references/vscode-api#TextEditor)のsetDecorationsに[TextEditorDecorationType](https://code.visualstudio.com/api/references/vscode-api#TextEditorDecorationType)(装飾の設定)と[Range](https://code.visualstudio.com/api/references/vscode-api#Range)(開始位置と終了位置のペア)の配列を渡せば色をつけられるようだ。

**TextEditorDecorationTypeの作成**

```typescript
    decorationTheme = vscode.window.createTextEditorDecorationType({
        'borderWidth': '1px',
        'borderStyle': 'solid',
        'light': {
            'backgroundColor': 'rgba(124,77, 255, 0.3)',
            'borderColor': 'rgba(124,77, 255, 0.4)',
            'color': 'rgba(255, 0, 0, 1.0)'
        },
        'dark': {
            'backgroundColor': 'rgba(255, 255, 204, 0.3)',
            'borderColor': 'rgba(255, 255, 204, 0.4)',
            'color': 'rgba(255, 255, 0, 1.0)'
        }
    });
```

**装飾範囲(Range)の取得**

```typescript
        // lineNumberは行番号, startIndexとendIndexは開始位置と終了位置(桁番号)
        let ranges = new Array<Range>();
        const startPosition = new vscode.Position(lineNumber, startIndex);
        const endPosition = new vscode.Position(lineNumber, endIndex);
        const range = new vscode.Range(startPosition, endPosition);
        ranges.push(range);
```

**装飾**

```typescript
    editor.setDecorations(decorationTheme, ranges);
```