📝

Neovim on Nix/HomeManager で息の長いプラグイン管理とプロジェクトごとの隔離環境を実現

2024/12/09に公開

この記事は CADDi プロダクトチーム Advent Calendar 2024 の9日目の記事です。

想定読者

  • neovim を開発用エディタとしてに利用している
  • nix についてなんとなく知っている

はじめに

Neovimプラグイン管理に感じる課題

Neovimでのプラグイン管理は多くの優れたツールによって支えられてきました。dein.vim、packer.nvim、lazy.nvim、Masonなどのツールは素晴らしい機能を提供し、Vim/Neovimコミュニティに大きな価値をもたらしてきました。私がこれまで快適なNeovim環境を構築できたのもメンテナの方々のたゆまぬ努力のおかげであり、感謝と尊敬の念を持たずにはいられません。

しかし、エディタ環境の長期的な維持という観点から見ると、いくつかの課題も見えてきます。

  • プラグインマネージャーなど各種ツールのメンテナンス負荷がowner個人に集中してしまっていること
  • 個別のエコシステムへの依存

筆者個人の意見ですが、1~2年に1度何かしらのツールがメンテナンス停止になってしまい、環境を大掃除する必要性がでてくるというのがneovimをIDEとして利用する際の課題感でした。

長く使い続けられるベースの環境を作りたいというモチベーションから、今回はNixをつかってのNeovim環境の構築を検討してみました。

Nixによるアプローチ

Nixは、NixOS Foundation という大きなコミュニティによって維持されている包括的なパッケージマネージャーです。Nixには以下のような特徴があります。

  • 宣言的な設定によって環境を再現可能
  • プロジェクト固有の隔離された環境をつくれる

この記事では、NixとHome Managerというツールを利用してNeovim環境を構築する方法を紹介します。これによって、

  • NixとHomeManager というツールのみに依存してneovim環境を構築することができる
  • Package の install を Environment as a Code として定義/管理することで、環境の再現性を高めることができる
  • プロジェクトごとに隔離された開発環境をホストマシン上で実現する

といったメリットを享受していきたいと思います。

ツールの紹介

Nix とは

WIP

HomeManager とは

WIP

構築手順

0. 当記事で構築するもの

Nix と HomeManager のみを用いて、Neovimの環境を構築します。
例としてTypeScriptのLSPをinstallして利用する設定を行います。

当記事では触れないもの

  • Nix や HomeManager の詳細な説明はしません
  • Nix language の 文法は説明しません
  • lua の 文法は説明しません

1. Nixのインストール

sh <(curl -L https://nixos.org/nix/install) --daemon

MacOS や Windows については、公式のインストールガイドを参照してください。
nix-install

2. HomeManagerのインストール

HomeManager に従ってinstallします。

home-manager の channel を add します。

nix-channel --add https://github.com/nix-community/home-manager/archive/master.tar.gz home-manager
nix-channel --update

home-manager の install commandを実行します。

nix-shell '<home-manager>' -A install

インストールに成功すると "~/.config/home-manager/home.nix" に以下のようなファイルが生成されています。今後ここにhome環境のグローバルな設定を記述していきます。

{ config, pkgs, ... }:

{
  home.username = "<your name>";
  home.homeDirectory = "/Users/<your name>";

  home.stateVersion = "24.11";

  home.packages = [
  ];

  home.file = {
  };

  home.sessionVariables = {
    # EDITOR = "emacs";
  };

  programs.home-manager.enable = true;
}

3. Neovim のインストール

~/.config/home-manager/home.nix を編集していきます。

{ config, pkgs, ... }:

{
  # 省略...
  
  programs.neovim  = {
    enable = true;    
    plugins = with pkgs.vimPlugins; [
      nvim-lspconfig	
    ];
  };
}

これだけでneovimのinstall準備は完了です。以下のコマンドを実行してinstallしましょう。

home-manager switch

nvim が nix経由でinstall されているはずです。

which nvim

4. init.lua の作成と読み込み

まずは init.lua ファイルを作成します。簡単にTypeScriptのLSPを読み込む設定だけ書いていきましょう。
置き場所は 好きな場所にしてください。

local lspconfig = require('lspconfig')

-- Project毎に読み込みLSPを変えられるように、環境変数でLSPパスを指定可能にしています。
local ts_lsp_path = os.getenv("PATH_TO_TS_LSP") 

if ts_lsp_path then
    lspconfig.ts_ls.setup {
        cmd = {ts_lsp_path, "--stdio"},
        on_attach = function(client, bufnr)
            local set = vim.keymap.set

            -- このあたりはお好みでどうぞ
            set("n", "gD", "<cmd>lua vim.lsp.buf.declaration()<CR>")
            set("n", "gs", "<cmd>lua goto_source_definition()<CR>")
            set("n", "gd", "<cmd>lua vim.lsp.buf.definition()<CR>")
            set("n", "<space>h", "<cmd>lua vim.lsp.buf.hover()<CR>")
            set("n", "gi", "<cmd>lua vim.lsp.buf.implementation()<CR>")
            set("n", "gr", "<cmd>lua vim.lsp.buf.references()<CR>")
        end
    }
else
  print("Please set PATH_TO_TS_LSP environment variable")
end

次に HomeManager で 作成したinit.lua を neovimの設定ファイルとして読み込むようにします。

home.file というattributeに値を設定すると、そのファイルがnix store上にコピーされた上で symbolic link が作成されます。
以下の例では ~/dotfiles/nvim/init.lua にあるファイルを nix store上にコピーし、そのシンボリックリンクを ~/.config/nvim/init.lua に作成します。

{ config, pkgs, ... }:

{
  # 省略...

  home.file = {
    ".config/nvim/init.lua".source = <path to your init.lua file>;
  # 例
    # ".config/nvim/init.lua".source = ~/dotfiles/nvim/init.lua;
  };
  
}

以下のコマンドを実行して適用完了です

home-manager switch

6. shell.nix で ディレクトリ毎に隔離されたTypeScript開発環境を構築

つづいて shell.nix を作成して、ディレクトリ毎に隔離されたTypeScript開発環境を構築していきます。Language Server もここでinstallしていきます。

まずはサンプルファイルをつくります

mkdir typescript-nix
cd typescript-nix
touch index.ts

このまま index.ts を nvim で開いてもまだLSPは起動せず "Please set PATH_TO_TS_LSP environment variable" と表示されるはずです。
typescript-nix ディレクトリ配下に shell.nix を作成していきましょう

{ pkgs ? import <nixpkgs> {} }:

pkgs.mkShellNoCC {
  buildInputs = with pkgs; [
    nodejs_20
    nodePackages.typescript
    nodePackages.typescript-language-server
    nodePackages.npm
    nodePackages.pnpm
  ];

  shellHook = ''
    export PATH_TO_TS_LSP="${pkgs.nodePackages.typescript-language-server}/bin/typescript-language-server"
    echo "TypeScript development environment loaded"
    echo "Node version: $(node --version)" 
    echo "TypeScript version: $(tsc --version)"
  '';
}

nix-shellコマンドを入力し隔離された環境を起動してみましょう。

nix-shell 

このシェル内で nvim を起動し、index.ts を開いてみると、TypeScriptのLSPが起動しているはずです。

shell.nix / nix-shell とは(WIP)

7. nix-direnv でディレクトリ毎の個別環境を自動読み込みする

ディレクトリに移動するたびに nix-shell を起動するのは面倒なので、nix-direnvを導入してshell.nixがあるディレクトリに移動するたびにnix-shellが自動起動するようにしましょう。

home.nix を編集して direnv と nix-direnv をenableします。

{ config, pkgs, ... }:

{
  # 省略...

  programs.direnv = {
    enable = true;
    # zsh  や fish 向けのplugin もあるのでお好みで
    enableBashIntegration = true;
    nix-direnv.enable = true;
  };

}

変更を適用します。

home-manager switch

enableBashIntegration をenable = true にしているため .bashrc に direnvの初期化処理が追加されます。( eval "$(direnv hook bash)" )

先ほど作成した typescript-nix ディレクトリで direnvを使えるようにしましょう。

cd typescript-nix
echo "use nix" > .envrc

課題

Nix / HomeManager が packer.nvim や lazy.nvim の役割を完全に代替できるものかどうかは、まだ筆者には判断がついておらず検証が必要です。

LSP周りの管理には十分使えると感じていますが、その他のプラグイン管理に十分なものかがまだわかっていません。たとえばlazy.nvimがやってくれているような遅延読み込み機能はこの構成だけでは実現できないため、自前でluaを書いて実装する必要があります。もしかするとNix/HomeManager と lazy.nvim を併用するのがベストな選択肢の可能性もあります。年始にむけてコツコツと検証を進めていきたいと思います。

あとがき

弊社CADDiでは絶賛エンジニアを採用中です。興味がある方はぜひご応募・ご連絡ください!

https://recruit.caddi.tech/

それでは皆さん、良い年末を!

Discussion