OmniSharp-Vim と fzf を連携させる

## 最初に結論

Vim で C# の開発をやっているマイノリティの皆さまの役に立つかもしれない記事です。

![Monosnap ~_works_aspnetcore-app-workshop 2023-10-3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960076/1f3ea0f1-b338-5bb1-3953-85f7e0a2e43c.png)

下記の設定を行うことで上の図のように Omnisharp-Vim の機能である `omnisharp_find_usages` （使っている場所を調べるコマンド） の結果を fzf の画面でプレビュー付きで確認できるようになります！（画像のソースコードは [aspnetcore-app-workshop](https://github.com/dotnet-presentations/aspnetcore-app-workshop) のコードです）

下記の設定の場合に必要なもの
* [neovim](https://github.com/neovim/neovim)
* [omnisharp-vim](https://github.com/OmniSharp/omnisharp-vim)
* [fzf](https://github.com/junegunn/fzf/tree/master)
* [bat](https://github.com/sharkdp/bat/tree/master)

init.vim (vimrc) の設定
```init.vim
let g:OmniSharp_selector_ui = 'fzf'
let g:OmniSharp_selector_findusages = 'fzf'
let g:OmniSharp_fzf_options = {
  \'options': [
    \'--delimiter', ':| col ',
    \'--preview', 'bat --color=always {1} --highlight-line {2}',
    \'--preview-window', '+{2}+3/2'
  \]
\}
```

## 解説

ここからは、上記の設定に行きつくまでの流れにそって設定の説明をします。

### きっかけ

10月から担当している案件が変わりました。案件が変わるのをきっかけに Omnisharp-Vim の使い方をおさらいしておこうと公式の内容をざっと見ると [fzf と連携できる](https://github.com/OmniSharp/omnisharp-vim#optional-install-fzf-vim-clap-ctrlpvim-or-unitevim)ことが分かりました。

![Monosnap OmniSharp_omnisharp-vim_ Vim omnicompleti.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960076/f23cc8ee-9d15-9dc1-8828-5ba73ea97bf2.png)

fzf を使わない場合、 `omnisharp_find_usages` は vim 上で別画面で表示されます。

![Monosnap ~_works_aspnetcore-app-workshop 2023-10-3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960076/d525422f-a06a-11fc-4e75-9886f44f6969.png)


この場合、検索結果の画面を消す作業が発生します。（場合によっては便利なんですが、大体は検索してみたいファイルが決まったら消えて欲しい場面のほうが私は多かったです。）
また、デフォルトの検索結果の画面は該当箇所の前後のコードがプレビューで見れないので、本当に自分が見たいコードかどうかはファイルを開かないと分からないのが不便です。

これを fzf にすれば、フローティングウィンドウに出来て、さらにプレビュー機能も使えるのでは？と思い、Omnisharp-Vim と fzf を連携させてみることにしました。

### fzf と連携してみる

公式の通り下記の設定を行ってみました。

```init.vim
let g:OmniSharp_selector_ui = 'fzf'
let g:OmniSharp_selector_findusages = 'fzf'
```

が、エラーを吐きました。ガーン。どうやら vim のプラグインとして fzf が認識できていないようです。

![__works_budget 2023-10-20 14.53.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960076/0f56d328-e848-282c-e403-346ccc30ba5b.png)

[公式の手順](https://github.com/junegunn/fzf/blob/master/README-VIM.md#installation) を確認してみたところ、init.vim に `set rtp+=` で fzf へのパスを指定すれば良いようでしたが、これが上手くいきませんでした。fzf は scoop 経由でインストールしていましたが、通すべきパスが正直分かりませんでしたので諦めました。
[公式の手順](https://github.com/junegunn/fzf/blob/master/README-VIM.md#installation) にはvim プラグインとしてインストールする手順も書かれていたので、そちらで対応します。

私はプラグインマネージャーとして [dein.vim](https://github.com/Shougo/dein.vim) を使っていますので、[こちら](https://github.com/Shougo/dein.vim/issues/74)を参考にしつつ、下記設定で fzf を vim プラグインとして認識できるようにインストールしました。

```dein.toml
[[plugins]] # fzf
repo = 'junegunn/fzf'
build = './install --all'
merged = 0
```

今一度、 `omnisharp_find_usages` を実行すると下記画面が出ました！
![Monosnap ~_works_aspnetcore-app-workshop 2023-10-3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960076/c7bf164b-1ce9-e141-46ab-064a5c0d9e4c.png)

（行末が若干青くなっているのは、私が行末に空文字がある場合に色付けをするようにvimを設定しているためです。後述するフローティングウィンドウにすると、この手のvimの設定はフローティングウィンドウには引き継がれませんでした。）

とりあえず、fzf を使うことができました！
これによって、毎回検索結果の画面を手動で消す必要がなくなりました。
また、予期せぬナイスな機能として、検索結果に対して更なる絞り込みができるようになりました。（よく考えれば fzf ってそういうものなんですが...）

![Monosnap ~_works_aspnetcore-app-workshop 2023-10-3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960076/9b7b7e02-2507-ebe9-80c2-48593b991089.png)

### フローティングウィンドウにしたい

さて、fzf は使えるようになりましたが、まだフローティングウィンドウにしてプレビュー画面を出せていません。
ここからは fzf の設定が必要そうです。

[OmniSharp-Vim のヘルプ](https://github.com/OmniSharp/omnisharp-vim/blob/f9c5d3e3375e8b5688a4506e813cb21bdc7329b1/doc/omnisharp-vim.txt#L425-L427) によると fzfへのオプションは `let g:OmniSharp_fzf_options = { 'right': '50%' }` のような感じで渡せるようです。

![Monosnap omnisharp-vim_doc_omnisharp-vim.txt at ma.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960076/a471cf91-a2b8-3063-e14a-1861ab576656.png)

では、オプションはどのように書くのでしょうか？
[OmniSharp-Vim の fzf オプションは `fzf#run` を実行しているだけっぽいです。](https://github.com/OmniSharp/omnisharp-vim/blob/f9c5d3e3375e8b5688a4506e813cb21bdc7329b1/autoload/fzf/OmniSharp.vim#L27C1-L31)


![Monosnap omnisharp-vim_autoload_fzf_OmniSharp.vim .png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960076/52f3fdff-5afa-5842-94ea-e44dadbc7832.png)


なので [`fzf#run` の内容](https://github.com/junegunn/fzf/blob/master/README-VIM.md#fzfrun)を見ていると、こんな感じでリストで渡せば良さそうなことが分かりました。
![Monosnap fzf_README-VIM.md at master · junegunn_fz.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960076/848d5816-7b81-4d8f-260d-d6997262b305.png)

試しに --preview のオプションを渡してみます。

```init.vim
let g:OmniSharp_fzf_options = {'options': ['--preview']}
```

するとプレビュー画面はエラーを吐いていますが、フローティングウィンドウとプレビュー画面自体は出現するようになりました。
![Monosnap ~_works_aspnetcore-app-workshop 2023-10-3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960076/e0e2c8e6-d078-53b2-fdac-119eb74c11c0.png)

### fzf のオプション指定を詰める

さて、ここまでくれば、あとは fzf の設定を頑張れば良さそうです。
[fzf の Preview Window の内容を見ます。](https://github.com/junegunn/fzf/blob/master/README.md#preview-window)

![Monosnap fzf_README.md at master · junegunn_fzf - .png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960076/eefe9d1d-1379-d49b-4aca-f9bcca2f36f4.png)

どうやらプレビューで見たいものを cat や bat に渡すことで、プレビューが見れるようです。
見たいものは `{}` を使って渡すようです。`{}` の説明には `{} is replaced with the single-quoted string of the focused line` とありますが、今一つピンと来ていません。

bat の使い方としては `bat 見たいファイルのパス` なので `{}` の部分が、おそらくファイルのパスになればうまく行く気がします。

一旦、vim ではなくPowerShell上で確認してみます。

```powershell
echo "test.txt" | fzf --preview 'bat --color=always {}'
```

上記を実行すると、`test.txt` の内容が bat で確認できました。
![Monosnap ~_works 2023-10-31 22.25.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960076/ab1c8462-95a7-2b97-27c4-68231f3ccf86.png)
どうやら `{}` は fzf で選択している行、この場合だと `test.txt` が文字列として展開されるようです。

`{}` をもう少し調べてみると下記の記事が大変分かりやすかったです。

https://zenn.dev/eetann/articles/2022-08-27-fzf-preview

となると、今回の `omnisharp_find_usages` を実行したときに fzf 上で選択している行がどのような形式の文字列になっているかを確認する必要がありそうです。

ちなみにプレビュー画面で確認するには bat に以下の情報を渡す必要があります。
* ファイルのパス
* ハイライトしたい行の行番号

今一度、vim での fzf の選択行を見てみます。
![Monosnap ~_works_aspnetcore-app-workshop 2023-10-3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960076/ba080cbf-8ffa-800d-2278-f3ca51d516e4.png)

選択されている行は `src\ConferencePlanner\BackEnd\Data\ApplicationDbContext.cs: 27 col 18     public DbSet<Session> Sessions => Set<Sess..` となっています。 （`..` の部分は表示上省略されているだけで、画面を大きくすると最後まで表示されます。）

まとめると
```
vimを開いた場所からのファイルの相対パス: 行数 col 列数 検索したシンボルを使っている行の内容
```
となっていました。
ここから、ファイルの相対パスと行数がとれると素敵なプレビュー画面になりそうです。

#### delimiter を指定してファイルの相対パスと行数を取得する

[先の記事](https://zenn.dev/eetann/articles/2022-08-27-fzf-preview)によると、区切り文字を指定したい場合は `--delimiter` で指定するとありました。
[fzf のヘルプ](https://github.com/junegunn/fzf/blob/1cfa3ee4c7c1741fca3cd26fb13451621d14bbf2/man/man1/fzf.1#L83-L85) を見ると `--delimiter` は正規表現で指定できるようです。
![Monosnap fzf_man_man1_fzf.1 at master · junegunn_f.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960076/e44f00d5-43b6-284c-0c35-5268985d7580.png)

今回の場合だと、欲しいのはファイルの相対パスと行数だけです。なので正規表現でファイルの相対パスと行数だけ、取りだせれば良さそうです。

欲張らずにとりあえず、まずは delimiter に `:` を指定して、ファイルの相対パスだけ取り出せるか確認してみます。確認は vim ではなく PowerShell 上で確認していきます。

```powershell
echo "src\ConferencePlanner\BackEnd\Data\ApplicationDbContext.cs: 27 col 18     public DbSet<Session> Sessions => Set<Session>();" | fzf --delimiter : --preview 'bat --color=always {1}'
```

上記の echo の内容は `omnisharp_find_usages` を実行したときに fzf 上で選択している行の内容にしています。重要なのは `fzf --delimiter : --preview 'bat --color=always {1}` の部分です。区切り文字に `:` を指定し、`{1}` で区切った文字列の1番目を取りだしています。

結果を見てみます。
![Monosnap ~_works_aspnetcore-app-workshop 2023-10-3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960076/9b09c3b2-4812-5ecf-2212-808247e25a9a.png)

ｷﾀ━━━━(ﾟ∀ﾟ)━━━━!!

ついでに二番目以降の文字列はどうなっているか見ましょう。`{1}` の部分を `{2}` に変えてみます。するとエラーになりました。
![Monosnap ~_works_aspnetcore-app-workshop 2023-10-3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960076/a6b8615f-0aae-8927-219a-f523922b6eca.png)
bat の引数に `{2}` の内容がファイルのパスとして渡されてエラーとなっています。
ファイルのパスとして `27 col 18     public DbSet<S2ssion> Sessions => Set<Session>();` が渡されたようです。つまり`:`の区切られた文字列の塊の2番目が渡されたようです。

前述のように区切り文字は正規表現で指定できるので or 指定も可能です。今回の場合だと行数にあたる `27` という数値をとりたいので `:| col `(colの後に半角スペース）を指定すればうまくいく気がします。
同様に PowerShell 上で動作確認をします。

まずはファイルの相対パスが取得できるかを確認します。
```powershell
echo "src\ConferencePlanner\BackEnd\Data\ApplicationDbContext.cs: 27 col 18     public DbSet<S2ssion> Sessions => Set<Session>();" | fzf --delimiter ":| col " --preview 'bat --color=always {1}'
```
上記を実行し先ほどと同様に bat でファイルの中身を見ることができたので `{1}` の部分でファイルの相対パスを取得できていそうです。

![Monosnap ~_works_aspnetcore-app-workshop 2023-10-3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960076/5a3d222a-9660-1b3d-1cc0-44fa32257247.png)

次は `{1}` の部分を `{2}`に変更して実行します。
```powershell
echo "src\ConferencePlanner\BackEnd\Data\ApplicationDbContext.cs: 27 col 18     public DbSet<S2ssion> Sessions => Set<Session>();" | fzf --delimiter ":| col " --preview 'bat --color=always {2}'
```
すると行数にあたる `27` が bat の引数として渡され、エラーを吐いています。よって、delimiter の指定は `:| col `でいけそうです。
![Monosnap ~_works_aspnetcore-app-workshop 2023-10-3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960076/6f8e91a2-04af-4225-a695-0f0e9c83d295.png)

#### ファイルの相対パスと行数を使って検索対象の行をハイライト表示にする

さて、ファイルの相対パスと行数をとる手段は分かりました。次は、検索対象の行をハイライト表示にするようにします。ハイライト表示は簡単で `--highlight-line {2}` で指定すればいいようです。
PowerShell で確認します。
```powershell
echo "src\ConferencePlanner\BackEnd\Data\ApplicationDbContext.cs: 27 col 18     public DbSet<S2ssion> Sessions => Set<Session>();" | fzf --delimiter ":| col " --preview 'bat --color=always {1} --highlight-line {2}'
```
fzf 以降のコマンドを抜き出すと `fzf --delimiter ":| col " --preview 'bat --color=always {1} --highlight-line {2}` となっています。（ `--highlight-line {2}` が追加）

実行結果です。
![Monosnap ~_works_aspnetcore-app-workshop 2023-10-3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960076/4fb688b3-ca8b-1c21-d026-57abf31f57d3.png)

あれ？ハイライトされてない？と思いスクロールしてみると、ちゃんと `27` 行目がハイライトされていました。
![Monosnap ~ 2023-10-31 23.18.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960076/92fddbee-f94b-607b-34d1-6d28d56694bd.png)

なるほどです。ハイライトだけでなく、表示を開始する行の指定も必要のようです。
[先の記事](https://zenn.dev/eetann/articles/2022-08-27-fzf-preview)によると `--preview-window` オプションでスクロールのオフセット位置を指定できるとありました。
今回はハイライト行がプレビュー画面の真ん中あたりに来て欲しいので、記事を参考にして、(27行目の位置 + 3行（ヘッダー分）)/2（真ん中を指定）をオフセット量にします。

またまた、PowerShell上で確認しましょう。
```powershell
echo "src\ConferencePlanner\BackEnd\Data\ApplicationDbContext.cs: 27 col 18     public DbSet<S2ssion> Sessions => Set<Session>();" | fzf --delimiter ":| col " --preview 'bat --color=always {1} --highlight-line {2}' --preview-window '+{2}+3/2'
```
先ほどのコマンドに `--preview-window '+{2}+3/2'` の内容が増えています。

![Monosnap ~_works_aspnetcore-app-workshop 2023-10-3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960076/c93fe9d5-bbc7-7b10-e98d-cfa49f0e033d.png)

やりました！これこそが私が求めていたプレビュー画面です！

#### 最後はinit.vim (vimrc) で指定

さぁ、PowerShell上では動作を確認できましたので、あとは vim 上で実現するだけです。
vim 側では `OmniSharp_fzf_options` で指定すれば良いことは分かっています。
なのでこう、書けば良いわけです。（一番最初に示した内容と同じです。）

```init.vim
let g:OmniSharp_fzf_options = {
  \'options': [
    \'--delimiter', ':| col ',
    \'--preview', 'bat --color=always {1} --highlight-line {2}',
    \'--preview-window', '+{2}+3/2'
  \]
\}
```

他の設定と合わせて書くと、こうなります。
```init.vim
let g:OmniSharp_selector_ui = 'fzf'
let g:OmniSharp_selector_findusages = 'fzf'
let g:OmniSharp_fzf_options = {
  \'options': [
    \'--delimiter', ':| col ',
    \'--preview', 'bat --color=always {1} --highlight-line {2}',
    \'--preview-window', '+{2}+3/2'
  \]
\}
```

結果は、こうなります！見事に Omnisharp-Vim で検索されたものが fzf に渡されてプレビュー画面で内容確認ができるようになっています。イエーイ。
![Monosnap ~_works_aspnetcore-app-workshop 2023-10-3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2960076/1f3ea0f1-b338-5bb1-3953-85f7e0a2e43c.png)

あとはお好みで bat のカラーテーマを変更すると良いでしょう。
以下の記事が役に立ちました。

https://github.com/sharkdp/bat/blob/master/doc/README-ja.md#%E3%83%8F%E3%82%A4%E3%83%A9%E3%82%A4%E3%83%88-%E3%83%86%E3%83%BC%E3%83%9E

https://ryamashina.com/itml/20220504/

## あとがき

この設定のおかげで開発速度爆上がりです（自分比）。ちゃんと、fzf のオプションを確認したことがなく、今まではコピペばっかりだったので大変勉強になりました。
時々開発環境を見直してみると、当時は気づけなかったことにも気づけて良いなと感じました。