以下の内容はhttps://syu-m-5151.hatenablog.com/entry/2026/01/29/130742より取得しました。


2026年1月 Neovim の Rust 環境を見直した

はじめに

github.com

先週、エージェントが書いた200行のコードを開いた。move |ctx| { ... } というクロージャがあった。この ctx は何をキャプチャしているのか。所有権は移動しているのか、借用なのか。コードを読んでも分からない。コンパイルしてエラーが出るまで待つか、エージェントに「このクロージャは何をキャプチャしてる?」と聞くか。どちらも面倒だった。

エージェントにすべてを任せれば楽になる、と思っていた時期がある。しかし1ヶ月ほど使って気づいた。大きな変更はエージェントが得意だ。ファイルを跨いだリファクタリング、新機能の実装、テストの追加。これらは確かにエージェントに任せた方が速い。しかし、生成されたコードの一部だけを直したいとき、エージェントに再度依頼するのは効率が悪い。「この unwrap()? に変えたい」「このクロージャの引数名を直したい」「この変数を別のスコープに移動したい」といった微修正は、手で直した方が速い。

私は1日の開発時間のうち、7割をエージェントとの対話に、3割をNeovimでの直接作業に使っている。この3割のほとんどはコードリーディングとコードベースの理解で、実際に手で修正するのは5%程度だ。それでも、コードを「読む」環境が貧弱だと、全体の生産性が落ちる。

Neovimを使う理由は、思考のスピードで編集できるからだ。ciw で単語を置換し、. で繰り返し、/ で検索して n で次へ。この一連の操作が指に染み付いていると、「直したい」と思った瞬間に直せる。エージェントに依頼を書いて、結果を待って、差分を確認する時間がない。

私は Claude Code も Neovim から起動している。ターミナルで claude を叩き、エージェントと対話し、生成されたコードを Neovim で開いて確認する。すべてが同じ環境で完結する。エージェントが書いたコードを「読む」環境と、細部を「直す」環境。この両方が揃って初めて、エージェント時代の開発は快適になる。

この記事では、Neovim 0.11+ と Rust の開発環境を見直した結果を紹介する。冒頭で困っていた「クロージャのキャプチャが分からない」問題を解決する Inlay Hints と、素早い修正を可能にするキーマップに重点を置いている。

構成の概要

私の開発環境全体については以下の記事で紹介している。本記事では Rust 関連の設定に絞って解説する。

syu-m-5151.hatenablog.com

私の Neovim 環境は NvChad をベースにしている。プラグイン管理には lazy.nvim を使い、Rust 関連は以下の構成だ。

コンポーネント 役割
rust-analyzer LSP(言語サーバー)、nvim-lspconfig 経由で設定
rustaceanvim Rust専用の拡張機能(テスト実行、マクロ展開など)
crates.nvim Cargo.toml の依存関係管理
nvim-dap デバッグサポート(LLDB連携)
conform.nvim フォーマッタ(rustfmt)
mason.nvim LSP/DAP のインストール管理

エージェント連携として claudecode.nvim も入れている。Neovim から Claude Code を起動し、生成されたコードをそのまま編集できる。

以下では、この構成を「コードを読む」→「テスト・実行する」→「細部を直す」の順で紹介する。rust-analyzer と Inlay Hints でコードを理解し、rustaceanvim でテストを回し、conform.nvim でフォーマットを整える。この流れが、エージェントが書いたコードを確認・修正するワークフローと対応している。

rust-analyzer の設定

github.com

github.com

rust-analyzer は Rust の公式 LSP(Language Server Protocol)実装だ。LSP とは、エディタに補完、定義ジャンプ、エラー表示などの機能を提供するプロトコルで、VSCode でも Neovim でも同じ言語サーバーを使える。エージェントが生成したコードを読む際、型やライフタイム(Rust特有のメモリ管理の仕組みで、参照がいつまで有効かを示す)の情報がインラインで表示されると理解が格段に速くなる。

私は nvim-lspconfig 経由で設定している。Neovim 0.11+ では vim.lsp.config API が使えるようになり、設定がシンプルになった。

-- lua/configs/lspconfig.lua
rust_analyzer = {
  settings = {
    ["rust-analyzer"] = {
      checkOnSave = {
        command = "clippy",
        extraArgs = { "--all", "--", "-W", "clippy::all" },
      },
      cargo = {
        allFeatures = true,
        loadOutDirsFromCheck = true,
        buildScripts = { enable = true },
      },
      procMacro = {
        enable = true,
        attributes = { enable = true },
      },
      inlayHints = {
        enable = true,
        chainingHints = { enable = true },
        typeHints = { enable = true, hideClosureInitialization = true },
        parameterHints = { enable = true },
        closureReturnTypeHints = { enable = "with_block" },
        lifetimeElisionHints = { enable = "skip_trivial", useParameterNames = true },
        maxLength = 25,
        bindingModeHints = { enable = true },
        closureCaptureHints = { enable = true },
        discriminantHints = { enable = "fieldless" },
        expressionAdjustmentHints = { enable = "reborrow" },
        rangeExclusiveHints = { enable = true },
      },
      completion = {
        autoimport = { enable = true },
        postfix = { enable = true },
        callable = { snippets = "fill_arguments" },
        fullFunctionSignatures = { enable = true },
        privateEditable = { enable = true },
      },
      imports = {
        granularity = { group = "module" },
        prefix = "self",
      },
      diagnostics = {
        enable = true,
        experimental = { enable = true },
        styleLints = { enable = true },
      },
      semanticHighlighting = {
        operator = { specialization = { enable = true } },
        punctuation = { enable = true, specialization = { enable = true } },
        strings = { enable = true },
      },
      hover = {
        actions = {
          enable = true,
          references = { enable = true },
          run = { enable = true },
          debug = { enable = true },
          gotoTypeDef = { enable = true },
          implementations = { enable = true },
        },
        documentation = { enable = true, keywords = { enable = true } },
        links = { enable = true },
      },
      typing = {
        autoClosingAngleBrackets = { enable = true },
      },
      lens = {
        enable = true,
        references = { enable = true, adt = { enable = true }, enumVariant = { enable = true }, method = { enable = true }, trait = { enable = true } },
        implementations = { enable = true },
        run = { enable = true },
        debug = { enable = true },
      },
      workspace = {
        symbol = { search = { kind = "all_symbols" } },
      },
    },
  },
}

-- Neovim 0.11+ の新しい API を使用
vim.lsp.config("rust_analyzer", config)
vim.lsp.enable("rust_analyzer")

主な設定項目

inlayHints - エージェントが生成したコードを読むとき、最も役立つのがこれだ。エディタ内にインラインで型情報やパラメータ名が表示される。特に closureCaptureHints は重宝している。Rustではクロージャ|x| x + 1 のような無名関数)が外部の変数を使うとき、その変数を「キャプチャ」する。move |data| { ... } と書くと、data の所有権がクロージャに移動(ムーブ)する。この「何がキャプチャされているか」がエディタ上に [move: data] と表示されるようになる。エージェントが書いたクロージャを理解するのに、コンパイルエラーを待つ必要がなくなった。rangeExclusiveHints も地味に便利で、0..len が排他的(len を含まない)であることを .. の横に明示してくれる。

diagnostics.experimental - 実験的な診断機能を有効化する。styleLints を有効にすると、Clippy のスタイル系リントも保存時に表示される。エージェントが生成したコードは動くが、慣用的でないことがある。この設定で「動くけど直したほうがいい」箇所が分かる。

semanticHighlighting - 演算子や句読点に対してセマンティックハイライトを適用する。*self.data のような式で * が参照外しとして色付けされると、複雑な式の構造が視覚的に分かる。ただし、色数を増やしすぎるとノイズになるので、私は operatorpunctuation のみ有効にしている。

hover.actions - ホバー時に「Run」「Debug」「Go to Type Definition」などのアクションを表示する。エージェントが追加したテストを実行したいとき、テスト関数にカーソルを合わせて K を押すだけで実行できる。

typing.autoClosingAngleBrackets - Vec< と入力すると自動的に > が補完される。エージェントが書いた型を微修正するとき、> の数を数えなくて済む。

rustaceanvim の設定

github.com

rust-analyzer がコードを「読む」ための機能を提供するのに対し、rustaceanvim は「テスト・実行・デバッグ」のための機能を提供する。両者は補完関係にあり、rust-analyzer の LSP 機能に加えて、Rust 特有の操作(マクロ展開、テスト実行など)を追加する。

rustaceanvim は rust-tools.nvim の後継だが、単なるメンテナンス引き継ぎではない。最大の違いは遅延読み込みへの対応で、.rs ファイルを開くまで何も読み込まない。私の環境では Neovim の起動時間が 120ms から 45ms に短縮された。

エージェント時代に rustaceanvim が重要な理由は、「素早い確認と修正」のワークフローを支えることにある。エージェントがコードを生成したら、テストを実行し、エラーがあれば修正し、また実行する。このサイクルを <leader>rt(テスト実行)と <leader>re(エラー説明)で高速に回せる。

-- lua/plugins/lang.lua
{
  "mrcjkb/rustaceanvim",
  version = "^5",
  lazy = false,
  init = function()
    vim.g.rustaceanvim = {
      tools = {
        hover_actions = { replace_builtin_hover = false },
        float_win_config = { border = "rounded" },
        inlay_hints = { auto = true },
        code_actions = { ui_select_fallback = true },
      },
      server = {
        on_attach = function(_, bufnr)
          local opts = { silent = true, buffer = bufnr }
          vim.keymap.set("n", "<leader>ra", function() vim.cmd.RustLsp "codeAction" end, vim.tbl_extend("force", opts, { desc = "Rust code action" }))
          vim.keymap.set("n", "<leader>rd", function() vim.cmd.RustLsp "debuggables" end, vim.tbl_extend("force", opts, { desc = "Rust debuggables" }))
          vim.keymap.set("n", "<leader>rr", function() vim.cmd.RustLsp "runnables" end, vim.tbl_extend("force", opts, { desc = "Rust runnables" }))
          vim.keymap.set("n", "<leader>rt", function() vim.cmd.RustLsp "testables" end, vim.tbl_extend("force", opts, { desc = "Rust testables" }))
          vim.keymap.set("n", "<leader>rm", function() vim.cmd.RustLsp "expandMacro" end, vim.tbl_extend("force", opts, { desc = "Expand macro" }))
          vim.keymap.set("n", "<leader>rc", function() vim.cmd.RustLsp "openCargo" end, vim.tbl_extend("force", opts, { desc = "Open Cargo.toml" }))
          vim.keymap.set("n", "<leader>rp", function() vim.cmd.RustLsp "parentModule" end, vim.tbl_extend("force", opts, { desc = "Parent module" }))
          vim.keymap.set("n", "<leader>rj", function() vim.cmd.RustLsp "joinLines" end, vim.tbl_extend("force", opts, { desc = "Join lines" }))
          vim.keymap.set("n", "<leader>rs", function() vim.cmd.RustLsp "ssr" end, vim.tbl_extend("force", opts, { desc = "Structural search replace" }))
          vim.keymap.set("n", "<leader>re", function() vim.cmd.RustLsp "explainError" end, vim.tbl_extend("force", opts, { desc = "Explain error" }))
          vim.keymap.set("n", "<leader>rD", function() vim.cmd.RustLsp "renderDiagnostic" end, vim.tbl_extend("force", opts, { desc = "Render diagnostic" }))
          vim.keymap.set("n", "K", function() vim.cmd.RustLsp { "hover", "actions" } end, vim.tbl_extend("force", opts, { desc = "Rust hover actions" }))
        end,
        default_settings = {
          ["rust-analyzer"] = {
            cargo = { allFeatures = true },
            checkOnSave = { command = "clippy" },
          },
        },
      },
      dap = {
        adapter = {
          type = "executable",
          command = "lldb-dap",
          name = "rt_lldb",
        },
      },
    }
  end,
},

キーマップ一覧

キー 機能
<leader>ra コードアクション
<leader>rr 実行可能ターゲットを選択して実行
<leader>rt テストを選択して実行
<leader>rd デバッグ実行
<leader>rm カーソル位置のマクロを展開
<leader>re エラーの詳細説明を表示
<leader>rD 診断をレンダリング
<leader>rs 構造的検索置換(SSR)
<leader>rp 親モジュールに移動
<leader>rj 行を結合
<leader>rc Cargo.toml を開く
K ホバーアクション付きドキュメント

crates.nvim の設定

github.com

Rustでは Cargo.toml というファイルで依存ライブラリ(クレートと呼ぶ)を管理する。Node.js の package.jsonPythonrequirements.txt に相当するものだ。クレートには「フィーチャー」という機能のオン・オフがあり、必要な機能だけを有効にしてビルドサイズを抑えることができる。

エージェントに「tokio を追加して」と頼むと、だいたい最新版を入れてくれる。しかし、フィーチャーの選択は雑なことが多い。tokio = { version = "1", features = ["full"] } と書かれていて、「full はオーバーキルだな、macros と rt-multi-thread だけでいいのに」と思うことがある。

crates.nvim があれば、Cargo.toml 上でクレートにカーソルを合わせて <leader>cf を押すだけで、フィーチャー一覧がポップアップする。必要なものだけ選んで、不要なものは外す。この微調整はエージェントに頼むより、手でやった方が速い。

-- lua/plugins/lang.lua
{
  "saecki/crates.nvim",
  tag = "stable",
  event = { "BufRead Cargo.toml" },
  dependencies = { "nvim-lua/plenary.nvim" },
  config = function()
    local crates = require "crates"
    crates.setup {
      completion = {
        cmp = { enabled = true },
        crates = { enabled = true, max_results = 8, min_chars = 3 },
      },
      lsp = {
        enabled = true,
        on_attach = function(_, bufnr)
          local opts = { silent = true, buffer = bufnr }
          vim.keymap.set("n", "<leader>ct", crates.toggle, vim.tbl_extend("force", opts, { desc = "Toggle crates" }))
          vim.keymap.set("n", "<leader>cr", crates.reload, vim.tbl_extend("force", opts, { desc = "Reload crates" }))
          vim.keymap.set("n", "<leader>cv", crates.show_versions_popup, vim.tbl_extend("force", opts, { desc = "Show versions" }))
          vim.keymap.set("n", "<leader>cf", crates.show_features_popup, vim.tbl_extend("force", opts, { desc = "Show features" }))
          vim.keymap.set("n", "<leader>cd", crates.show_dependencies_popup, vim.tbl_extend("force", opts, { desc = "Show dependencies" }))
          vim.keymap.set("n", "<leader>cu", crates.update_crate, vim.tbl_extend("force", opts, { desc = "Update crate" }))
          vim.keymap.set("v", "<leader>cu", crates.update_crates, vim.tbl_extend("force", opts, { desc = "Update crates" }))
          vim.keymap.set("n", "<leader>cU", crates.upgrade_crate, vim.tbl_extend("force", opts, { desc = "Upgrade crate" }))
          vim.keymap.set("v", "<leader>cU", crates.upgrade_crates, vim.tbl_extend("force", opts, { desc = "Upgrade crates" }))
          vim.keymap.set("n", "<leader>cA", crates.upgrade_all_crates, vim.tbl_extend("force", opts, { desc = "Upgrade all crates" }))
          vim.keymap.set("n", "<leader>cH", crates.open_homepage, vim.tbl_extend("force", opts, { desc = "Open homepage" }))
          vim.keymap.set("n", "<leader>cR", crates.open_repository, vim.tbl_extend("force", opts, { desc = "Open repository" }))
          vim.keymap.set("n", "<leader>cD", crates.open_documentation, vim.tbl_extend("force", opts, { desc = "Open docs.rs" }))
          vim.keymap.set("n", "<leader>cC", crates.open_crates_io, vim.tbl_extend("force", opts, { desc = "Open crates.io" }))
        end,
        actions = true,
        completion = true,
        hover = true,
      },
      popup = {
        border = "rounded",
        show_version_date = true,
        max_height = 30,
        min_width = 20,
      },
    }
  end,
},

キーマップ一覧

キー 機能
<leader>ct crates.nvim の表示切り替え
<leader>cr クレート情報を再読み込み
<leader>cv バージョン一覧をポップアップ表示
<leader>cf フィーチャー一覧を表示
<leader>cd 依存関係を表示
<leader>cu クレートを最新パッチバージョンに更新(ビジュアルモードで複数選択可)
<leader>cU クレートを最新バージョンにアップグレード
<leader>cA すべてのクレートをアップグレード
<leader>cH クレートのホームページを開く
<leader>cR クレートの GitHub リポジトリを開く
<leader>cD docs.rs を開く
<leader>cC crates.io を開く

nvim-dap によるデバッグ

github.com

github.com

github.com

github.com

エージェントが生成したコードで「なぜこの値になるのか分からない」という場面がある。println デバッグで済むこともあるが、複雑なロジックでは変数の変化を追いたくなる。

Rust のデバッグには LLDB を使う。LLDB は C/C++/Rust などのコンパイル言語向けのデバッガで、ブレークポイント(プログラムを一時停止する地点)を設定し、変数の中身を確認しながらステップ実行できる。macOS の場合は Homebrew で LLVM をインストールし、その中に含まれる lldb-dapDAP = Debug Adapter Protocol)を使う。

-- lua/plugins/lang.lua
{
  "mfussenegger/nvim-dap",
  lazy = true,
  dependencies = {
    "rcarriga/nvim-dap-ui",
    "nvim-neotest/nvim-nio",
    "theHamsta/nvim-dap-virtual-text",
  },
  keys = {
    { "<leader>db", function() require("dap").toggle_breakpoint() end, desc = "Toggle breakpoint" },
    { "<leader>dB", function() require("dap").set_breakpoint(vim.fn.input "Breakpoint condition: ") end, desc = "Conditional breakpoint" },
    { "<leader>dc", function() require("dap").continue() end, desc = "Continue" },
    { "<leader>dC", function() require("dap").run_to_cursor() end, desc = "Run to cursor" },
    { "<leader>di", function() require("dap").step_into() end, desc = "Step into" },
    { "<leader>do", function() require("dap").step_over() end, desc = "Step over" },
    { "<leader>dO", function() require("dap").step_out() end, desc = "Step out" },
    { "<leader>dp", function() require("dap").pause() end, desc = "Pause" },
    { "<leader>dr", function() require("dap").repl.toggle() end, desc = "Toggle REPL" },
    { "<leader>dt", function() require("dap").terminate() end, desc = "Terminate" },
    { "<leader>du", function() require("dapui").toggle() end, desc = "Toggle DAP UI" },
    { "<leader>de", function() require("dapui").eval() end, desc = "Eval", mode = { "n", "v" } },
  },
  config = function()
    local dap = require "dap"
    local dapui = require "dapui"

    -- DAP UI setup with custom layout
    dapui.setup {
      icons = { expanded = "▾", collapsed = "▸", current_frame = "▸" },
      layouts = {
        {
          elements = {
            { id = "scopes", size = 0.25 },
            { id = "breakpoints", size = 0.25 },
            { id = "stacks", size = 0.25 },
            { id = "watches", size = 0.25 },
          },
          size = 40,
          position = "left",
        },
        {
          elements = {
            { id = "repl", size = 0.5 },
            { id = "console", size = 0.5 },
          },
          size = 10,
          position = "bottom",
        },
      },
    }

    -- Virtual text for debugging
    require("nvim-dap-virtual-text").setup { enabled = true, commented = true }

    -- LLDB adapter
    dap.adapters.lldb = {
      type = "executable",
      command = "/opt/homebrew/opt/llvm/bin/lldb-dap",
      name = "lldb",
    }

    dap.configurations.rust = {
      {
        name = "Launch",
        type = "lldb",
        request = "launch",
        program = function()
          return vim.fn.input("Path to executable: ", vim.fn.getcwd() .. "/target/debug/", "file")
        end,
        cwd = "${workspaceFolder}",
        stopOnEntry = false,
        args = {},
        runInTerminal = false,
      },
    }

    -- Auto open/close DAP UI
    dap.listeners.after.event_initialized["dapui_config"] = function() dapui.open() end
    dap.listeners.before.event_terminated["dapui_config"] = function() dapui.close() end
    dap.listeners.before.event_exited["dapui_config"] = function() dapui.close() end

    -- Signs
    vim.fn.sign_define("DapBreakpoint", { text = "●", texthl = "DapBreakpoint" })
    vim.fn.sign_define("DapBreakpointCondition", { text = "●", texthl = "DapBreakpointCondition" })
    vim.fn.sign_define("DapStopped", { text = "▶", texthl = "DapStopped", linehl = "DapStoppedLine" })
  end,
},

キーマップ一覧

キー 機能
<leader>db ブレークポイントの切り替え
<leader>dB 条件付きブレークポイント
<leader>dc 続行
<leader>dC カーソル位置まで実行
<leader>di ステップイン
<leader>do ステップオーバー
<leader>dO ステップアウト
<leader>dp 一時停止
<leader>dr REPL の切り替え
<leader>dt 終了
<leader>du DAP UI の切り替え
<leader>de カーソル位置の式を評価(ビジュアルモードでも使用可)

conform.nvim でのフォーマット

github.com

保存時に rustfmt を自動実行する。エージェントが生成したコードはフォーマットが崩れていることがあるので、保存するだけで整形されるのは便利だ。

-- lua/plugins/lsp.lua
{
  "stevearc/conform.nvim",
  event = "BufWritePre",
  config = function()
    require("conform").setup {
      formatters_by_ft = {
        rust = { "rustfmt", lsp_format = "fallback" },
        -- 他の言語も設定可能
      },
      format_on_save = { timeout_ms = 500, lsp_fallback = true },
    }
  end,
},

ここまでで「コードを読む」「テスト・実行する」「細部を直す」の設定が揃った。最後に、これらのツール自体をどうインストールするかを紹介する。

Mason でのツールインストール

github.com

LSP サーバーやデバッグアダプタは Mason で管理する。:MasonInstall で個別にインストールすることもできるが、ensure_installed に書いておけば自動でインストールされる。

-- lua/plugins/lsp.lua
{
  "williamboman/mason.nvim",
  opts = {
    ensure_installed = {
      "rust-analyzer",
      "codelldb",  -- DAP adapter
      -- 他の言語のツールも同様に追加
    },
  },
},

まとめ

この設定を入れた翌日、またエージェントが書いたコードを開いた。move |ctx| { ... } というクロージャがある。今度は違った。クロージャの横に [move: ctx, config] と表示されている。何がキャプチャされているか、一目で分かる。コンパイルを待つ必要も、エージェントに聞く必要もない。

エージェントに任せる7割と、自分で読む3割。この3割を Neovim で快適にすることが、全体の生産性につながる。rust-analyzer の Inlay Hints でコードを読み、rustaceanvim のキーマップでテストを回し、思考のスピードで細部を直す。エージェント時代だからこそ、手元のエディタは大事だ。




以上の内容はhttps://syu-m-5151.hatenablog.com/entry/2026/01/29/130742より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14