Introduction
Denops
(/ˈdiːnoʊps/, pronounced
as dee-nops
) is an ecosystem designed for developing plugins for Vim and
Neovim using Deno (a TypeScript / JavaScript runtime).
Denops and Denops plugins (Vim plugins powered by Denops) offer the following features:
- Installable as a Vim plugin:
Denops follows the standard Vim plugin architecture. Users can install Denops itself and Denops plugins using a Vim plugin manager, just like any other Vim plugins. - Unified codebase for Vim and Neovim:
Denops provides a unified API for both Vim and Neovim. You can write a plugin that functions on both Vim and Neovim with a single codebase. - No worries about dependency management:
Deno includes a built-in dependency management system, allowing developers to write plugins with third-party libraries without concerns about dependency management. - Simple and efficient code:
Deno utilizes the V8 engine, significantly faster than Vim script. You can write a plugin with straightforward code, without the need for complex optimizations solely for performance. - Risk-free execution:
Denops plugins run in a separate process from Vim / Neovim. Even if a plugin freezes, Vim / Neovim remains unaffected.
Check out vim-denops GitHub Topics to discover Vim plugins using Denops.
Denops is primarily developed and maintained by the vim-denops organization (separated from the vim-jp organization). For questions, you can use GitHub Discussions (English), or visit the #tech-denops channel on Slack workspace for vim-jp (Japanese).
Installation
Prerequisites
Denops requires Deno to be installed on your system. Please refer to Deno's official manual for installation instructions.
After installing Deno, ensure that the deno
command is accessible from your
Vim.
:echo execpath("deno")
It should display the path to the deno
command. If it prints an empty string,
add the path to the deno
command to your PATH
environment variable.
tip
If you prefer not to modify the PATH
environment variable, you can set the
executable path to the g:denops#deno
variable in your vimrc
file like
this:
let g:denops#deno = "/path/to/deno"
Installation
Denops itself is a Vim plugin and can be installed using a Vim plugin manager, similar to other Vim plugins.
vim-plug
Plug 'vim-denops/denops.vim'
Jetpack.vim
Jetpack 'vim-denops/denops.vim'
dein.vim
call dein#add('vim-denops/denops.vim')
minpac
call minpac#add('vim-denops/denops.vim')
lazy.nvim
require("lazy").setup({
"vim-denops/denops.vim",
-- ...
})
Health Check
Denops provides a health checker to confirm that Denops is installed correctly.
You can check the health of Denops by running the :checkhealth
command
(Neovim) or :CheckHealth
(Vim with vim-healthcheck).
==============================================================================
denops: health#denops#check
- Supported Deno version: `1.45.0`
- Detected Deno version: `1.45.4`
- OK Deno version check: passed
- Supported Neovim version: `0.10.0`
- Detected Neovim version: `0.10.0`
- OK Neovim version check: passed
- Denops status: `running`
- OK Denops status check: passed
Getting Started
Denops (/ˈdiːnoʊps/,
pronounced dee-nops
) is an ecosystem for Vim / Neovim that empowers
developers to write plugins in TypeScript / JavaScript powered by Deno.
Let's start by creating a simple plugin to learn how to develop Denops plugins.
Create a Plugin
Create a directory named denops-getting-started
in your home directory and a
file named main.ts
within it, under denops/denops-getting-started/
:
$HOME
└── denops-getting-started
└── denops
└── denops-getting-started
└── main.ts
Next, write the following TypeScript code in main.ts
:
import type { Entrypoint } from "jsr:@denops/std@^7.0.0";
export const main: Entrypoint = (denops) => {
denops.dispatcher = {
async hello() {
await denops.cmd(`echo "Hello, Denops!"`);
},
};
};
Activate the Plugin
Add the following line to your Vim or Neovim configuration file (e.g.,
~/.vimrc
or ~/.config/nvim/init.vim
):
set runtimepath+=~/denops-getting-started
Or Neovim Lua configuration file (e.g., ~/.config/nvim/init.lua
):
vim.opt.runtimepath:append("~/denops-getting-started")
Try the Plugin
Restart Vim/Neovim and execute the following command:
:call denops#request('denops-getting-started', 'hello', [])
You should see "Hello, Denops!" displayed on the screen like:
Explanation of the Getting Started
In this section, we'll provide detailed information about the Getting Started. If you find it too detailed, feel free to skip this section and move on to the next chapter, especially if your goal is to start developing a Denops plugin promptly.
What is Denops?
Denops claims to be an ecosystem for developing Vim / Neovim (hereafter, when we refer to Vim without restriction, we also include Neovim) plugins using Deno, but, in reality, it is a Vim plugin with the following features:
- Detection and registration of Denops plugins
- Launching and connecting to Deno processes
- Calling Deno process-side functions from Vim via RPC (Remote Procedure Call)
- Calling Vim features from Deno process-side via RPC
By utilizing this plugin, you can control Vim from code written in TypeScript (Denops plugins).
note
RPC (Remote Procedure Call) is used, and while Vim uses a JSON-based custom specification, Neovim uses MessagePack-RPC (a slightly modified specification). However, Denops abstracts away these differences, so Denops plugin developers don't need to be aware of the RPC specification differences between Vim and Neovim.
What is a Vim Plugin?
When Vim starts, it searches for files named plugin/*.vim
in directories
specified in runtimepath
. Additionally, if a function like foo#bar#hoge()
is
called, it searches for files named autoload/foo/bar.vim
in the runtimepath
and reads the file, calling the foo#bar#hoge()
function defined within.
A Vim plugin is a set of predefined features provided to users, utilizing the
functionality mentioned above. Typically, an entry point is defined in
plugin/{plugin_name}.vim
, and detailed features are implemented in
autoload/{plugin_name}.vim
or autoload/{plugin_name}/*.vim
. For example,
here is the directory structure for a Vim plugin named hello
:
vim-hello
├── autoload
│ └── hello.vim # Defines the function `hello#hello()`
└── plugin
└── hello.vim # Defines the `Hello` command
note
For more detailed information on creating Vim plugins, refer to
:help write-plugin
.
What is a Denops Plugin?
When Denops is installed, in addition to Vim plugins, files named
denops/*/main.ts
are also searched when Vim starts. If a corresponding file is
found, Denops registers the parent directory name (foo
in the case of
denops/foo/main.ts
) as the plugin name. Then, it imports the corresponding
file as a TypeScript module and calls the function named main
.
A Denops plugin, similar to a Vim plugin, provides a set of features written in
TypeScript to users. Since Denops plugins typically include both TypeScript and
Vim script code, the directory structure looks like an extension of the Vim
plugin structure with an added denops
directory. For example, here is the
directory structure for a Denops plugin named hello
:
denops-hello
├── autoload
│ └── hello.vim # Tasks better written in Vim script (may not exist)
├── denops
│ └── hello
│ └── main.ts # Entry point for the Denops plugin (mandatory)
└── plugin
└── hello.vim # Entry point written in Vim script (optional)
In the Getting Started, we created a file named
denops/denops-getting-started/main.ts
and added its parent directory
(denops-getting-started
) to runtimepath
. There were no autoload
or
plugin
directories because we didn't provide an entry point that Vim could
easily call.
Understanding the Code in Getting Started
In the Getting Started, we wrote the following code in the main.ts
file:
import type { Entrypoint } from "jsr:@denops/std@^7.0.0";
export const main: Entrypoint = (denops) => {
denops.dispatcher = {
async hello() {
await denops.cmd(`echo "Hello, Denops!"`);
},
};
};
Let's break down this code step by step.
About Imports
import type { Entrypoint } from "jsr:@denops/std@^7.0.0";
The first line imports the Entrypoint
type from the @denops/std standard
library. You can find detailed information about the library by checking the
URL: https://jsr.io/@denops/std@7.0.0
(replace jsr:
to https://jsr.io/
).
We fixed the version in the import URL, so it's recommended to check for details
and update to the latest version URL.
Note that we use import type
syntax, which is part of TypeScript's
Type-Only Imports and Export.
This syntax can be written as import { type Entrypoint }
with the same
meaning. Using import { Entrypoint }
for a type-only import is also valid.
note
Denops plugins are dynamically imported, so there might be differences in
Denops versions between development and usage. Therefore, to minimize
differences between Denops versions, only type information is exposed. The
implementation can be found in
denops/@denops-private/denops.ts
,
but it is not publicly exposed for the reasons mentioned above.
This type information is provided by @denops/core, and @denops/std simply re-exports the type information from @denops/core. However, @denops/core is intended to be referenced only by denops.vim and @denops/std, so Denops plugin developers don't need to use it directly.
About Entry Point
export const main: Entrypoint = (denops) => {
// Omitted...
};
The above code exports the main
function. The main
function is called by
Denops, and it takes the
Denops instance (denops
) as an
argument. Denops plugins use this denops
to add user-defined APIs or call
Vim's features.
About User-Defined APIs
denops.dispatcher = {
async hello() {
// Omitted...
},
};
The code above adds a user-defined API named hello
to denops.dispatcher
.
denops.dispatcher
is defined as follows, and each method takes unknown
types
for both arguments and return values:
interface Dispatcher {
[key: string]: (...args: unknown[]) => unknown;
}
By defining methods in denops.dispatcher
, you can freely define APIs. Since
the methods registered in denops.dispatcher
are always called with await
,
you can make them asynchronous by returning a Promise
.
The methods defined in denops.dispatcher
can be called from Vim using the
following functions:
Function | Description |
---|---|
denops#request | Synchronously calls a user-defined API and returns the result. |
denops#request_async | Asynchronously calls a user-defined API and passes the result to callbacks. |
denops#notify | Calls a user-defined API without waiting for completion and discards the result. |
At the end of the Getting Started, we used
denops#request('denops-getting-started', 'hello', [])
to call the user-defined
API named hello
in denops-getting-started
plugin.
About Calling Vim's features
await denops.cmd(`echo "Hello, Denops!"`);
With the received denops
, you can call Vim functions, execute Vim commands, or
evaluate Vim expressions. In the example above, the hello
API internally uses
denops.cmd
to execute the echo
command in Vim. The denops
object provides
several methods:
Method | Description |
---|---|
call | Calls a Vim function and returns the result. |
batch | Calls multiple Vim functions in bulk and returns the results in bulk. |
cmd | Executes a Vim command. If ctx is provided, it is expanded as local variables. |
eval | Evaluate a Vim expression and returns the result. If ctx is provided, it is expanded as local variables. |
dispatch | Calls a user-defined API of another Denops plugin and returns the result. |
Although denops
provides low-level interfaces, @denops/std combines these
low-level interfaces to offer higher-level interfaces. Therefore, it's
recommended to use @denops/std to call Vim's features in actual plugin
development.
For example, use
function
module to call
Vim's function instead of denops.call
like:
import * as fn from "jsr:@denops/std@^7.0.0/function";
// Bad (result1 is `unknown`)
const result1 = await denops.call("expand", "%");
// Good (result2 is `string`)
const result2 = await fn.expand(denops, "%");
If developers use function
module instead, they can benefit from features like
auto-completion and type checking provided by LSP (Language Server Protocol).
Next Steps
In the next step, follow the tutorial to learn how to develop a minimum Denops plugin.
Tutorial: Hello World
In this chapter, we will create a minimal Denops plugin that greets the user. Most of the code resembles the previous Getting Started, so feel free to skip this chapter if you are already familiar with it.
note
The plugin we will create in this section can be found at https://github.com/vim-denops/denops-helloworld.vim
Creating a Minimal Vim Plugin
Let's start by crafting a minimal Vim plugin called denops-helloworld
.
Create a directory named denops-helloworld
in your home directory and a Vim
script file as follows:
denops-helloworld
└── plugin
└── denops-helloworld.vim
The content of the plugin/denops-helloworld.vim
file is as follows:
if exists('g:loaded_denops_helloworld')
finish
endif
let g:loaded_denops_helloworld = 1
command! DenopsHello echo 'Hello, Denops!'
The initial four lines (as shown below) serve as a guard, preventing the plugin from being loaded more than once. Using this guard is a common practice to ensure the plugin is loaded only once.
if exists('g:loaded_denops_helloworld')
finish
endif
let g:loaded_denops_helloworld = 1
The final line (as displayed below) defines a command named DenopsHello
that
invokes the echo 'Hello, Denops!'
command. Thus, when you execute
:DenopsHello
in Vim, it will display "Hello, Denops!".
command! DenopsHello echo 'Hello, Denops!'
Upon startup, Vim searches and loads files named plugin/*.vim
in directories
specified in runtimepath
. To activate the plugin, add the following line to
your Vim configuration file (e.g., ~/.vimrc
or ~/.config/nvim/init.vim
):
set runtimepath+=~/denops-helloworld
For Neovim's Lua configuration file (e.g., ~/.config/nvim/init.lua
), use:
vim.opt.runtimepath:append("~/denops-helloworld")
Restart Vim and execute :DenopsHello
to witness the message "Hello, Denops!".
Creating a Minimal Denops Plugin
When denops.vim is installed, it searches for files named denops/*/main.ts
in addition to Vim plugins when Vim starts.
If a corresponding file is found, Denops registers the parent directory name
(foo
in the case of denops/foo/main.ts
) as the plugin name. It then imports
the corresponding file as a TypeScript module and calls the function named
main
.
note
Denops plugins typically include both TypeScript and Vim script code, so the
directory structure looks like an extension of the Vim plugin structure with
an added denops
directory.
Let's add denops/denops-helloworld/main.ts
to the denops-helloworld
directory that we created in the previous section. The directory tree will be as
follows:
denops-helloworld
├── denops
│ └── denops-helloworld
│ └── main.ts
└── plugin
└── denops-helloworld.vim
Here is the content of the denops/denops-helloworld/main.ts
file:
import type { Entrypoint } from "jsr:@denops/std@^7.0.0";
export const main: Entrypoint = (denops) => {
console.log("Hello, Denops from TypeScript!");
};
warning
As shown above, developers can use console.log
(or console.warn
,
console.error
, etc.) for debug output. The content will be echoed to Vim.
However, it is not recommended to use console.log
in production code.
Instead, use denops.cmd("echo '...'")
or the echo
function in the helper
module of the @denops/std
library.
Once you've created the file, restart Vim, and "Hello, Denops from TypeScript!" will be displayed on Vim startup.
Adding Denops API to the Plugin
In the previous section, we created a minimal Denops plugin. In this section, we will enhance the plugin by adding an API.
Open denops/denops-helloworld/main.ts
and rewrite the content with the
following code:
import type { Entrypoint } from "jsr:@denops/std@^7.0.0";
import { assert, is } from "jsr:@core/unknownutil@^4.3.0";
export const main: Entrypoint = (denops) => {
denops.dispatcher = {
hello(name) {
assert(name, is.String);
return `Hello, ${name || "Denops"}!`;
},
};
};
The above code adds a new API hello
to the plugin. The hello
API takes a
string name
and returns a greeting message. See
About User-Defined APIs in Explanation of the Getting started
for details about User-Defined APIs.
note
While Vim script does not facilitate types, Denops uses unknown
types on the
interface between Vim and Denops. That's why we use
unknownutil to ensure that the name
is
of type string
in the above code.
Once you've updated the file, restart Vim, and execute the following command, you will see the message "Hello, Your name!".
:echo denops#request("denops-helloworld", "hello", ["Your name"])
As shown, users can call the Denops API via the denops#request()
function.
Calling Vim Features from the Plugin
If you want to use a Vim feature from your Denops plugin, you can call it via
the denops
instance passed to the plugin's main
function. You can rewrite
main.ts
as follows to register the DenopsHello
as a Vim command:
import type { Entrypoint } from "jsr:@denops/std@^7.0.0";
import { assert, is } from "jsr:@core/unknownutil@^4.3.0";
export const main: Entrypoint = (denops) => {
denops.dispatcher = {
async init() {
// This is just an example.
// Developers usually should define commands directly in Vim script.
await denops.cmd(
`command! -nargs=? DenopsHello echomsg denops#request('denops-helloworld', 'hello', [<q-args>])`,
);
},
hello(name) {
assert(name, is.String);
return `Hello, ${name || "Denops"}!`;
},
};
};
Then, rewrite plugin/denops-helloworld.vim
to automatically call the init
API on plugin load via the DenopsPluginPost:{plugin_name}
autocmd:
if exists('g:loaded_denops_helloworld')
finish
endif
let g:loaded_denops_helloworld = 1
augroup denops_helloworld
autocmd!
autocmd User DenopsPluginPost:denops-helloworld
\ call denops#notify('denops-helloworld', 'init', [])
augroup END
Once Vim is restarted, the DenopsHello
command will be registered.
Then you can run:
:DenopsHello Your name
If the plugin has been registered successfully, you will see Hello, Your name!
as a result.
Next Steps
In the next step, follow the tutorial to learn how to develop a real Denops plugin.
Tutorial (Maze)
Now that you have grasped the basics of developing Denops plugins in the previous chapters, it's time to delve into creating a more functional plugin.
So, out of the blue, have you ever felt the urge to solve mazes while programming? Personally, I haven't, but there might be enthusiasts who enjoy it immensely. In any case, let's embark on developing a Denops plugin that can generate and display a maze in Vim at any time. Well, don't ask me why we would want to do that.
note
The plugin we will create in this chapter can be found at https://github.com/vim-denops/denops-maze.vim
Utilizing Third-Party Library
Certainly, starting with coding a maze generation algorithm would be nice.
However, since you're now using Deno, you can conveniently employ a third-party
library called maze_generator.
Let's define a Maze
command similar to DenopsHello
; Maze
generates a maze
and outputs it.
note
The maze_generator
library is a third-party library that generates a maze.
It is not a part of Deno or Denops. You can use any third-party library that
is compatible with Deno in your Denops plugin. Thanks to Deno, developers and
users don't need to worry about the installation of third-party libraries.
Deno automatically downloads and caches the library when it is imported.
Create the denops-maze
plugin and place it under ~/denops-maze
. The
directory tree will look like this:
~/denops-maze
├── denops
│ └── denops-maze
│ └── main.ts
└── plugin
└── denops-maze.vim
The content of the denops/denops-maze/main.ts
file will be:
import type { Entrypoint } from "jsr:@denops/std@^7.0.0";
import { Maze } from "npm:@thewizardbear/maze_generator@^0.4.0";
export const main: Entrypoint = (denops) => {
denops.dispatcher = {
maze() {
const maze = new Maze({}).generate();
const content = maze.getString();
console.log(content);
},
};
};
The content of the plugin/denops-maze.vim
file will be:
if exists('g:loaded_denops_maze')
finish
endif
let g:loaded_denops_maze = 1
" Function called once the plugin is loaded
function! s:init() abort
command! Maze call denops#request('denops-maze', 'maze', [])
endfunction
augroup denops_maze
autocmd!
autocmd User DenopsPluginPost:denops-maze call s:init()
augroup END
tip
The Maze
command is defined once the plugin is loaded with the above code.
If you wish to define the command immediately after Vim startup, you can
define the command and use denops#plugin#wait()
or
denops#plugin#wait_async()
in the function to wait for plugin load, like
this:
if exists('g:loaded_denops_maze')
finish
endif
let g:loaded_denops_maze = 1
function! s:maze() abort
if denops#plugin#wait('denops-maze')
" Something went wrong
return
endif
call denops#request('denops-maze', 'maze', [])
endfunction
command! Maze call s:maze()
Don't forget to activate the plugin by adding the following line to your
vimrc
:
set runtimepath+=~/denops-maze
Then, restart Vim and execute :Maze
to see the generated maze. Note that it
may take a few seconds for the first startup because Deno will download the
dependencies, but it happens only once.
Outputting Content to a Buffer
In the previous section, we echoed the maze to the echo area. However, echoing the maze to the echo area is not very practical. In this section, we will output the maze to a buffer so that users can yank the maze with daily Vim operations!
Let's modify the code to make the generated maze output to a buffer.
import type { Entrypoint } from "jsr:@denops/std@^7.0.0";
import { Maze } from "npm:@thewizardbear/maze_generator@^0.4.0";
export const main: Entrypoint = (denops) => {
denops.dispatcher = {
async maze() {
const maze = new Maze({}).generate();
const content = maze.getString();
await denops.cmd("enew");
await denops.call("setline", 1, content.split(/\r?\n/g));
},
};
};
In this code, denops.cmd
executes the Vim command enew
to open a new buffer
in the current window. Then, denops.call
calls the Vim function setline()
to
write the maze to the buffer.
Restart Vim, rerun the Maze
command, and then you can see:
Adjusting Maze Size to Fit the Window
In the previous section, we outputted the maze to a buffer. However, the maze size can sometimes be too large or too small for the window. It would be better to have a maze that fits the current window size.
Let's modify the plugin to ensure the generated maze fits the current window size.
import type { Entrypoint } from "jsr:@denops/std@^7.0.0";
import * as fn from "jsr:@denops/std@^7.0.0/function";
import { Maze } from "npm:@thewizardbear/maze_generator@^0.4.0";
export const main: Entrypoint = (denops) => {
denops.dispatcher = {
async maze() {
await denops.cmd("enew");
const winWidth = await fn.winwidth(denops, 0);
const winHeight = await fn.winheight(denops, 0);
const maze = new Maze({
xSize: winWidth / 3,
ySize: winHeight / 3,
}).generate();
const content = maze.getString();
await fn.setline(denops, 1, content.split(/\r?\n/g));
},
};
};
In this code, we utilize the function
module (aliased to fn
) of
@denops/std
(Denops Standard Library) to call winwidth()
, winheight()
, and
setline()
functions. Then, we create a maze that fits the current window size
and write it to the buffer.
So why do we use the function
module instead of denops.call
? With
denops.call
, developers must know the function name, arguments, return type,
and manually cast the return value to the expected type (like as string
).
However, with the function
module, developers can use auto-completion, type
checking, etc. It is more convenient and safe to use the function
module.
tip
The function
module of the @denops/std
library provides a set of functions
that are available on both Vim and Neovim. If you'd like to use Vim or Neovim
only functions, use the vim
or nvim
module under the function
module
instead.
See the function module of @denops/std API document for more details.
Restart Vim, rerun the :Maze
command, and then you can see:
Properly Create a Virtual Buffer
Now that the maze is displayed in a buffer, but it is not properly configured.
For example, if a user executes the :edit
command on the buffer, the maze will
disappear. This is because Vim does not know how to reload the buffer content,
and we must inform Vim about the content of the buffer when it is reloaded.
In this section, we will use the buffer
module of @denops/std
to create a
proper virtual buffer that concretizes the buffer content. Let's modify the
main.ts
file as follows:
import type { Entrypoint } from "jsr:@denops/std@^7.0.0";
import * as buffer from "jsr:@denops/std@^7.0.0/buffer";
import * as fn from "jsr:@denops/std@^7.0.0/function";
import { Maze } from "npm:@thewizardbear/maze_generator@^0.4.0";
export const main: Entrypoint = (denops) => {
denops.dispatcher = {
async maze() {
const { bufnr, winnr } = await buffer.open(denops, "maze://");
const winWidth = await fn.winwidth(denops, winnr);
const winHeight = await fn.winheight(denops, winnr);
const maze = new Maze({
xSize: winWidth / 3,
ySize: winHeight / 3,
}).generate();
const content = maze.getString();
await buffer.replace(denops, bufnr, content.split(/\r?\n/g));
await buffer.concrete(denops, bufnr);
},
};
};
In this code, we use buffer.open
to open a maze://
buffer and get the buffer
number (bufnr
) and the window number (winnr
). Because Denops works
asynchronously, the current buffer or window may be changed from what we
expected. That's why developers should use buffer.open
to open a buffer and
save the buffer number and the window number for further operations.
Then, we call fn.winwidth
and fn.winheight
with the obtained window number
to get the window size. Again, the current window might be changed, so we should
use winnr
to specify the window.
note
Vim may execute some events between RPC calls, so the current buffer or window really may be changed from what we expected. Denops plugin developers should be careful about this. The best practice for avoiding this problem is to avoid using current and always specify the buffer number or window number.
After that, we use buffer.replace
to replace the content of the buffer.
Actually, replacing the buffer content is a bit tricky. Developers should care
about modifiable
options to avoid unmodifiable errors, foldmethod
options to
keep foldings, and should remove the buffer content that is not replaced by
setline
or setbufline
, etc. The buffer.replace
function will care about
all of those, so developers should avoid using setline
or setbufline
directly.
At the end, we call buffer.concrete
to concretize the buffer content. This
function defines BufReadCmd
autocmd to restore the content when the buffer is
reloaded. Without this, the buffer content will be discarded when the user
executes the :edit
command.
Restart Vim, rerun the :Maze
command, and then you can see:
Try the :edit
command on the buffer, and you can see the maze is still there.
Properly Configure the Buffer
In the previous section, we didn't configure the buffer options, so the buffer
remains modifiable and persists after being closed. In this section, we will
configure the buffer options to make the buffer non-modifiable and remove the
buffer after closure. Open the main.ts
file and modify the maze
method as
follows:
import type { Entrypoint } from "jsr:@denops/std@^7.0.0";
import * as buffer from "jsr:@denops/std@^7.0.0/buffer";
import * as fn from "jsr:@denops/std@^7.0.0/function";
import * as op from "jsr:@denops/std@^7.0.0/option";
import { Maze } from "npm:@thewizardbear/maze_generator@^0.4.0";
export const main: Entrypoint = (denops) => {
denops.dispatcher = {
async maze() {
const { bufnr, winnr } = await buffer.open(denops, "maze://");
const winWidth = await fn.winwidth(denops, winnr);
const winHeight = await fn.winheight(denops, winnr);
const maze = new Maze({
xSize: winWidth / 3,
ySize: winHeight / 3,
}).generate();
const content = maze.getString();
await buffer.replace(denops, bufnr, content.split(/\r?\n/g));
await buffer.concrete(denops, bufnr);
await op.bufhidden.setLocal(denops, "wipe");
await op.modifiable.setLocal(denops, false);
},
};
};
In this code, we use op.bufhidden.setLocal
to set the bufhidden
option to
wipe
so that the buffer is wiped out when it is closed. Additionally, we use
op.modifiable.setLocal
to set the modifiable
option to false
to make the
buffer non-modifiable. Note that since we use buffer.replace
to replace the
content of the buffer, there is no need to explicitly set the modifiable
option to true
before replacing the content.
Restart Vim, rerun the :Maze
command, and confirm that the buffer is not
modifiable.
Reducing the Number of RPC Calls
As Denops employs RPC to interact with Vim, the volume of RPC calls
significantly influences the plugin's performance. In this section, we aim to
enhance performance by reducing the number of RPC calls using the batch
module
from @denops/std
. Let's revise the main.ts
file as follows:
import type { Entrypoint } from "jsr:@denops/std@^7.0.0";
import { batch, collect } from "jsr:@denops/std@^7.0.0/batch";
import * as buffer from "jsr:@denops/std@^7.0.0/buffer";
import * as fn from "jsr:@denops/std@^7.0.0/function";
import * as op from "jsr:@denops/std@^7.0.0/option";
import { Maze } from "npm:@thewizardbear/maze_generator@^0.4.0";
export const main: Entrypoint = (denops) => {
denops.dispatcher = {
async maze() {
const { bufnr, winnr } = await buffer.open(denops, "maze://");
const [winWidth, winHeight] = await collect(denops, (denops) => [
fn.winwidth(denops, winnr),
fn.winheight(denops, winnr),
]);
const maze = new Maze({
xSize: winWidth / 3,
ySize: winHeight / 3,
}).generate();
const content = maze.getString();
await batch(denops, async (denops) => {
await buffer.replace(denops, bufnr, content.split(/\r?\n/g));
await buffer.concrete(denops, bufnr);
await op.bufhidden.setLocal(denops, "wipe");
await op.modifiable.setLocal(denops, false);
});
},
};
};
In this code, we use the collect
function to gather window size values and the
batch
function to execute multiple commands in a single RPC. This optimization
significantly reduces the number of RPC calls, thereby improving the plugin's
performance.
The collect
function is designed for collecting multiple values in a single
RPC, offering the following features:
- Execution of
denops.call
ordenops.eval
within thecollect
is delayed and executed in a single RPC with the results. - The result of
denops.call
ordenops.eval
in thecollect
is always falsy, indicating that branching (if, switch, etc.) is not allowed. - Execution of
denops.redraw
ordenops.cmd
in thecollect
is not allowed. - Execution of
batch
orcollect
in thecollect
is not allowed, indicating that nesting is not allowed.
In short, only the following operations are allowed in the collect
:
denops.call
ordenops.eval
that returns a value.- Functions in the
function
module that return a value. - Functions in the
option
module that return a value. - Functions in the
variable
module that return a value.
The batch
function is designed for executing multiple commands in a single
RPC, offering the following features:
- Execution of
denops.call
,denops.cmd
, ordenops.eval
in thebatch
is delayed and executed in a single RPC without the results. - The result of
denops.call
ordenops.eval
in thebatch
is always falsy, indicating that branching (if, switch, etc.) is not allowed. - Execution of
denops.redraw
is accumulated and only executed once at the end of thebatch
. - Execution of
batch
in thebatch
is allowed, indicating that nesting is allowed. - Execution of
collect
in thebatch
is not allowed, indicating that nesting is not allowed.
In short, only the following operations are allowed in the batch
:
denops.call
,denops.cmd
, ordenops.eval
(without the results).- Functions in the
function
module (without the results). - Functions in the
option
module (without the results). - Functions in the
variable
module (without the results). - Functions in other modules that do not call
collect
internally.
In the previous code, the number of RPC calls was more than 7, but after using
batch
and collect
, the number of RPC calls is reduced to 3. Although this is
a small plugin, the performance improvement may not be noticeable. However, in a
larger plugin, the performance improvement will be significant.
Restart Vim, rerun the :Maze
command, and confirm that the plugin works
properly with batch
and collect
.
Next Steps
In the next step, read API references or real-world plugins
- API reference
- lambdalisue/gin.vim
- vim-skk/skkeleton
- Shougo/ddu.vim
- Find one from the
vim-denops
topic
API Reference
There is a standard module @denops/std to develop denops plugins. It provides various functions to interact with Vim and Neovim and some shorthands to make it easier to write plugins.
You can find API references about the module by checking the Deno doc page:
https://jsr.io/@denops/std
.
FAQ
How to Check Denops Startup Time
To check the startup time of Denops or Denops plugins, utilize denops-startup-recorder. This plugin visualizes the timing of events related to Denops and Denops plugin startup.
It shows the result in echo area like:
How to Check Denops Performance
To assess Denops performance, employ denops-benchmark. This plugin measures the number of operations or characters that can be processed in milliseconds.
It shows the result in a buffer like: