LOST IN BLUE

2025/04/01

GitHub Copilotをシェルでも使う ~ Xonsh を添えて ~

コーディングするときに便利なGitHub Copilotをシェル上でも使いたいなと思いました。
例えば、

$ git branch -l
  feature/foo
* main

のときに、git checkout feature/fooを補完してくれるとちょっとうれしいです。
よって、今回はXonshというPython製のシェルで、GitHub Copilotを使った補完を作ってみました。

completion

実装は以下のリポジトリに置いてあります。(置いてあるだけ)
あくまで趣味・実験用のコードで実用に耐えうる保証はありません。好きに改変してください。

使うもの

Xonsh

Python製のシェル(非POSIX)。
Pythonがそのまま動く変わったシェルで、Pythonのスーパーセットとして動作します。
簡単に機能拡張・改変することができ、今回も割とやりたいことが簡単に実現できました。

Xontrib

Xonshの拡張機能のことをXontribと呼びます。
当然Pythonで書くことができ、Xonshの機能を拡張したりデフォルトの挙動を上書きできたりする。
今回はGitHub Copilotを使った補完を提供するXontribを作成することになります。

copilot.vim

GitHub Copilotは公開されているAPIやSDKがこれの実装当時はありませんでした。(今はある)
そこで、Vim用に公式が用意しているGitHub Copilotプラグインであるcopilot.vimにGitHub Copilotを使うためのJavascriptサーバーが入っているので、これを流用します。
GitHub Copilotをcopilot.vim越しに叩くやり方は結構スタンダード(要出典)で、いろいろドキュメントがあります。

つくる

copilot.vimの仕様

copilot.vimにはほとんどLSP(Language Server Protocol)に準拠した言語サーバーが入っています。
LSPは言語サーバーが主にエディターにどのようにオートコンプリート、定義への移動などの機能を提供するかを定義したプロトコルで、いろいろな言語拡張機能がLSPに準拠しています。

copilot.vimのGitHubCopilot言語サーバーはLSPに少し拡張メソッドが加えられてたものとなっています。 基本的な使い方は一般的なLanguage Serverと同じで、JSON-RPCでstdin/stdoutを通じて通信します。

PythonでのLSPクライアントの実装としてpylspclientがあったのでこれを使わせてもらいました。
基本的に初期化してdidOpenして、その後にgetCompletionsという独自メソッドを呼んであげるだけで良いです。
実際にどんな通信をすれば良いのかは実装や、上述のサイトを読んでください。

Xonshで動く独自補完Xontribをつくる

Xonshで独自の補完を実装する方法は、以下の公式ドキュメントで丁寧に説明されています。

これをベースに、先述のcopilot.vimのGitHubCopilot言語サーバーから補完候補を取得して、Xonshの補完候補として返すものを実装してあげればOKです。
少しトリッキーな点として、GitHubCopilot言語サーバーはsubprocess.Popen()でサブプロセスとして起動して、グローバル変数に格納してあげることでXonshのセッション中ずっと使えるようにしています。

たいへんだったこと

LSPによる補完の取得

LSPは基本的にエディターで使うことを想定しているため、Web APIのようにリクエストにすべての情報を詰め込んで問い合わせれば結果が返ってくるというものでは無く、何かしらのドキュメント・その内容などを状態として管理し、クライアントとサーバー間でやり取りをする必要があります。
よって、今回も補完を取得するために、一時ファイルを作成して、そこにこれまでのヒストリーやコンソール出力を保存し、その一時ファイルのN行目M文字目からの補完を下さいというリクエストを送る必要がありました。
そのため、補完機能を作るだけならそこまでコードは必要無いのですが、コマンド・コンソール出力を逐次反映させつつ、適切にLSPにリクエストを送るためにそこそこ肥大化してしまいました。

また今回はgetCompletionsというGitHubCopilot言語サーバーの独自メソッドを使用しましたが、LSP的にはtextDocument/completiontextDocument/inlineCompletionがあるので、これらを使う実装に変えていく必要があるかもしれません。

MacOSでのXonshの標準出力周りの怪奇現象

Xonshの機能である、$XONSH_CAPTURE_ALWAYSなどを有効にすることで、他のコマンドのコンソール出力もヒストリーから取得することができます。
今回はこれも補完のための情報として使うことでより良い補完ができるようにしたのですが、どういうわけかMacOSでは動きませんでした。

具体的な症状としては$XONSH_CAPTURE_ALWAYSを有効にしていると、LSPサーバーから標準出力を通してレスポンスが取得できなくなってしまうというものです。
WSLでは問題無く動作していたので、原因調査もいろいろ見誤りかなり難航しました。
Xonsh由来・MacOS由来の仕様か不具合かは分かりませんが、せっかくコンソール出力が取得できるXonshの嬉しさが無くなってしまうのは悲しかったです。

補完の速さ

Xonshでの補完には当然他のbash completionのような補完機能もあります。
しかし、1つの補完が完了しないと補完候補の表示ができないため、GitHub Copilotによる補完が遅いと他の補完の足も引っ張ってしまいます。
実際、たまにGitHub Copilotから補完が返ってくるのが遅いときがWeb越しに通信しているので当然あるのですが、そういうときは他の補完も遅くなってしまいます。
これはどうしようもないので、一旦タイムアウトで誤魔化ました。

GitHub Copilot SDKが公開された

copilot.vimの中にあるGitHub CopilotのLSPサーバーで試行錯誤し、ようやくできたぞというタイミングで、GitHub CopilotのSDKが公開されました。

基本的にはcopilot.vimからLSPサーバーのみを取り出したものだと思います。
これに置き換える必要がありますが、認証周りも実装してあげる必要があるので(今はvimでの認証情報をそのまま使っている)まだ手が出せていません。

まとめ

GitHub Copilotが補完してくれるXonsh拡張機能を作ってみました。
Xonshはいろいろ弄りやすくて楽しいので、興味があればぜひ触ってみてください。(人を選ぶとは思いますが)
GitHub Copilotもエディター向けではありますがSDKが公開されたので、いろいろ遊びやすくなってきました。
近年の言語拡張やAI支援の強化は著しいので、シェルもLSPに基づいて実装される時代が来るのかもしれませんね。