はじめに
全てのエンジニアが生成AIやAgentに夢中な2025年。私、nwiizoは今日もNeovimのプラグインを開発しています。今回は、RustのCargoコマンドをNeovimから直接実行できるプラグイン「cargo.nvim」の開発で得た知見を共有したいと思います。
🦀 Built cargo.nvim - a Neovim plugin for seamless Rust development. Build & run Rust right in your editor, no terminal switching needed! ⚡️ #Rust #nvimhttps://t.co/YxVJzpYfmv pic.twitter.com/x5yIdpEdVN
— nwiizo (@nwiizo) 2025年1月15日
「なぜRustなんだ?」と思われる方もいるかもしれません。正直に言うと、私もその理由を完全には説明できません。ただ、このプラグインを作るとき、「これはRustで書くべきだ」という強い直感がありました。影響ヒューリスティックというか...直感というか...。もちろん、システムプログラミング言語としての堅牢性や、非同期処理の扱いやすさといった技術的な理由もありますが...実はそれ以上に「Rustでプラグインを書くのがかっこいい」という、エンジニアとしてのロマンを追求した結果です。はい、完全にロマン駆動開発です。
「なぜ今更vim?」「それAIで解決できないの?」という声が聞こえてきそうですが、私にとってvimプラグイン開発は単なるツール作りではありません。手触り感のあるエンジニアリング、そう呼びたくなる体験なのです。最新のAIが次々と登場する中で、あえて低レイヤーな開発に没頭する。それは、ある意味で技術的なロマンなのかもしれません。
エンジニアの仕事はどんどんAIに寄り添うものになっていくでしょう。それは素晴らしいことだと思います。でも、だからこそ、私は基礎となる技術や職人的なクラフトマンシップを大切にしたいと考えています。
そんな思いを込めて、今回はRustでvimプラグインを作ってみました。はい、かなり強引な導入ですが...。
良ければGitHubでStarをつけていただけると嬉しいです。みなさんの応援が、「なんとなくRustで書いちゃった」この暴挙(?)の正当性を証明してくれる気がします。きっと...たぶん...。
ちなみにRust のプロジェクトでこういうリポジトリも存在している。 github.com
プロジェクトの構造
まず、cargo.nvimの基本構造を見てみましょう:
. ├── Cargo.toml # Rust の依存関係と設定 ├── LICENSE # ライセンス情報 ├── README.md # プロジェクトの説明 ├── build.rs # ビルド設定 ├── lua/ # Lua モジュール │ └── cargo/ │ └── init.lua # プラグインのメイン実装 ├── plugin/ # Neovim プラグイン │ └── cargo.lua # プラグインのエントリーポイント └── src/ # Rust ソースコード └── lib.rs # Rust のコア実装
この構造は、RustとLuaのハイブリッドな実装を効率的に管理するために設計されています。
RustとLuaの役割分担
cargo.nvimの特徴的な点は、RustとLuaを明確に役割分担していることです。この分担により、各言語の強みを最大限に活かした実装を実現しています。
Rust (src/lib.rs)の役割
Rust側では、プラグインの中核となる処理を担当しています。コアロジックの実装として、Cargoコマンドの実行を制御し、非同期処理を管理します。特に重要なのは、システムレベルの操作やエラーハンドリングの部分です。Rustの型システムと所有権モデルを活用することで、堅牢な実装を実現しています。
また、パフォーマンスクリティカルな処理もRustの担当です。コマンド実行の最適化に加え、メモリ管理やスレッド制御など、システムリソースに直接関わる部分を効率的に処理します。これにより、プラグイン全体の実行性能を高いレベルで維持しています。
Lua (lua/cargo/init.lua)の役割
一方、Lua側はユーザーとの接点となる部分を担当します。UIの実装では、フローティングウィンドウの表示や制御を行い、バッファの管理やキーマッピングの設定を担います。また、シンタックスハイライトによる出力の視覚的な整理も、Luaが担当する重要な役割の一つです。
さらに、Neovim APIとの連携も Luaの重要な責務です。イベントハンドリングやバッファ管理、ユーザー設定の処理など、Neovimとの緊密な連携が必要な部分を担当します。Luaの柔軟性を活かし、Neovimの機能を最大限に引き出す実装を行っています。
使用している主要なパッケージ
Rustの依存クレート (Cargo.toml)
[dependencies] # Lua連携のためのクレート mlua = { version = "0.9", features = ["luajit", "module"] } # JSONシリアライズ/デシリアライズ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" # 非同期処理 tokio = { version = "1.0", features = ["full"] } # HTTPクライアント(将来の拡張用) reqwest = { version = "0.11", features = ["json"] }
各クレートの選定理由と役割について説明します。
mlua
-
- 非同期処理の実装
- マルチスレッド制御
- リソース管理
serde
- 設定ファイルの読み込み
- JSONデータの処理
- 型安全なデータ変換
アーキテクチャの概要
cargo.nvimは2層アーキテクチャを採用しています。これは、RustとLuaの特性を最大限に活かすために慎重に設計された構造です。
[Rust Layer] ↓ mlua [Lua Layer] ↓ Neovim API [Neovim]
この2層アーキテクチャの採用により、プラグインの品質と保守性を大きく向上させることができました。Rust層とLua層の責任を明確に分離することで、各レイヤーの役割が明確になり、コードの見通しが格段に良くなりました。
特筆すべきは、mluaを介した層間の連携です。Rustの型安全性と高いパフォーマンスを維持しながら、Luaの柔軟性を活かしたNeovim APIの利用が可能となっています。この組み合わせにより、システムレベルの処理とユーザーインターフェースの実装を、それぞれの言語の強みを最大限に活かして実現できています。
また、この構造によってパフォーマンスの最適化も容易になりました。Rustでの処理が必要な重い処理と、Luaで十分な軽い処理を適切に分離することで、全体的な実行効率を高いレベルで維持できています。加えて、今後の機能追加や修正においても、各層の独立性が高いため、変更の影響範囲を最小限に抑えることができます。
開発を通じて得られた知見
プラグインの開発を進める中で、いくつかの重要な気づきがありました。
開発当初から意識していたのは責任分担です。RustとLuaの役割を明確に分けることで、開発の効率が大きく向上しました。Rustにはシステムレベルの処理を任せ、LuaではUI/UXの実装に専念する。この単純な原則が、結果として開発全体をスムーズにしてくれました。
エラーハンドリングも大きな学びの一つでした。RustとLuaの特性を活かし、Rust側では可能な限り厳密なエラー処理を行い、Lua側ではそれらのエラーをユーザーにとって理解しやすい形で表示する。この組み合わせが、プラグインの信頼性向上に貢献しています。
パフォーマンスについては、特に非同期処理の活用が効果的でした。Cargoコマンドの実行時間が長くなる場合でも、UIの応答性を維持できています。また、メモリ効率を意識した実装により、長時間の使用でもリソース消費を抑えられています。
クロスプラットフォーム対応は予想以上に課題となりました。build.rsでの環境別設定や、パス処理の違いへの対応など、細かな配慮が必要でした。面倒なので自分と同じ環境のユーザーのみに対応しましたがユーザーが増えたら対応を考えます。
これらの経験は、今後の開発にも活かしていきたいと考えています。
Rust層の実装詳細
コアストラクチャ
まず、プラグインの中核となるRustの実装を見ていきます。
#[derive(Clone)] struct CargoCommands { runtime: Arc<Runtime>, } impl CargoCommands { fn new() -> LuaResult<Self> { Ok(Self { runtime: Arc::new( tokio::runtime::Builder::new_current_thread() .enable_all() .build() .map_err(|e| LuaError::RuntimeError(e.to_string()))?, ), }) } }
このコードの重要なポイントは:
Clone
トレイトの実装によるランタイムの共有Arc
による安全な参照カウント- Tokioランタイムの効率的な管理
非同期コマンド実行
cargo.nvimの核となる機能、Cargoコマンドの実行処理です。
impl CargoCommands { async fn execute_cargo_command(&self, command: &str, args: &[&str]) -> LuaResult<String> { let mut cmd = Command::new("cargo"); cmd.arg(command); cmd.args(args); let output = cmd.output().map_err(|e| { LuaError::RuntimeError(format!("Failed to execute cargo {}: {}", command, e)) })?; if !output.status.success() { let error = String::from_utf8_lossy(&output.stderr); return Err(LuaError::RuntimeError(format!( "cargo {} failed: {}", command, error ))); } Ok(String::from_utf8_lossy(&output.stdout).into_owned()) } }
実装のポイント: - エラーの詳細な伝播 - 出力のUTF-8変換処理 - ステータスコードによる成功/失敗の判定
Lua層の実装詳細
ウィンドウ管理
Neovimのウィンドウ管理を実装する部分です。
local function create_float_win(opts) -- ウィンドウサイズの計算 local width = math.floor(vim.o.columns * opts.window_width) local height = math.floor(vim.o.lines * opts.window_height) -- バッファの作成と設定 local bufnr = vim.api.nvim_create_buf(false, true) vim.api.nvim_buf_set_option(bufnr, "buftype", "nofile") vim.api.nvim_buf_set_option(bufnr, "swapfile", false) vim.api.nvim_buf_set_option(bufnr, "modifiable", true) vim.api.nvim_buf_set_option(bufnr, "filetype", "cargo-output") -- ウィンドウ設定 local win_opts = { relative = "editor", width = width, height = height, col = math.floor((vim.o.columns - width) / 2), row = math.floor((vim.o.lines - height) / 2), style = "minimal", border = opts.border } local winnr = vim.api.nvim_open_win(bufnr, true, win_opts) return bufnr, winnr end
実装のポイント: - 画面サイズに応じた動的なレイアウト - バッファとウィンドウの適切な設定 - ユーザーカスタマイズ可能なオプション
出力処理
コマンド出力の整形と表示を担当する部分です。
local function process_output(lines) local processed = {} for _, line in ipairs(lines) do local timestamp = os.date("%H:%M:%S") local prefixed_line = string.format("[%s] ", timestamp) if line:match("^error") then table.insert(processed, prefixed_line .. "@error@" .. line) elseif line:match("^warning") then table.insert(processed, prefixed_line .. "@warning@" .. line) elseif line:match("^%s*Compiling") then table.insert(processed, prefixed_line .. "@info@" .. line) elseif line:match("^%s*Running") then table.insert(processed, prefixed_line .. "@info@" .. line) elseif line:match("^%s*Finished") then table.insert(processed, prefixed_line .. "@success@" .. line) else table.insert(processed, prefixed_line .. line) end end return processed end
実装のポイント: - タイムスタンプによる実行時間の可視化 - 出力種別に応じた装飾 - 効率的な文字列処理
プラグインの初期化と設定
動的ライブラリのロード
プラグインの初期化時に重要な、動的ライブラリのロード処理です。
local function load_cargo_lib() local plugin_dir = vim.fn.fnamemodify(vim.fn.resolve(debug.getinfo(1, "S").source:sub(2)), ":h:h:h") local lib_name = vim.fn.has("mac") == 1 and "libcargo_nvim.dylib" or vim.fn.has("win32") == 1 and "cargo_nvim.dll" or "libcargo_nvim.so" local lib_path = plugin_dir .. "/target/release/" .. lib_name if vim.fn.filereadable(lib_path) == 0 then error(string.format("Cargo library not found: %s", lib_path)) end return package.loadlib(lib_path, "luaopen_cargo_nvim")() end
実装のポイント: - クロスプラットフォーム対応 - 適切なエラーハンドリング - ライブラリパスの動的解決
パフォーマンス最適化のポイント
Rust側の最適化
Lua側の最適化
バッファ管理
- 必要最小限のバッファ更新
- 効率的な行挿入
- メモリ使用量の最適化
ウィンドウ管理
- リソースの適切な解放
- イベントの効率的な処理
- 画面更新の最適化
おわりに
「なんとなく」Rustを選んで始めたcargo.nvimの開発でしたが、結果として多くの学びがありました。特に印象的だったのは、RustとLuaという異なる言語の組み合わせが予想以上に効果的だったことです。当初は「かっこいいから」という理由で選んだRustですが、システムレベルの処理とエラーハンドリングの面で、その選択は正しかったと確信しています。
設計面では、「なんとなく」とは正反対の、明確な責任分担の重要性を学びました。RustとLuaの境界をしっかりと定義し、各言語の得意分野を活かすことで、保守性の高い構造を実現できました。また、非同期処理やメモリ効率の最適化など、パフォーマンスに関する知見も得られました。
思えば、「なんとなく」から始まったこのプロジェクトは、むしろ「必然」だったのかもしれません。時としてエンジニアの直感は、技術的な正当性を伴って現実のものとなるのだと、身をもって体験しました。
今後も、この「なんとなく」と「確信」が混ざり合った独特な開発体験を大切にしながら、より使いやすく、高性能なプラグインを目指して開発を続けていきたいと思います。最後まで読んでいただき、ありがとうございました。
自作したツールcargo.nvimについて、This Week in RustにPRを送信したところ、マージされ、"Great project, thanks for the submission!"というコメントをいただきました。https://t.co/RB1ExrAhwJ pic.twitter.com/zcUpS6eX2G
— nwiizo (@nwiizo) 2025年1月16日