NeovimでC#開発をしたい!
2024-12-11
azblob://2024/12/10/eyecatch/2024-12-11-write-csharp-with-neovim-000.png

はじめに

この記事は,FIXER Rookies Advent Calendar 2024の11日目の記事です.

室です.仕事でC#を書いていたところ,Neovimで書きたくなったのでとりあえずすぐに手を付けられそうなところをいじりました.

ターゲットの技術スタック

  • ASP.NET Core

準備

社の規定上Linuxネイティブは使えないので,WSLでやります.ディストロはお好みで.

今回はArch Linuxを使用します.

とりあえず,一通りお好みの環境構築をしておきます.

環境をセットアップした様子

シェル

シェルはfishを使っています.POSIX非互換なのでスクリプトに癖がありますが,特に何も設定しなくても入力補完とかがいい感じに動いてくれるのがとても楽です.

プロンプトはstarshipTokyo Nightテーマを使っています.プラグインは特に使ってないです.

ターミナルマルチプレクサ

Tmuxです.Zellijに移行しようとしたことはありますが,派手過ぎて好みに合わない上にキーバインドを吸われまくって断念しました.やはりPrefix+任意キーのほうが楽です.

PrefixはC-xに設定しています.意図せずNanoが起動したときに泣きを見ますが,押しやすいので耐えています.

テーマはtokyonight-stormをベースに,区切り文字などをちょっといじっています.

Neovimの設定をしよう

では,NeovimでC#を書いていきましょう.

LSP周り

LSP(Language Server Protocol)というのは,従来は各開発ツールに依存していた入力補完や定義ジャンプ,警告表示などを標準化したプロトコルです.異なる言語でも同じプロトコルで対応できるので,バックエンドを取り換えれば1つのエディタで複数の言語に対応することができますし,逆に異なるエディタでも1つのバックエンドがあればその言語に対応できることになります.有名どころだとEmacsやVisual Studio Codeが対応しています.

Neovimも対応しており,LSPの設定周りをいい感じにしてくれるプラグインがいくつかあります.今回は,設定をLuaで統一したいので,一番オーソドックスなnvim-lspconfigを使います.

C#のLSPとしてはomnisharpが有名ですが,こちらは高確率で起動に失敗するので,csharp_lsを使います.Code Actionの種類がomnisharpより少ない,入力補完時に型推論をしてくれないなど不便な点はありますが,ちゃんと起動してくれるのでこっちを使っています.

テスト系

neotestneotest-dotnetで何とかなります.

私はF6キーで,現在のファイルにある単体テストを実行するように設定しています.

デバッガ

処理を途中で止めて変数の中身を見たりできるやつです.これに関しては,いろいろと面倒くさいので来週の記事に書きます.白状するとNeovimではまだ動かせていないです.

その他ツール系

カラースキームはtokyonight.nvim,ステータスバーはlualine.nvim,タブバーはbufferline.nvim,ファジーファインダーはtelescope.nvim,入力補完はnvim-cmpを使っています.定番ですね.

あとはhop.nvimで,任意の単語にジャンプできるようにしています.とても便利.

課題点

割と本題です.omnisharpが起動時によく落ちる話をしましたが,csharp_lsでもいろいろと不便な点があります.

サマリコメント

よく使う割に割と切実な問題はこれです.C#には,以下のように関数のDocをXML形式で書くことができます.

置く場所に困った公式Doc

C#/// <summary>
/// ユーザー名を取得
/// </summary>
/// <param name="userId">ユーザーのID</param>
/// <returns>ユーザー名</returns>
public string GetUserName(string userId)
{
    // 略
}
 

Visual Studio (Code) やJetBrains Riderなんかを使っていると,宣言済みの関数(やクラスなど)の上の行で///を入力するとひな形を生成してくれます.楽ですね.

一方のNeovimは,ほぼすべて手打ちです.一応変数名やタグ名は補完が出ますが,結局/<を何回も打たないといけないので,小指がやたら疲れます.スニペットでどうにかしたい.

auto import

C#の場合はusingですね.import(using)されていない関数とかを追加するとimport(using)を勝手に追加してくれる機能です.TypeScriptやRustなんかを書いているとよく使いますね.

これも,Visual Studio (Code) やRiderでは勝手にやってくれますが,Neovimではどういうわけかうまくいきません.

nvim-lspconfigOmnisharpの設定を見ると,auto importをしてくれそうなオプションがありますが,これをtrueに設定してもうまく動いてくれませんでした.ちなみにcsharp_lsはそもそもオプションがありません

じゃあどうするのかってことですが,csharp_lsはusingしていないメソッドを使った後にCode Actionを呼ぶとusingを追加することができるので,これで無理やり解決してます.

クラス名/名前空間名の補完

ファイル構造に基づいた名前空間,ファイル名に基づいたクラス名.Visual StudioやRiderではファイル作成時に勝手に追加してくれますが,Neovimは当然追加してくれません.

クラス名については,以下の感じでLuaSnipで補完できます.

local ls = require("luasnip")

local s = ls.snippet
local sn = ls.snippet_node
local isn = ls.indent_snippet_node
local t = ls.text_node
local i = ls.insert_node
local f = ls.function_node
local c = ls.choice_node
local d = ls.dynamic_node
local r = ls.restore_node
local ai = require("luasnip.nodes.absolute_indexer")
local events = require("luasnip.util.events")
local extras = require("luasnip.extras")
local l = extras.lambda
local rep = extras.rep
local p = extras.partial
local m = extras.match
local n = extras.nonempty
local dl = extras.dynamic_lambda
local fmt = require("luasnip.extras.fmt").fmt
local fmta = require("luasnip.extras.fmt").fmta
local conds = require("luasnip.extras.expand_conditions")
local postfix = require("luasnip.extras.postfix").postfix
local types = require("luasnip.util.types")
local parse = require("luasnip.util.parser").parse_snippet
local ms = ls.multi_snippet
local k = require("luasnip.nodes.key_indexer").new_key

ls.add_snippets("cs", {
  s({ trig = "class", name = "Class snippet" }, {
    t("class "),
    f(function() return vim.fn.expand("%:t:r") end),
    t({ " {", "    " }),
    i(0),
    t({ "", "}" }),
  }),
  s({ trig = "interface", name = "Interface snippet" }, {
    t("interface "),
    f(function() return vim.fn.expand("%:t:r") end),
    t({ " {", "    " }),
    i(0),
    t({ "", "}" }),
  }),
  s({ trig = "record", name = "Record snippet" }, {
    t("record "),
    f(function() return vim.fn.expand("%:t:r") end),
    t({ " {", "    " }),
    i(0),
    t({ "", "}" }),
  }),
  s({ trig = "struct", name = "Struct snippet" }, {
    t("struct "),
    f(function() return vim.fn.expand("%:t:r") end),
    t({ " {", "    " }),
    i(0),
    t({ "", "}" }),
  }),
})
 

名前空間も同じノリでやろうとしましたが,意外と苦戦してまだできていません.今は同じディレクトリにあるファイルからコピペしてます.

まとめ

Visual StudioとかRider使おう

よくある質問

Q. VSCode Neovim/IdeaVim使わないの?

A. 操作がキーボードだけで完結しないので使いません

次回は1日飛ばして小野寺春樹氏がJqをパワーで解決するそうです.