denops.vim(ディーノップス)は、TypeScriptやJavaScriptで書いたプラグインをDeno上で動かし、VimとNeovimの両方から使えるようにするエコシステムです。vim-jpのメンバーが中心に開発しており、補完のddc.vim、UIのddu.vim、日本語入力のskkeletonなどがこの上で動いています。この記事では2026年11月時点の最新版v8.0.2を前提に、動作要件、導入と動作確認、プラグイン開発の最小構成、起動を速くする共有サーバー、Vimとの往復を減らす書き方を扱います。コード例と計測値は、Vim 9.1.1752とDeno 2.9.7で実際に動かして得たものです。
まとめ|denops.vim v8の要件と始め方
- 最新版はv8.0.2(2026年3月22日公開)です。Vim 9.1.1646以上かNeovim 0.11.3以上、Deno 2.3.0以上が必要です。Vim/Neovimの版が足りないと起動時に警告を出して読み込みを止めます。Denoの版は起動時に検査されず、
:checkhealth denopsでエラーとして出ます。 - 利用者の準備は、Denoをインストールして
denoをPATHに通し、denops.vimを普通のプラグインとして追加するだけです。動作確認にはdenops-helloworld.vimの:DenopsHelloを使います。 - プラグインは
denops/<名前>/main.tsでmainを公開し、denops.dispatcherに関数を登録して、Vim scriptからdenops#request()で呼びます。 - 起動のたびにDenoのプロセスを立ち上げるので、手元の計測では使えるようになるまで約1.4〜1.6秒かかりました。共有サーバーにすると約0.8〜1.0秒です。
- Vimの関数を何百回も呼ぶ処理は、
awaitを1回ずつ並べると遅くなります。1,000行のgetlineは1回ずつ待つと約0.6秒、collectかaccumulateでまとめると約15〜35ミリ秒でした。 - 標準モジュールは
jsr:@denops/std(最新8.2.0)から読み込みます。deno.land/x/denops_stdはv6.5.1で更新が止まっており、accumulateのような新しいAPIはJSR版にしかありません。
denops.vimの仕組み|Denoサーバーとchannelで動く構成
denops.vim本体・@denops/std・各プラグインの役割分担
denops.vimという名前は3つの層をまとめて指すことが多いので、最初に分けておきます。1つめはVim/Neovimに入れる本体vim-denops/denops.vimで、Denoのプロセスを起動して通信路を張ります。2つめはプラグイン作者が読み込む標準モジュール@denops/stdで、Vimの関数・変数・オプション・autocmdをTypeScriptから型付きで扱うラッパーです。3つめがddc.vimやskkeletonのような個々のdenopsプラグインです。
利用者が入れるのは本体と使いたいプラグインだけです。@denops/stdは各プラグインがJSRから読み込みます。
Denoサーバーとの通信とプラグイン読み込みの流れ
Vimが起動すると、VimEnterのタイミングでdenops.vimがDenoのプロセス(denopsサーバー)を立ち上げ、Vimのchannel(Neovimではmsgpack-rpc)で接続します。接続が確立するとDenopsReadyというautocmdが発火し、runtimepath上のdenops/*/main.tsが探索されて、各プラグインのmain関数が呼ばれます。
プラグインのコードはエディタとは別プロセスで動きます。ただし、Vimからdenops#request()で呼ぶと結果を同期的に待つため、その間は編集操作も待たされます。結果を待たずに済む呼び出しはdenops#notify()かdenops#request_async()にします。逆向きのTypeScript側からVimを呼ぶAPIは非同期で、denops.call()やdenops.cmd()はPromiseを返します。往復の回数がそのまま待ち時間になります(後述の実測を参照)。
Denoプロセスには既定で全権限が付きます。ヘルプ(doc/denops.txt)にあるg:denops#server#deno_argsの既定値は['-q', '--no-lock', '-A']です。Denoのパーミッションモデルでファイルやネットワークへのアクセスを絞る仕組みは、denopsプラグインでは効いていません。導入するプラグインは、ほかのVimプラグインと同じく作者とコードを見て選ぶ必要があります。
v8の動作要件|Vim 9.1.1646・Neovim 0.11.3・Deno 2.3.0
v8.0.2の要件は、リポジトリ内のdenops/supported_versions.jsonと起動スクリプトplugin/denops.vimに書かれています。
| 対象 | 必要な版 | 満たさないとき |
|---|---|---|
| Vim | 9.1.1646以上 | 警告して読み込み中止 |
| Neovim | 0.11.3以上 | 警告して読み込み中止 |
| Deno | 2.3.0以上 | 起動は続行(checkhealthでエラー) |
| deno実行ファイル | PATH上かg:denops#deno |
警告して読み込み中止 |
Vim/Neovimの版が足りないと、起動時に[denops] Denops requires Vim 9.1.1646 or Neovim 0.11.3.と表示されます。g:denops_disable_version_checkを1にすればこのチェックを飛ばせますが、動作は保証されません。Ubuntu 24.04の標準パッケージのVimは9.1.0016なので、この要件を満たしません。ディストリビューションごとのVimの版とパッチの確認方法はVim 9.2の新機能と最新バージョン確認・更新手順にまとめています。
要件はメジャー版を上げるときに見直されます。READMEのサポート方針では、VimはHomebrewとvim-win32-installerが配布している最新版より古い版までを対象に下限を決め、NeovimとDenoは直近2つのマイナー版までを対象にします。2026年11月13日時点でv8.0.2より新しいリリースは出ておらず、mainのsupported_versions.jsonも同じ値のままです。
見落としやすいのは、個々のプラグインが書いている要件と実際の下限が一致しないことです。ddc.vimとdpp.vimのREADMEは今も「Vim 9.1.0448+ or Neovim 0.10.0+」のままです。ddu.vimはREADMEから要件を外し、doc/ddu.txtのCOMPATIBILITYを見るよう案内しています(2025.08.26の項に「Vim 9.1.1646+ or Neovim (0.11.0+)」)。どれもdenops.vim v8.0以上を求めているので、実際の下限はdenops側のVim 9.1.1646とNeovim 0.11.3です。プラグインの要件とdenops.vim本体の要件のうち、高いほうが実際の下限になります。
導入手順|Denoのインストールから:DenopsHelloでの動作確認まで
Denoの配置とg:denops#denoの指定
DenoはDeno公式のインストール手順で入れ、Vimの中からdenoが見えているかを確かめます。
:echo exepath('deno')
/usr/local/bin/deno
空文字が返る場合は、シェルのPATHとVimが引き継いだPATHがずれています。GUI版のVimやランチャーから起動したNeovimでよく起こります。絶対パスを指定すれば確実です。
let g:denops#deno = '/usr/local/bin/deno'
プラグインマネージャーでの追加と動作確認
denops.vim本体は普通のVimプラグインとして追加します。vim-plugなら次の2行です。2行目は動作確認用のdenops-helloworld.vimで、確認が済んだら外して構いません。
Plug 'vim-denops/denops.vim'
Plug 'vim-denops/denops-helloworld.vim'
Vimを起動し直して:DenopsHelloを実行し、Hello, Denops!が表示されれば、Denoの起動からプラグインの読み込みまで通っています。Neovimのプラグイン管理の選び方はNeovim(nvim)の使い方、Neovim 0.12で入ったvim.packで管理する場合はNeovim 0.12系への移行手順を参照してください。
サーバーの状態は:echo denops#server#status()で確認できます。返る値はstopped、starting、preparing、running、closing、closedの6つで、正常ならrunningです。Neovimでは:checkhealth denopsで、対応版とDenoの版をまとめて点検できます。
Denoのバージョンとキャッシュ保存先の固定
システムのDenoをmiseやasdfで切り替えたり更新したりすると、denopsプラグインの依存モジュールの解決がやり直しになり、次の起動で数秒待たされます。denopsだけ版を固定したいなら、g:denops#denoに固定した版のDenoを、g:denops#deno_dirにそのDeno専用のキャッシュ置き場を指定します。どちらもdenops.vimが読み込まれる前に設定します。
let g:denops#deno = expand('~/.local/deno-2.9.7/bin/deno')
let g:denops#deno_dir = expand('~/.cache/denops-deno')
g:denops#deno_dirは、v8.0.2のautoload/denops/_internal/server/proc.vimでDenoの環境変数DENO_DIRとして渡されます。未指定ならシェルのDENO_DIRかDenoの既定のキャッシュを使います。固定するのは版の下限(2.3.0)を満たすDenoにしてください。
ヘルプ推奨の設定:Ctrl-Cでの中断とDenopsRestart
:h denops-recommendedには、vimrcに追加する設定例が載っています。:DenopsRestartや:DenopsFixCacheは本体が定義するコマンドではなく、この例を自分でvimrcに書いて初めて使えるコマンドです。
" 実行中のプラグイン処理を <C-c> で中断する
noremap <silent> <C-c> <Cmd>call denops#interrupt()<CR><C-c>
inoremap <silent> <C-c> <Cmd>call denops#interrupt()<CR><C-c>
cnoremap <silent> <C-c> <Cmd>call denops#interrupt()<CR><C-c>
" サーバーの再起動と、Denoモジュールのキャッシュ更新
command! DenopsRestart call denops#server#restart()
command! DenopsFixCache call denops#cache#update(#{reload: v:true})
プラグインを更新した直後に依存モジュールの解決で落ちる場合は、:DenopsFixCacheでキャッシュを取り直してから:DenopsRestartを実行すると直ることが多いです。
起動の遅さへの対処|共有サーバーの設定と実測値
denopsプラグインは、Denoプロセスの起動を待ってから読み込まれます。Intel Mac(macOS同梱のVim 9.1.1752、Deno 2.9.7、依存モジュールはキャッシュ済み)で、vimrcの読み込み開始からサンプルプラグインを呼べるようになるまでを3回計測しました。
| 構成 | DenopsReadyまで | プラグイン呼び出し可まで |
|---|---|---|
| Vimごとにサーバー起動(既定) | 約1.27〜1.42秒 | 約1.44〜1.57秒 |
| 共有サーバーへ接続 | 約0.66〜0.89秒 | 約0.80〜1.03秒 |
共有サーバーは、常駐させた1つのDenoプロセスに複数のVim/Neovimから接続する方式です。vimrcに接続先を書き、denops-shared-server.vimでOSのサービスとして登録します。
let g:denops_server_addr = '127.0.0.1:32123'
" Plug 'vim-denops/denops-shared-server.vim' を追加したうえで一度だけ実行
" :call denops_shared_server#install()
手動で起動するなら、denops.vimのディレクトリでdeno run -A --no-lock denops/@denops-private/cli.ts --hostname=127.0.0.1 --port=32123を実行します。
共有サーバーは接続してきたVimごとにDenoのWorkerを1つ立てる作りで(cli.tsのprocessWorker)、プラグインの処理はVimごとに分かれています。短縮できるのはDenoプロセス自体の起動分です。サーバーはvimrcとは別に常駐するので、Denoやdenops.vimを更新したときはサービス側も再起動してください。Windowsで共有サーバーにしても遅い場合は、ウイルス対策ソフトがDenoのキャッシュ(%LOCALAPPDATA%\deno)を起動のたびにスキャンしていないかを確認します。
プラグイン開発の最小構成|main.tsとdispatcher
ディレクトリ構成とEntrypointの書き方
ここでは、カレントバッファの単語数を数えるwordcountプラグインを例にします。denops.vimはruntimepath上のdenops/<プラグイン名>/main.tsを探すので、ディレクトリ名がそのままプラグイン名になります。例を試すには、vimrcにset runtimepath^=~/wordcountのように作成したディレクトリを追加し、Vimを再起動してください。
wordcount/
├── denops/
│ └── wordcount/
│ ├── deno.jsonc
│ └── main.ts
└── plugin/
└── wordcount.vim
main.tsではEntrypoint型のmainを公開し、denops.dispatcherにVimから呼べる関数を登録します。
import type { Entrypoint } from "@denops/std";
import * as fn from "@denops/std/function";
import * as vars from "@denops/std/variable";
import { assert, is } from "@core/unknownutil";
export const main: Entrypoint = (denops) => {
denops.dispatcher = {
async count(bufnr: unknown): Promise<number> {
assert(bufnr, is.Number);
const lines = await fn.getbufline(denops, bufnr, 1, "$");
const words = lines.join(" ").split(/\s+/).filter((w) => w !== "");
await vars.b.set(denops, "wordcount_last", words.length);
return words.length;
},
host(): string {
return `${denops.meta.host} ${denops.meta.version} (${denops.meta.mode})`;
},
};
};
Vimから渡る引数は型が保証されないので、unknownで受けてassertで検査してから使います。mainはVimの起動のたびに呼ばれるため、重い初期化はここに書かず、必要になったときに呼ぶ関数に分けてください。
VimとNeovimで処理を分けたいときは、has('nvim')を呼ばずにdenops.meta.host("vim"か"nvim")で分岐します。fn.has()はVimへの往復が1回発生しますが、denops.metaは接続時に受け取った値なので往復が要りません。
Vim scriptからの呼び出し:denops#requestとdenops#notify
plugin/wordcount.vimでは、dispatcherの関数を呼ぶコマンドを定義します。
if exists('g:loaded_wordcount')
finish
endif
let g:loaded_wordcount = 1
command! WordCount echo denops#request('wordcount', 'count', [bufnr('%')])
「denops runs plugins」「written in TypeScript」の2行を入れたバッファで:WordCountを実行すると6が表示され、b:wordcount_lastにも6が入りました。呼び出し方は3種類あり、戻り値を待つならdenops#request()、待たなくてよいならdenops#notify()、戻り値をコールバックで受けるならdenops#request_async()を使います。
Vimとdenopsの間で受け渡せるのは、JSONやmsgpackに直せる値(数値・文字列・リスト・辞書など)だけです。Funcrefや、NeovimのLuaの関数・テーブル参照はそのまま渡せません。コールバックが必要な場面では、TypeScript側の関数を@denops/std/lambdaのlambda.add()で登録し、Vim側からはその呼び出し式を使います。逆に、Vim script側のラムダやFuncrefをDeno側に渡したいときは、denops#callback#register()で登録して得たIDを渡し、Deno側からdenops#callback#callで呼びます。
import mapによる依存バージョンの一元管理
v8では、プラグインのディレクトリに置いたdeno.json、deno.jsonc、import_map.json、import_map.jsoncを、この順に探してimport mapとして読みます。前節のmain.tsが"@denops/std"のような短い名前で読み込めるのは、次のdeno.jsoncを置いているからです。
{
"imports": {
"@denops/std": "jsr:@denops/std@^8.2.0",
"@core/unknownutil": "jsr:@core/unknownutil@^4.3.0"
}
}
このファイルを置かずに短い名前で書くと、モジュールを解決できずに読み込みが失敗します。import mapを使わない場合は、"jsr:@denops/[email protected]/function"のように版付きの完全な指定子を各ファイルに書きます。import mapなら依存の版を上げるときの書き換えが1か所で済みます。
開発中の確認:deno checkとg:denops#debug
型の誤りは、Vimを起動する前にdenops/wordcountディレクトリでdeno check main.tsを実行すれば見つけられます。前節のコードはDeno 2.9.7でエラーなく通りました。プラグインの直下から確認したい場合は、直下に{"workspace": ["./denops/wordcount"]}と書いたdeno.jsonを置きます。置かないとimport mapが読まれず、TS2307で失敗します。
vimrcでlet g:denops#debug = 1を指定すると、denops本体のデバッグメッセージが出るようになり、プラグインから見えるdenops.meta.modeがreleaseからdebugに変わります。実行するとhost()の戻り値はvim 9.1.1752 (debug)になりました。この変数はdenopsの初期化前に設定する必要があり、性能に影響する機能も有効になるので普段は0にしておきます。
Vimとの往復を減らす書き方|batch・collect・accumulateの実測
denopsプラグインの遅さは、起動よりもVimとの往復の回数で決まることが多いです。@denops/std/batchには、複数の呼び出しを1回の往復にまとめる関数が3つあります。accumulateは@denops/std 8.2.0(2025年11月15日公開)で追加されたもので、deno.land/x版にはありません。
| 関数 | 戻り値 | ブロック内で結果を使った分岐 |
|---|---|---|
batch |
受け取らない | 不可(呼び出しは偽値を返す) |
collect |
呼び出しごとの結果の配列 | 不可(入れ子も不可) |
accumulate |
ブロックの戻り値 | 可(同じタイミングの呼び出しをまとめる) |
batchとcollectは、ブロックの中で呼んだ関数をため込み、最後に1回で送ります。ブロックの中でawait denops.call(...)しても結果は返らないので、「has('nvim')の結果で処理を変える」ようなコードは書けません。accumulateのドキュメントによれば、accumulateは普通の非同期関数として書いたまま、同じマイクロタスクの間に出た呼び出しを1回の往復にまとめます。副作用のある呼び出しを混ぜる場合は、実行順に注意が必要です。
1,000行のバッファでfn.getline()を1行ずつ呼ぶ処理を、4通りに書いて計測しました。各方式を5回ずつ測った中央値で、Vimを3回起動し直した範囲を示します(Vim 9.1.1752・Deno 2.9.7・@denops/std 8.2.0・Intel Mac)。4方式とも返った1,000行は同じでした。
| 書き方 | 100回 | 1,000回 |
|---|---|---|
| for文で1回ずつawait | 51〜84ms | 595〜624ms |
| Promise.allで同時に投げる | 5.8〜9.2ms | 73〜85ms |
| collect | 1.3〜4.0ms | 15〜30ms |
| accumulate | 1.7〜3.2ms | 19〜35ms |
import * as fn from "@denops/std/function";
import { accumulate, collect } from "@denops/std/batch";
const idx = Array.from({ length: 1000 }, (_, i) => i + 1);
// collect: 返り値を配列で受ける。ブロック内では結果を見られない
const a = await collect(denops, (d) => idx.map((i) => fn.getline(d, i)));
// accumulate: 普通に await できる。同じタイミングの呼び出しが1回にまとまる
const b = await accumulate(denops, (d) =>
Promise.all(idx.map((i) => fn.getline(d, i)))
);
for文で1回ずつ待つ書き方は、collectやaccumulateでまとめた場合より17〜40倍ほど遅くなりました。Promise.allで同時に投げるだけでも7〜8倍速くなりますが、往復は1,000回のままです。読み取りを並べるだけならcollect、取得した値で次の呼び出しを決める処理ならaccumulateを使います。設定を続けて書き換えるだけならbatchで十分です。数回しか呼ばない処理は、まとめなくても体感に差は出ません。手を入れるのは、行や候補の数だけ呼び出しが増えるループです。ただし、この例のように連続した行を読むだけなら、fn.getline(denops, 1, "$")の1回で全行を取れます。まとめる関数を使う前に、範囲を一度に受け取れる関数がないかを先に確かめてください。
古い記事のコードの扱い|deno.land/x版の更新停止とJSRへの移行
denops.vimの入門記事は2021〜2023年のものが多く、次のような読み込み方が残っています。
import { Denops } from "https://deno.land/x/[email protected]/mod.ts";
標準モジュールの配布先は、denops.vim v7(2024年7月27日公開)の時期にJSRへ移りました。JSRの@denops/stdは2024年7月3日の7.0.0-pre0が最初で、2026年11月時点の最新は8.2.0です。deno.land/x/denops_stdの最新はv6.5.1で止まっています。
denops_std v5.0.2とv1.0.0で書いた最小プラグインは、v8.0.2・Deno 2.9.7・Vim 9.1.1752でもそのまま読み込まれて動きました。それでもJSRへ移すのは、更新と新しいAPI(前節のaccumulateなど)がJSR版だけに入るためです。プラグインを外すときの後片付け(EntrypointからAsyncDisposableを返し、[Symbol.asyncDispose]に処理を書く形)も、JSR版の@denops/core 8系にしかありません。移すときは読み込み先をjsr:@denops/stdに変え、export const main: Entrypointの形に揃えます。
Vim script・Lua・coc.nvimとの比較と採用判断
| 方式 | 記述言語 | 動くエディタ | 追加ランタイム |
|---|---|---|---|
| Vim script | Vim script / Vim9 script | Vim・Neovim(Vim9 scriptはVimのみ) | 不要 |
| Lua | Lua | Neovim | 不要 |
| coc.nvim拡張 | JavaScript/TypeScript | Vim・Neovim | Node.js |
| denops.vim | TypeScript/JavaScript | Vim・Neovim | Deno |
coc.nvimはVS Code風の拡張を読み込んで言語サーバーをホストするNode.jsの拡張ホストで、LSPによる補完と診断が中心です。denops.vimのREADMEも、coc.nvimから強く影響を受けたと書いています。ただしdenops.vim自体は機能を持たない土台で、補完もUIもその上のプラグインが担います。補完環境を選ぶ際は、LSP補完一式をすぐ使いたいならcoc.nvim、ddc.vimやddu.vimを自分で組み合わせて設定したいならdenops.vim、と用途で分かれます。
denops.vimを選ぶ理由がはっきりするのは、VimとNeovimの両方で同じプラグインを動かしたい場合と、npmやJSRのライブラリ(パーサーや辞書処理など)を使いたい場合です。
逆に、Neovimだけで使う小さなプラグインや、起動直後に必ず動いてほしい処理には向きません。Luaなら追加ランタイム不要で起動待ちもありませんが、denopsでは上の実測のとおり最初の呼び出しまで既定で約1.4〜1.6秒、共有サーバーでも約0.8秒待ちます。Denoを入れられない社内端末や、配布先にDenoの導入を求められない場合も、denops製プラグインは選べません。
代表的なdenops製プラグインと要件
| プラグイン | 用途 | 公式の要件 |
|---|---|---|
| Shougo/ddc.vim | 補完フレームワーク | Deno 2.3.0+・denops v8.0+ |
| Shougo/ddu.vim | UIフレームワーク | Deno 2.3.0+・denops v8.0+ |
| Shougo/dpp.vim | プラグインマネージャー | Deno 2.3.0+・denops v8.0+ |
| lambdalisue/vim-gin | Git操作 | Deno・denops.vim |
| vim-skk/skkeleton | SKK日本語入力 | Deno 1.42.0+ |
ddc.vimとddu.vimは本体だけでは何もせず、候補の取得元(source)や表示(ui)、絞り込み(filter)を別プラグインとして組み合わせて使います。プラグインが何本あってもDenoプロセスは1つなので、プロセス起動の待ちは1回分で済みます。
skkeletonのREADMEはDeno 1.42.0+としか書いていませんが、v8と組み合わせるなら実際の下限は上の要件表のとおりです。
よくあるトラブルと対処|Channel is not ready yetとDeno未検出
起動直後にプラグインを呼ぶと失敗することがあります。VimEnterの直後にdenops#request()を呼ぶと、[denops] Channel is not ready yetというエラーになりました。このときサーバーの状態はstartingです。起動時に処理したい場合は、autocmd User DenopsPluginPost:wordcountでプラグインの読み込み完了を待つか、denops#plugin#wait('wordcount')で待ってから呼びます(同期で待つので、画面を止めたくない場合はdenops#plugin#wait_async()を使います)。
g:denops#denoが存在しないパスを指していると、[denops] A '...' (g:denops#deno) is not executable. Denops requires executable Deno.と表示されてdenops全体が読み込まれません。前述のexepath('deno')で、Vimから見えるパスを確認してください。
プラグインを更新した後に、依存モジュールが見つからないエラーが出ることもあります。この場合は:DenopsFixCacheでキャッシュを取り直します。版の要件を満たしているかは、Neovimなら:checkhealth denops、Vimなら:versionのIncluded patchesで確かめます。
denops.vimに関するよくある質問
denopsの読み方は何ですか?
READMEの発音記号は/ˈdiːnoʊps/で、「ディーノップス」に近い読み方です。日本語の記事では「デノップス」と書かれることもあります。
denops.vimを使うのにDenoは必須ですか?
必須です。v8.0.2ではDeno 2.3.0以上が必要で、denoを実行できないとdenops.vimは警告を出して読み込みを止めます。
Vim scriptが書けなくてもプラグインを作れますか?
処理の中身はTypeScriptで書けます。Vim scriptファイルは必須ではありません。ただし、TypeScriptからコマンドなどを定義する場合も、denops.cmd()に渡すExコマンドの知識は必要です。
Neovimだけで使うならLuaで書くほうがよいですか?
Neovim専用で起動直後から動いてほしいならLuaが向いています。VimとNeovimの両方で動かしたい場合や、npm・JSRのライブラリを使いたい場合はdenops.vimを選びます。
denops製プラグインを入れるとVimの起動は遅くなりますか?
Vimの画面自体は待たされませんが、プラグインが使えるようになるまでDenoの起動分の待ち時間があります。手元の計測では約1.4〜1.6秒で、共有サーバーを使うと約0.8〜1.0秒でした。