この記事は Zenn にも投稿しています。

この記事は Vim 駅伝 2023/06/09(金) の記事です。

前回は sontixyou さんの Neovimで今開いているファイルのパスをコピーするスクリプトを作った話 です。 次回は 2023/06/12(月) に公開される予定です。

はじめに

Vim / Neovim にはエディタの設定を行うスクリプト言語として、 Vim script というものがあります。 また、最近だと Vim では Vim9 script 、 Neovim では Lua を使用して設定ファイルを書くこともできます。 ですが、 Vim9 script は Vim でしか使用できず、 Lua は Neovim でしか使用できません。(一部は Vim でも使用できます。) この Vim と Neovim の差は徐々に大きくなってきている気がしています。

そんな中、 Deno を使用して Vim / Neovim 両対応のプラグインを作成できる denops.vim というエコシステムができているのはご存知の方も多いのではないでしょうか。

以下にいくつかの有名なプラグインを紹介します。

また、GitHubの「vim-denopsタグ」 ( https://github.com/topics/vim-denops ) では、「denops.vim」製のプラグインをさらに探すことができます。

私もこの denops.vim が好きで、いくつかのプラグインを自分用に作ってきました。

本記事は、私のその denops.vim 好きが度を超えて、ついにプラグインだけでなく設定も denops.vim を利用して書くようになってしまった・・・というちょっと特殊で変態ちっくな内容になっています・・・。

dvpm (Denops Vim / Neovim Plugin Manager)

設定ファイルを denops.vim で書くということは、プラグインのインストールについても自分で何かしらの手段を用意しなければなりません。 そこで、 denops.vim を利用したプラグインマネージャーを作成してみました。 詳細は以下のリポジトリを参照してください。

これ自体は denops.vim のプラグインではなく、 denops.vim プラグインから利用するための Deno のライブラリとなります。(何を言っているかわからないかもしれません。)

使い方は README に記載していますが、まずは前提条件となる Deno をインストールします。

公式サイトによると以下コマンドで簡単にインストールできるようです。

  • Mac / Linux
bash
1
curl -fsSL https://deno.land/x/install/install.sh | sh
  • Windows
bash
1
irm https://deno.land/install.ps1 | iex

次に、Vim / Neovim の設定ファイルに以下を記載します。

Neovim の場合

  • ~/.config/nvim/init.lua (Mac / Linux)
  • ~/AppData/Local/nvim/init.lua (Windows)
lua
1
2
3
4
5
local denops = vim.fn.expand("~/.cache/nvim/dvpm/github.com/vim-denops/denops.vim")
if not vim.loop.fs_stat(denops) then
  vim.fn.system({ "git", "clone", "https://github.com/vim-denops/denops.vim", denops })
end
vim.opt.runtimepath:prepend(denops)

Vim の場合

  • ~/.vimrc (Mac / Linux)
  • ~/_vimrc (Windows)
vim
1
2
3
4
5
let s:denops = expand("~/.cache/vim/dvpm/github.com/vim-denops/denops.vim")
if !isdirectory(s:denops)
  execute 'silent! !git clone https://github.com/vim-denops/denops.vim ' .. s:denops
endif
execute 'set runtimepath^=' . substitute(fnamemodify(s:denops, ':p') , '[/\\]$', '', '')

denops.vim を git clone して runtimepath へ追加しています。 ここだけは Vim script / Lua による記述が必要です。 既に設定ファイルをお持ちの方がほとんどかと思いますので、 Neovim を利用している場合は、 Vim駅伝 2023/05/26 の記事 Neovimの設定すべてをまるっと切り替えられるマルチプロファイル運用 を参考に一時的に切り替えて試してみることをおすすめします。

それでは、いよいよ記事タイトルにもある通り、 TypeScript (Deno) を使用して設定を書いていきます。

以下のファイルを作成します。 ファイルパスは異なりますが、中身は同じです。

Neovim の場合

  • ~/.config/nvim/denops/config/main.ts (Mac / Linux)
  • ~/AppData/Local/nvim/denops/config/main.ts (Windows)

Vim の場合

  • ~/.vim/denops/config/main.ts (Mac / Linux)
  • ~/vimfiles/denops/config/main.ts (Windows)
typescript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
import * as fn from "https://deno.land/x/denops_std@v5.0.0/function/mod.ts";
import * as mapping from "https://deno.land/x/denops_std@v5.0.0/mapping/mod.ts";
import { Denops } from "https://deno.land/x/denops_std@v5.0.0/mod.ts";
import { ensureString } from "https://deno.land/x/unknownutil@v2.1.1/mod.ts";
import { execute } from "https://deno.land/x/denops_std@v5.0.0/helper/mod.ts";
import { globals } from "https://deno.land/x/denops_std@v5.0.0/variable/mod.ts";

import { Dvpm } from "https://deno.land/x/dvpm@1.0.0/mod.ts";

export async function main(denops: Denops): Promise<void> {

  // プラグインをインストールするベースとなるパスです。
  const base_path = (await fn.has(denops, "nvim"))
    ? "~/.cache/nvim/dvpm"
    : "~/.cache/vim/dvpm";
  const base = ensureString(await fn.expand(denops, base_path));

  // ベースパスを引数に、 Dvpm.begin を実行して、 `dvpm` インスタンスを取得します。
  const dvpm = await Dvpm.begin(denops, { base });

  // 以降は `dvpm.add` を用いて必要なプラグインを追加していきます。
  await dvpm.add({ url: "yukimemi/dps-autocursor" });

  // ブランチを指定することもできます。
  // await dvpm.add({ url: "neoclide/coc.nvim", branch: "release" });

  // build オプションでは、 `install` か `update` 実施後に実行する処理を記載できます。
  // `Denops` オブジェクト以外に、 `PlugInfo` オブジェクトを引数に取ることもできます。
  // 含まれている情報については README を参照ください。
  await dvpm.add({
    url: "neoclide/coc.nvim",
    branch: "master",
    build: async ({ info }) => {
      const args = ["install", "--frozen-lockfile"];
      const cmd = new Deno.Command("yarn", { args, cwd: info?.dst });
      const output = await cmd.output();
      console.log(new TextDecoder().decode(output.stdout));
    },
  });

  // `before` はプラグインが runtimepath へ追加される前に実行されます。
  await dvpm.add({
    url: "yukimemi/dps-autobackup",
    before: async ({ denops }) => {
      // `denops_std` の関数で Vim のグローバル変数をセットしています。
      // let g:autobackup_dir = "~/.cache/autobackup" と等価。
      await globals.set(denops,
        "autobackup_dir",
        ensureString(await fn.expand(denops, "~/.cache/autobackup")),
      );
    },
  });
  // `after` はプラグインを runtimepath へ追加した後に実行されます。
  await dvpm.add({
    url: "folke/which-key.nvim",
    after: async ({ denops }) => {
      // `denops_std` の関数 `execute` では Vim のコマンドがなんでも実行可能です。
      await execute(denops, `lua require("which-key").setup()`);
    },
  });

  // `dst` でベースパスとは別の場所にプラグインを clone することも可能です。
  // 開発時などに便利。
  await dvpm.add({
    url: "yukimemi/dps-randomcolorscheme",
    dst: "~/src/github.com/yukimemi/dps-randomcolorscheme",
    before: async ({ denops }) => {
      // `denops_std` の `mapping` 用関数を利用するとキーマッピング設定ができます。
      await mapping.map(denops, "<space>ro", "<cmd>ChangeColorscheme<cr>", { mode: "n" });
      await mapping.map(denops, "<space>rd", "<cmd>DisableThisColorscheme<cr>", { mode: "n" });
      await mapping.map(denops, "<space>rl", "<cmd>LikeThisColorscheme<cr>", { mode: "n" });
      await mapping.map(denops, "<space>rh", "<cmd>HateThisColorscheme<cr>", { mode: "n" });
    },
  });
  //  プラグインの 有効 / 無効は `enabled` で切り替えることができます。
  await dvpm.add({
    url: "yukimemi/dps-hitori",
    enabled: false,
  });
  // `enabled` には関数指定もできるので 下記のように Vim だけ有効などの指定が可能です。
  await dvpm.add({
    url: "editorconfig/editorconfig-vim",
    enabled: async ({ denops }) => !(await fn.has(denops, "nvim")),
  });
  // `dependencies` を指定することで、プラグインが runtimepath へ追加される順序を制御することができます。
  await dvpm.add({
    url: "kana/vim-textobj-entire",
    dependencies: [{ url: "kana/vim-textobj-user" }],
  });

  // 最後に dvpm.end を呼べば完了です。
  await dvpm.end();

  console.log("Load completed !");
}

コメント部分に実施している内容を簡単に記載しているので、大まかな内容は理解できるかと思います。 上記のコードを保存して Vim / Neovim を再起動すると、 dvpm.add で指定したプラグインがインストールされるはずです。

指定できるオプションの詳細はリポジトリの README を参照ください。

仕組みについて

実は上記で行っていることは denops.vim を利用したプラグインを一つ作成した、ということになります。

Deno で Vim/Neovim のプラグインを書く (denops.vim)

の記事にあるように、 denops.vim は自動的に runtimepath 内の denops/*/main.ts を読み込み、 * の部分をプラグイン名として登録します。 そして、 main 関数が実行されます。 このコードでは denops/config/main.ts というファイルを作成しているので、 config という名前の denops.vim プラグインを作成していることになります。(これであなたも立派な denops.vim プラグイン作成者です!) ※設定として記載するために config という名前を使用していますが、自分の好きな名前に変更することも可能です。(config という名前のプラグインはさすがにないと思うので衝突の心配はないはずです・・・)

この denops.vim の仕組みに乗って作成された config プラグインの中から呼び出して利用しているのが yukimemi/dvpm: dvpm - Denops Vim/Neovim Plugin Manager になります。

そのため、前述したように dvpm 自体は denops.vim のプラグインではなく、ユーザーが作成する denops.vim プラグインである config から呼び出される Deno ライブラリということになります。

特徴

この設定方法の特徴は、なんといっても Vim / Neovim の起動速度です!! 私自身、現在 202 個のプラグインを使用していますが、 dstein64/vim-startuptime を使用して起動時間を計測した結果は以下の通りです。

neovim-startuptime

13.6 !!!めちゃめちゃ速い!!!

・・・はい・・・ただし、実はこの結果はプラグインがまったく読み込まれていない時間になります・・・。 プラグインをどれだけ入れているかは起動時間には関係しません。 実際には、 Vim / Neovim 起動時には denops.vim のみが読み込まれ、 denops.vim が先程自作した config プラグインを読み込み、さらにそれから dvpm がプラグインを runtimepath に追加していく処理がバックグラウンドで実行されます。 つまり、起動直後はプラグインを利用することはできませんが、どれだけプラグインを導入しても Vim / Neovim の起動時間は劣化しません。

自分の場合は、 Windows で Vim / Neovim の起動時間が気になっていたため、プラグインが使用できなくてもとりあえず最速で起動してくれるこの設定方法はけっこう気に入っています。

合うと思われる人

  • Windows で Vim / Neovim の起動時間が気になる人
  • エクスプローラーなどからファイルを直接開いて、即座に内容を表示したい人
  • ターミナルから直接ファイル指定して Vim / Neovim を起動する人
  • 起動直後はプラグインなしでも素の Vim / Neovim で問題なく操作できる人
  • TypeScript が大好きな人

合わないと思われる人

  • Vim / Neovim を起動して、 FF (ddu や Telescope) などからファイルを開く人
  • 必要なプラグインは起動直後から有効になっていることを望む人

Deno は非常に優れており、十分な速度を持っているため、起動直後はプラグインは有効ではありませんが、比較的すぐに使用することができます。そのため現状私は困っていません。 ただし、 denops.vim で設定を記載する、という方法は一般的ではないため、設定方法につまずくことが多いかもしれません。

denops_std の各モジュールドキュメントには、さまざまな設定方法が記載されているので読んでみてください。

また、私の設定も参考にはなるかと思います。 通常の Deno と同様に、設定を分割して別のファイルから import することもできます。

dotfiles/.config/nvim at main · yukimemi/dotfiles · GitHub

ddcddu のように、 JSON のような引数で設定を行うプラグインは非常に書きやすいです。(実は最近 TypeScript で設定ができるようになったらしいのでそっちのほうが良いと思います・・・。)

例えば、私の現時点の設定は以下になります。

108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
      await denops.call("ddc#custom#patch_global", {
        ui: "pum",
        autoCompleteEvents: [
          "InsertEnter",
          "TextChangedI",
          "TextChangedP",
          "CmdlineEnter",
          "CmdlineChanged",
          "TextChangedT",
        ],
        sources: ["nvim-lsp", "around", "vsnip", "file", "rg"],
        sourceOptions: {
          _: {
            ignoreCase: true,
            matchers: ["matcher_fuzzy"],
            sorters: ["sorter_fuzzy"],
            converters: ["converter_fuzzy"],
          },
          around: { mark: "around" },
          buffer: { mark: "buffer" },
          line: { mark: "line" },
          file: { mark: "file" },
          rg: { mark: "rg" },
          "nvim-lsp": {
            mark: "lsp",
            forceCompletionPattern: ".w*|:w*|->w*",
          },
        },
        sourceParams: {
          buffer: {
            requireSameFiletype: false,
            limitBytes: 50000,
            fromAltBuf: true,
            forceCollect: true,
          },
          file: {
            filenameChars: "[:keyword:].",
          },
        },
      });

一方で、 Lua でコールバックを含む場合はうまく書くことができないため、 execute を使って直接書いてしまうこともあります。 最悪の場合、このように execute を使って書くこともできますので、設定できないことはないはずです。

117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
      await execute(
        denops,
        `
        lua << EOB
          local actions = require("telescope.actions")
          local trouble = require("trouble.providers.telescope")

          local telescope = require("telescope")
          local borderless = false

          telescope.setup({
            defaults = {
              layout_strategy = "horizontal",
              mappings = {
                i = {
                  ["<c-t>"] = trouble.open_with_trouble,
                  ["<C-Down>"] = require("telescope.actions").cycle_history_next,
                  ["<C-Up>"] = require("telescope.actions").cycle_history_prev,
                },
              },
              prompt_prefix = " ",
              selection_caret = " ",
              winblend = borderless and 0 or 10,
            },
            extensions = {
              project = {
                base_dirs = {
                  { "~/src", max_depth = 4 },
                },
              },
            },
          })

          telescope.load_extension("file_browser")
          telescope.load_extension("project")
          -- telescope.load_extension("projects")
          telescope.load_extension("vim_bookmarks")
        EOB
        `,
      );

アップデート方法

プラグインのアップデート

インストールしたプラグインは :DvpmUpdate コマンドでアップデートできます。 現時点で唯一提供しているコマンドです。 アップデートも denops.vim 上で非同期に実行されるため、 Vim / Neovim の操作を妨げずにスムーズに行うことができます。

dvpm や denops_std のアップデート

dvpmdenops_std のアップデートには、 udd を使うと便利です。

config ディレクトリに移動して、次のコマンドを実行します。

  • Mac / Linux の場合 (zsh)
bash
1
udd **/*.ts
  • Windows の場合 (PowerShell)
powershell
1
udd $(gci -r -file *.ts)

これにより、各 import 文の @x.x.x バージョンが更新されます。

おまけ

Deno を設定として利用することで、 API の呼び出しが非常に簡単に書けます。また、 Denonpm の 豊富なライブラリも使用可能です。

例えば、先程の denops/config/main.ts に以下を追記してみます。

typescript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// import へ以下を追記
import * as buffer from "https://deno.land/x/denops_std@v5.0.0/buffer/mod.ts";

// ... 省略

export async function main(denops: Denops): Promise<void> {
  // ... 省略

  // main 関数の最後に以下を追記
  const area = "130000";
  const res = await fetch(
    `https://www.jma.go.jp/bosai/forecast/data/overview_forecast/${area}.json`,
  );
  const msg = [
    "☆東京の天気情報☆",
    "------------------------------",
    "",
    ...(await res.json()).text.split("\n"),
  ];
  const buf = await buffer.open(denops, "forecast");
  await fn.setbufvar(denops, buf.bufnr, "&buftype", "nofile");
  await fn.setbufvar(denops, buf.bufnr, "&swapfile", 0);
  await buffer.replace(denops, buf.bufnr, msg);
  await buffer.concrete(denops, buf.bufnr);
}

これだけの記載で Vim / Neovim 起動時にお天気情報を確認することができます!

neovim-startuptime

nvim-notify を使用して以下の記載に変更すると、通知として確認することも可能です。これで雨の日も安心。

typescript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
export async function main(denops: Denops): Promise<void> {
  // ... 省略
  // プラグインへ以下を追加。
  await dvpm.add({
    url: "rcarriga/nvim-notify",
    after: async ({ denops }) => {
      await denops.cmd(`lua require("notify").setup()`);
    },
  });

  // main 関数の最後に以下を追記
  const area = "130000";
  const res = await fetch(
    `https://www.jma.go.jp/bosai/forecast/data/overview_forecast/${area}.json`,
  );
  const msg = [
    "☆東京の天気情報☆",
    "------------------------------",
    "",
    ...(await res.json()).text.split("\n"),
  ];
  await denops.call(
    `luaeval`,
    `
      require("notify")(_A.msg, vim.log.levels.INFO, {
        timeout = _A.timeout,
        on_open = function(win)
          vim.wo[win].wrap = true
        end,
      })
    `,
    { msg, timeout: 30000 },
  );
}

weather-notify

おわりに

denops.vim は非常に書きやすくて楽しいので、 私みたいに設定ファイルを全て・・・というのはさすがにやり過ぎですが、先程のお天気通知みたいなプラグインであれば本当にすぐ作れちゃうので、ぜひとも何か作ってみてください。