メインコンテンツまでスキップ

どうしてもNeovimでコードレビューしたい

neovim-logo

NeovimのロゴはJason Longさんによるもので、CC BY 3.0としてライセンスされています

概要

これまでWeb UIでコードレビューを行っていたのですが、型情報を出したり参照を調べたり、「ここは型推論に任せちゃっていいんじゃない?」って思ったけど確信が持てない時にちょっと編集したりがどうにも非力で、課題感がありました。
これらの課題を解決するために、普段使っているエディタを使ってコードレビューするべく、色々やってみました。

試してダメだったこと

しばらくocto.nvimを試しに使ってみていましたが:

  • 動作自体が不安定に感じる
  • コードをエディタで見たいだけであって、コメント等の仕組みはWeb UIで構わない

等の理由から、使うのをやめました。
他にもいくつか既存のソリューションはあったのですが、どれもMy vim wayではない気がして、やめました。

想定するワークフロー

なければ作ればいいじゃないということで、理想のワークフローを考えました。

  1. ローカルにレビュー対象のコードをpullする
  2. baseに対する差分として、普段エディタで差分を確認するのと同じフローでコードレビューする
  3. コメントはGithub上で行う

手順に対応する実現方法

  1. 単にレビュー対象ブランチをpullすればOK
  2. fugitive.vimのdifftool連携を使えばOK
  3. 💥 ここだけ実現手段がない。
Pull Requestのbaseブランチとの差を求める方法について

gh コマンドで設定されたbaseを取得してから git merge-base を使って、差分の基点を求めることにしました。
最終的にはこのようなvimのコマンドとして一連の流れをまとめています。

追記
diffの指定に ... を使うことでわざわざ git merge-base しなくても同様の挙動になるようです。

ワークフロー実現のために必要なもの

Neovim上でレビュー中の箇所に素早くコメントをするためには、エディタのカーソル位置に対応する、PRのページの位置をブラウザで開く機能が必要かと思いました。

実現に向けて障壁になるもの

  • PRのURL取得
  • ファイルのリンク取得( # 以降の文字列)

前者に関してはGithub CLIが公式から出ており、簡単に取得できます。
しかし後者に関してはパッと見ると、実はかなりトリッキーです。

例えば .editorconfig という、gitリポジトリのベースから見た相対パスを含むファイル名のリンクは、 #diff-0947e2727d6bad8cd0ac4122f5314bb5b04e337393075bc4b5ef143b17fcbd5b というハッシュに変換されます。
前半の diff- は共通で入っているので分かりやすいですが、後半のハッシュ生成ロジックが謎です。

一瞬諦めかけたのですが、これはどうやら上記に示したパス + ファイル名に対して、単純にsha256を適用しているだけみたいです。
であれば簡単な話で、後は機械的にリンクを組み立てられそうです。

なぜパス + ファイル名にsha256が適用されているのか?

URLにパス + ファイル名をそのまま含めてしまうと、privateリポジトリであってもURLからパス + ファイル名自体は分かってしまうことの対策かな?と思いました。
ただPR以外は単純に実名がURLに入っているので、本当のところの意図は謎です…

成果物

というわけでできたものがこちらのプラグインです。

pbrowse.vim

このプラグインを作るのに実際はあまり手を動かしておらず、Claude 3.7 Sonnetに大部分を書いてもらいました。
もちろん上がってきたものに「これは違うのでは?」とか「もっとこうして欲しい」みたいな注文はつけるのですが、それにしても随分AIが優秀になってきたなあと、感慨深いです。

番外編: ボツになった方針・制作物

当初ハッシュの生成ロジックが分からず、Chromeで開いたPRのページをNeovimから同期的に操作しようかと思って、以下の構想で作業を始めました。

  1. NeovimとChromeの間にpubsubサーバーを立てて、ChromeはPRのページを開いたらsubscribeする
    • subscribeはChrome extensionから行う
  2. Neovimでコードレビューするが、コメントしたいときは対象ファイルと行をpublishする
  3. Chromeはその通知を受けて、コメントダイアログを開く

pubsubの実装方針

ここが個人的に面白いところなのですが、WebSocket等のあるある技術を使わず、Fetch APIを使ったreadable streamを通知に使う方針で考えていました。
Neovimからサーバーにはstdinでstreamにして、サーバーからChromeにはHTTPでstreamにすることで、NeovimからChromeにパイプする設計思想です。
つまり必要なのはWebSocketを喋る高度なpubsubサーバーではなく、stdinに入ってきたものをhttpで流すだけの簡単なサーバーで事足りるということです。

ncコマンドを用いた検証の様子

このようにstdinに入れた文字列が、リアルタイムにChrome DevToolsのConsoleに出力されています。
(手でHTTPレスポンスを打ち込んでいる様子が味わい深い。「私はWebサーバーです」って言ってもいい状態だと思う。)

Chrome extension側もたったこれだけの処理でsubscribeしています。

fetch("http://localhost:1234")
.then((response) => {
const reader = response.body.getReader();
reader.read().then(function pump({ done, value }) {
if (done) return

const message = new TextDecoder().decode(value);
console.log(message);

return reader.read().then(pump);
});
})
.catch((err) => console.error(err));

ボツ成果物

PoCでncを使ってやったような、stdinに入ってきたものをHTTPで流すだけのサーバーです。
これだけのコードでプロトコルを越えてパイプできるのはアツい。

stdin2http