❄️

nvim-treesitterのパーサーをnixで管理します

に公開

経緯

現在Neovim本体の管理にnixを利用しているのだが、大量にあるnvim-treesitterのパーサーもnixで管理したいと思いはじめた。しかしプラグインの設定までもnixで管理するのはやや過剰であり、また逆に不便になる。そこでnixでの管理と設定の利便性を両立させるように試行錯誤し、その解決法の1つを導いた。

TL;DR

プラグイン本体をnixで生成し適当なパスに配置、またvimrc以下にプラグインの設定を書きパッケージマネージャー側で2つを読み取りマージする。

環境

home-manager + dpp.vim

どうしたか

アイデア

2つの両立を考えた結果、nixで~/.cache/dpp/_generated/nvim-treesitterにパーサー入りプラグインを配置し、dpp-ext-localでローカルプラグインとして読み込むことにした。

_generatedを~/.cache/dpp以下に配置した理由

自分の環境では~/dotfiles/vim~/.config/nvimにシンボリックリンクしている。この状態で~/.config/nvim/dpp/_generatedにhome-managerで配置しようとしてもエラーが発生する。一方これを回避するために~/dotfiles/vim/dpp/_generatedに配置することも考えたが、これは美しくない。_generatedは消えても再生成できるなどの理由から~/.cache/dpp以下に配置するのが美しいと考えた。

これを実現するために、pkgs.symlinkJoinを用いてnvim-treesitter本体とパーサーを1つのディレクトリにまとめてそれを配置することにした。

実装

home-managerの設定ファイルに

  home.file =
    let
      base = ".cache/dpp/_generated";
    in
    pkgs.lib.attrsets.foldlAttrs (
      acc: name: drv:
      acc // { "${base}/${name}".source = drv; }
    ) { } (import ./plugins.nix { inherit pkgs; });

を書き加え、plugins.nix

plugin.nix
{ pkgs }:
{
  nvim-treesitter =
    let
      ts = pkgs.vimPlugins.nvim-treesitter;
    in
    pkgs.symlinkJoin {
      name = "ts-all";
      paths = [
        ts
      ] ++ ts.withAllGrammars.dependencies;
    };
}

とする。こうすることにより~/.cache/dpp/_generated/nvim-treesitterにnvim-treesitter全部入りのパスを指定したプラグインが配置される。これをdpp.vimが読めるようにすればよい。

dpp-ext-localを用いて以下のようにして読み込む。

const [localExt, localOptions, localParams] = await args.dpp.getExt(
  args.denops,
  options,
  "local",
) as [LocalExt | undefined, ExtOptions, LocalParams];
if (localExt) {
  const local = localExt.actions.local;

  const localPlugins = await local.callback({
    denops: args.denops,
    context,
    options,
    protocols,
    extOptions: localOptions,
    extParams: localParams,
    actionParams: {
      directory: "~/.cache/dpp/_generated",
    },
  });

  for (const plugin of localPlugins) {
    if (plugin.name in recordPlugins) {
      Object.assign(recordPlugins[plugin.name], plugin);
    } else {
      recordPlugins[plugin.name] = plugin;
    }
  }
}

また、nvim-treesitterの設定でパーサーの自動アップデートを記述している場合は削除する。この変更で既存の設定はそのままに、プラグイン本体とパーサーはnixのものを使うことができる。

これでbuiltinのパーサーについては完了した。次に外部のパーサーを追加する方法を記す。

外部のパーサーを追加する

ここではtree-sitter-satysfiを追加することにしよう。まずsatysfiのパーサーを作成する。適当な場所に

{ tree-sitter, fetchFromGitHub }: tree-sitter.buildGrammar {
  language = "satysfi";
  version = "5519c547418ecb31ac7d63e64653aed726b5d1c3";
  src = fetchFromGitHub {
    owner = "monaqa";
    repo = "tree-sitter-satysfi";
    rev = "5519c547418ecb31ac7d63e64653aed726b5d1c3";
    fetchSubmodules = false;
    sha256 = "sha256-yei8UHiVChYpx2UyPsDyOd3usItZN68rwu0+VoBtPi0=";
  };
}

(一例、nvfetcherを利用してもよい)を配置し、callPackageをする。これをtree-sitter-satysfiとする。これをplugins.nixに渡し、以下のようにすればよい。

plugin.nix
-            paths = [ ts ] ++ ts.withAllGrammars.dependencies;
+            paths = [ ts (pkgs.neovimUtils.grammarToPlugin tree-sitter-satysfi) ] ++ ts.withAllGrammars.dependencies;

これで新たなパーサーが追加された。どうやらREADMEにあるqueries/以下をコピーする工程は必要ないらしい。

おわりに

nvim-treesitterの設定自体はそこまで頻繁にいじるものでもないので完全にnix側に倒してもよかった。だがこの機会に導入してみたことが他の様々なプラグイン本体をnixで管理したくなった時に役立つのだと思う。

余談

拡張性のある実装をしているため、たとえばskkの辞書もnixで管理したくなったとき、

plugins.nix
skk-dict = pkgs.skkDictionaries.l;

の行を追記し、vim側で

const s:skk_dict = dpp#get('skk-dict').path

とすれば大きな手を加えることなく管理ができる。

GitHubで編集を提案

Discussion