Repository files navigation

Bunvim

Bunvim is a Bun client that allows you to interact with Neovim through RPC using TypeScript and JavaScript. If you're familiar with Neovim's Lua API, you'll find this client easy to use.

This client includes TypeScript definitions, generated from Neovim's api-metadata, describing API function signatures, including function parameters and return values. The Neovim version the bundled definitions were generated against is recorded in the file's header comment. If you're running a different Neovim version, you can regenerate them against your local Neovim with the command bunx bunvim types.

All functionality is implemented in one file. If you're looking for higher levels of abstraction, take a look at neovim/node-client and neoclide/neovim. Good luck.

✅ Requirements

📦 Installation

bun install bunvim

💻 Usage

For an example of a plugin using Bunvim, take a look at github-preview.nvim.

You should keep a tab open with Neovim API docs when working with Bunvim. Although this client includes generated TypeScript types, you'll find the detailed descriptions in the official docs very helpful if not necessary.

Create a script:

// my-plugin.tsimport{attach}from"bunvim";// RPC listening addressconstSOCKET="/tmp/bunvim.nvim.socket";constnvim=awaitattach({socket: SOCKET,client: {name: "my-plugin-name"},});// append "hello world" to current bufferawaitnvim.call("nvim_buf_set_lines",[0,-1,-1,true,["hello world"]]);// disable relative numbersawaitnvim.call("nvim_set_option_value",["relativenumber",false,{}]);// create a vertical splitawaitnvim.call("nvim_command",["vs"]);// print cursor position on cursor moveawaitnvim.call("nvim_create_autocmd",[["CursorHold","CursorHoldI"],{desc: "Print Cursor Position",command: `lua local cursor_pos = vim.api.nvim_win_get_cursor(0) vim.print(cursor_pos)`,},]);nvim.detach();

Initialize Neovim with the RPC listening address specified above:

nvim --listen /tmp/bunvim.nvim.socket

Execute your script from another terminal:

bun run my-plugin.ts

If your plugin is executed as a child process of Neovim:

-- somewhere in your neovim lua config fileslocalfunctionrun_script()
-- neovim sets the environment variable NVIM in all its child processes-- NVIM = the RPC listening address assigned to the current neovim instance-- neovim sets an RPC listening address if you don't manually specify one-- https://neovim.io/doc/user/builtin.html#jobstart-envvim.fn.jobstart("bun run my-plugin.ts", {
cwd=vim.fn.expand("~/path/to/plugin/"),
})
endvim.api.nvim_create_user_command("RunMyScript", run_script, {})

You could then open Neovim without manually specifying an RPC listening address, just nvim and then run the command :RunMyScript. Your Bun process would then have access to the NVIM environment variable.

// my-plugin.tsimport{attach}from"bunvim";constSOCKET=process.env["NVIM"];if(!SOCKET)throwError("socket missing");constnvim=awaitattach({socket: SOCKET,client: {name: "my-plugin-name"},});

📖 API Reference

This module exports only one method attach and a bunch of TypeScript types. attach returns an Nvim object that can be used to interact with Neovim.

nvim.call(function: string, args: unknown[])

Used to call any of these functions. They're all typed. You should get function names autocompletion & warnings from TypeScript if the parameters don't match the expected types. Some function calls return a value, others don't.

constbufferContent=awaitnvim.call("nvim_buf_get_lines",[0,0,-1,true]);

nvim.channelId

RPC Channel ID.

constchannelId=nvim.channelId;awaitnvim.call("nvim_create_autocmd",[["CursorMove"],{desc: "Notify my-plugin",command: `lua vim.rpcnotify(${channelId}, "my-notification")`,},]);

nvim.onNotification(notification: string, callback: function)

Registers a handler for a specific RPC Notification.

Notifications must be typed before you declare a handler for them, or TypeScript will complain.

import{attach,typeBaseEvents,typeEventsMap}from"bunvim";// an interface to define your notifications and their argsinterfaceMyEventsextendsBaseEvents{requests: EventsMap;// default typenotifications: {// declare custom notification: "cursor_move",// that would be called with args: [row: number, col: number]"cursor_move": [row: number,col: number];};}// attach to neovimconstnvim=awaitattach<MyEvents>({ ... })letcount=0;// register a handler for the notification "cursor_move"nvim.onNotification("cursor_move",async([row,col])=>{// "row" and "col" are of type "number" as specified above// CAUTION:// it's up to you to make sure the handler receives the correct args,// bunvim doesn't do any validations// print row and col in neovimawaitnvim.call("nvim_exec_lua",[`print("row: ${row} - col: ${col}")`,[]]);// return `true` to remove handlerreturncount++>=5;});// multiple handlers can be registered for the same notificationnvim.onNotification("cursor_move",async([row,col])=>{// replace contents in current buffer lines 1 and 2awaitnvim.call("nvim_buf_set_lines",[0,0,2,true,[`row: ${row}`,`col: ${col}`]]);});constchannelId=nvim.channelId;// create autocommand to notify our plugin via `vim.rpcnotify`// whenever the cursor movesawaitnvim.call("nvim_create_autocmd",[["CursorHold","CursorHoldI"],{desc: "Notify on Cursor Move",command: `lua local cursor_pos = vim.api.nvim_win_get_cursor(0) local row = cursor_pos[1] local col = cursor_pos[2] vim.rpcnotify(${channelId}, "cursor_move", row, col)`,},]);

nvim.onRequest(request: string, callback: function)

Registers a handler for a specific RPC Request.

Requests must be typed before you declare a handler for them, or TypeScript will complain.

The difference between an RPC Notification and an RPC Request, is that requests block neovim until a response is returned. Notifications are non-blocking.

import{attach,typeBaseEvents,typeEventsMap}from"bunvim";import{gracefulShutdown}from"./utils.ts";// an interface to define your requests and their argsinterfaceMyEventsextendsBaseEvents{notifications: EventsMap;// default typerequests: {// declare custom request: "before_exit",// that would be called with args: [buffer_name: string]"before_exit": [buffer_name: string];};}// attach to neovimconstnvim=awaitattach<MyEvents>({ ... })// register a handler for the request "before_exit"nvim.onRequest("before_exit",async([buffer_name])=>{// "buffer_name" is of type "string" as specified above// CAUTION:// it's up to you to make sure the handler receives the correct args,// bunvim doesn't do any validations// this should actually never get called,// because this handler gets overwritten belowconsole.log("buffer_name: ",buffer_name);// we must return something to unblock neovimreturnnull;});// only one handler per request may be registered.// if you call `nvim.onRequest` for an already registered handler,// the older handler is replaced with the new one.nvim.onRequest("before_exit",async([buffer_name])=>{gracefulShutdown(buffer_name);returnnull;});constchannelId=nvim.channelId;// create autocommand to call our function via `vim.rpcrequest`// whenever neovim is about to closeawaitnvim.call("nvim_create_autocmd",[["VimLeavePre"],{desc: "RPC Request before exit",command: `lua local buffer_name = vim.api.nvim_buf_get_name(0) vim.rpcrequest(${channelId}, "before_exit", buffer_name)`,},]);

nvim.detach()

Closes connection with neovim.

nvim.detach();

nvim.logger

Instance of winston logger. May be undefined if logging was not enabled.

Used to log data to console and/or file. Does not log/print messages to Neovim.

See Logging.

// log functions sorted from highest to lowest priority:nvim.logger?.error("error message");nvim.logger?.warn("warn message");nvim.logger?.info("info message");nvim.logger?.http("http message");nvim.logger?.verbose("verbose message");nvim.logger?.debug("debug message");nvim.logger?.silly("silly message");

🖨️ Logging

To enable logging to Console and/or File, a logging.level must be specified when calling the attach method.

constnvim=awaitattach({socket: SOCKET,client: {name: "my-plugin-name"},logging: {level: "debug",// <= LOG LEVEL},});nvim.logger?.info("hello world");

Bunvim internally logs with logger.debug() and logger.error(). Set logging.level higher than debug to not display bunvim's internal logs when printing logs for your plugin.

Levels from highest to lowest priority:

  1. error
  2. warn
  3. info
  4. http
  5. verbose
  6. debug
  7. silly

Console

After setting a logging.level, you can see your logs live with the command bunvim logs and specifying the client.name you defined in your attach.

In a terminal, run the command:

# this process will listen for logs and print them to the console# Ctrl-C to stop process
bunx bunvim logs my-plugin-name

For more information about the CLI tool, run the command:

bunx bunvim --help

File

You can also write your logs to a file by specifying a path when calling the attach method:

constnvim=awaitattach({socket: SOCKET,client: {name: "my-plugin-name"},logging: {level: "debug",// <= LOG LEVELfile: "/tmp/my-plugin-name.log",// <= PATH TO LOG FILE ("~" is not expanded)},});

Neovim

If you want to log/print a message to the user in Neovim, use:

import{NVIM_LOG_LEVELS}from"bunvim";awaitnvim.call("nvim_notify",["some message",NVIM_LOG_LEVELS.INFO,{}]);

About

Neovim Bun client.

Topics

Resources

Stars

33 stars

Watchers

1 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

Bunvim

Bunvim is a Bun client that allows you to interact with Neovim through RPC using TypeScript and JavaScript. If you're familiar with Neovim's Lua API, you'll find this client easy to use.

This client includes TypeScript definitions, generated from Neovim's api-metadata, describing API function signatures, including function parameters and return values. The Neovim version the bundled definitions were generated against is recorded in the file's header comment. If you're running a different Neovim version, you can regenerate them against your local Neovim with the command bunx bunvim types.

All functionality is implemented in one file. If you're looking for higher levels of abstraction, take a look at neovim/node-client and neoclide/neovim. Good luck.

✅ Requirements

📦 Installation

bun install bunvim

💻 Usage

For an example of a plugin using Bunvim, take a look at github-preview.nvim.

You should keep a tab open with Neovim API docs when working with Bunvim. Although this client includes generated TypeScript types, you'll find the detailed descriptions in the official docs very helpful if not necessary.

Create a script:

// my-plugin.tsimport{attach}from"bunvim";// RPC listening addressconstSOCKET="/tmp/bunvim.nvim.socket";constnvim=awaitattach({socket: SOCKET,client: {name: "my-plugin-name"},});// append "hello world" to current bufferawaitnvim.call("nvim_buf_set_lines",[0,-1,-1,true,["hello world"]]);// disable relative numbersawaitnvim.call("nvim_set_option_value",["relativenumber",false,{}]);// create a vertical splitawaitnvim.call("nvim_command",["vs"]);// print cursor position on cursor moveawaitnvim.call("nvim_create_autocmd",[["CursorHold","CursorHoldI"],{desc: "Print Cursor Position",command: `lua local cursor_pos = vim.api.nvim_win_get_cursor(0) vim.print(cursor_pos)`,},]);nvim.detach();

Initialize Neovim with the RPC listening address specified above:

nvim --listen /tmp/bunvim.nvim.socket

Execute your script from another terminal:

bun run my-plugin.ts

If your plugin is executed as a child process of Neovim:

-- somewhere in your neovim lua config fileslocalfunctionrun_script()
-- neovim sets the environment variable NVIM in all its child processes-- NVIM = the RPC listening address assigned to the current neovim instance-- neovim sets an RPC listening address if you don't manually specify one-- https://neovim.io/doc/user/builtin.html#jobstart-envvim.fn.jobstart("bun run my-plugin.ts", {
cwd=vim.fn.expand("~/path/to/plugin/"),
})
endvim.api.nvim_create_user_command("RunMyScript", run_script, {})

You could then open Neovim without manually specifying an RPC listening address, just nvim and then run the command :RunMyScript. Your Bun process would then have access to the NVIM environment variable.

// my-plugin.tsimport{attach}from"bunvim";constSOCKET=process.env["NVIM"];if(!SOCKET)throwError("socket missing");constnvim=awaitattach({socket: SOCKET,client: {name: "my-plugin-name"},});

📖 API Reference

This module exports only one method attach and a bunch of TypeScript types. attach returns an Nvim object that can be used to interact with Neovim.

nvim.call(function: string, args: unknown[])

Used to call any of these functions. They're all typed. You should get function names autocompletion & warnings from TypeScript if the parameters don't match the expected types. Some function calls return a value, others don't.

constbufferContent=awaitnvim.call("nvim_buf_get_lines",[0,0,-1,true]);

nvim.channelId

RPC Channel ID.

constchannelId=nvim.channelId;awaitnvim.call("nvim_create_autocmd",[["CursorMove"],{desc: "Notify my-plugin",command: `lua vim.rpcnotify(${channelId}, "my-notification")`,},]);

nvim.onNotification(notification: string, callback: function)

Registers a handler for a specific RPC Notification.

Notifications must be typed before you declare a handler for them, or TypeScript will complain.

import{attach,typeBaseEvents,typeEventsMap}from"bunvim";// an interface to define your notifications and their argsinterfaceMyEventsextendsBaseEvents{requests: EventsMap;// default typenotifications: {// declare custom notification: "cursor_move",// that would be called with args: [row: number, col: number]"cursor_move": [row: number,col: number];};}// attach to neovimconstnvim=awaitattach<MyEvents>({ ... })letcount=0;// register a handler for the notification "cursor_move"nvim.onNotification("cursor_move",async([row,col])=>{// "row" and "col" are of type "number" as specified above// CAUTION:// it's up to you to make sure the handler receives the correct args,// bunvim doesn't do any validations// print row and col in neovimawaitnvim.call("nvim_exec_lua",[`print("row: ${row} - col: ${col}")`,[]]);// return `true` to remove handlerreturncount++>=5;});// multiple handlers can be registered for the same notificationnvim.onNotification("cursor_move",async([row,col])=>{// replace contents in current buffer lines 1 and 2awaitnvim.call("nvim_buf_set_lines",[0,0,2,true,[`row: ${row}`,`col: ${col}`]]);});constchannelId=nvim.channelId;// create autocommand to notify our plugin via `vim.rpcnotify`// whenever the cursor movesawaitnvim.call("nvim_create_autocmd",[["CursorHold","CursorHoldI"],{desc: "Notify on Cursor Move",command: `lua local cursor_pos = vim.api.nvim_win_get_cursor(0) local row = cursor_pos[1] local col = cursor_pos[2] vim.rpcnotify(${channelId}, "cursor_move", row, col)`,},]);

nvim.onRequest(request: string, callback: function)

Registers a handler for a specific RPC Request.

Requests must be typed before you declare a handler for them, or TypeScript will complain.

The difference between an RPC Notification and an RPC Request, is that requests block neovim until a response is returned. Notifications are non-blocking.

import{attach,typeBaseEvents,typeEventsMap}from"bunvim";import{gracefulShutdown}from"./utils.ts";// an interface to define your requests and their argsinterfaceMyEventsextendsBaseEvents{notifications: EventsMap;// default typerequests: {// declare custom request: "before_exit",// that would be called with args: [buffer_name: string]"before_exit": [buffer_name: string];};}// attach to neovimconstnvim=awaitattach<MyEvents>({ ... })// register a handler for the request "before_exit"nvim.onRequest("before_exit",async([buffer_name])=>{// "buffer_name" is of type "string" as specified above// CAUTION:// it's up to you to make sure the handler receives the correct args,// bunvim doesn't do any validations// this should actually never get called,// because this handler gets overwritten belowconsole.log("buffer_name: ",buffer_name);// we must return something to unblock neovimreturnnull;});// only one handler per request may be registered.// if you call `nvim.onRequest` for an already registered handler,// the older handler is replaced with the new one.nvim.onRequest("before_exit",async([buffer_name])=>{gracefulShutdown(buffer_name);returnnull;});constchannelId=nvim.channelId;// create autocommand to call our function via `vim.rpcrequest`// whenever neovim is about to closeawaitnvim.call("nvim_create_autocmd",[["VimLeavePre"],{desc: "RPC Request before exit",command: `lua local buffer_name = vim.api.nvim_buf_get_name(0) vim.rpcrequest(${channelId}, "before_exit", buffer_name)`,},]);

nvim.detach()

Closes connection with neovim.

nvim.detach();

nvim.logger

Instance of winston logger. May be undefined if logging was not enabled.

Used to log data to console and/or file. Does not log/print messages to Neovim.

See Logging.

// log functions sorted from highest to lowest priority:nvim.logger?.error("error message");nvim.logger?.warn("warn message");nvim.logger?.info("info message");nvim.logger?.http("http message");nvim.logger?.verbose("verbose message");nvim.logger?.debug("debug message");nvim.logger?.silly("silly message");

🖨️ Logging

To enable logging to Console and/or File, a logging.level must be specified when calling the attach method.

constnvim=awaitattach({socket: SOCKET,client: {name: "my-plugin-name"},logging: {level: "debug",// <= LOG LEVEL},});nvim.logger?.info("hello world");

Bunvim internally logs with logger.debug() and logger.error(). Set logging.level higher than debug to not display bunvim's internal logs when printing logs for your plugin.

Levels from highest to lowest priority:

  1. error
  2. warn
  3. info
  4. http
  5. verbose
  6. debug
  7. silly

Console

After setting a logging.level, you can see your logs live with the command bunvim logs and specifying the client.name you defined in your attach.

In a terminal, run the command:

# this process will listen for logs and print them to the console# Ctrl-C to stop process
bunx bunvim logs my-plugin-name

For more information about the CLI tool, run the command:

bunx bunvim --help

File

You can also write your logs to a file by specifying a path when calling the attach method:

constnvim=awaitattach({socket: SOCKET,client: {name: "my-plugin-name"},logging: {level: "debug",// <= LOG LEVELfile: "/tmp/my-plugin-name.log",// <= PATH TO LOG FILE ("~" is not expanded)},});

Neovim

If you want to log/print a message to the user in Neovim, use:

import{NVIM_LOG_LEVELS}from"bunvim";awaitnvim.call("nvim_notify",["some message",NVIM_LOG_LEVELS.INFO,{}]);

About

Neovim Bun client.

Topics

Resources

Stars

33 stars

Watchers

1 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Bunvim

Bunvim is a Bun client that allows you to interact with Neovim through RPC using TypeScript and JavaScript. If you're familiar with Neovim's Lua API, you'll find this client easy to use.

This client includes TypeScript definitions, generated from Neovim's api-metadata, describing API function signatures, including function parameters and return values. The Neovim version the bundled definitions were generated against is recorded in the file's header comment. If you're running a different Neovim version, you can regenerate them against your local Neovim with the command bunx bunvim types.

All functionality is implemented in one file. If you're looking for higher levels of abstraction, take a look at neovim/node-client and neoclide/neovim. Good luck.

✅ Requirements

📦 Installation

bun install bunvim

💻 Usage

For an example of a plugin using Bunvim, take a look at github-preview.nvim.

You should keep a tab open with Neovim API docs when working with Bunvim. Although this client includes generated TypeScript types, you'll find the detailed descriptions in the official docs very helpful if not necessary.

Create a script:

// my-plugin.tsimport{attach}from"bunvim";// RPC listening addressconstSOCKET="/tmp/bunvim.nvim.socket";constnvim=awaitattach({socket: SOCKET,client: {name: "my-plugin-name"},});// append "hello world" to current bufferawaitnvim.call("nvim_buf_set_lines",[0,-1,-1,true,["hello world"]]);// disable relative numbersawaitnvim.call("nvim_set_option_value",["relativenumber",false,{}]);// create a vertical splitawaitnvim.call("nvim_command",["vs"]);// print cursor position on cursor moveawaitnvim.call("nvim_create_autocmd",[["CursorHold","CursorHoldI"],{desc: "Print Cursor Position",command: `lua local cursor_pos = vim.api.nvim_win_get_cursor(0) vim.print(cursor_pos)`,},]);nvim.detach();

Initialize Neovim with the RPC listening address specified above:

nvim --listen /tmp/bunvim.nvim.socket

Execute your script from another terminal:

bun run my-plugin.ts

If your plugin is executed as a child process of Neovim:

-- somewhere in your neovim lua config fileslocalfunctionrun_script()
-- neovim sets the environment variable NVIM in all its child processes-- NVIM = the RPC listening address assigned to the current neovim instance-- neovim sets an RPC listening address if you don't manually specify one-- https://neovim.io/doc/user/builtin.html#jobstart-envvim.fn.jobstart("bun run my-plugin.ts", {
cwd=vim.fn.expand("~/path/to/plugin/"),
})
endvim.api.nvim_create_user_command("RunMyScript", run_script, {})

You could then open Neovim without manually specifying an RPC listening address, just nvim and then run the command :RunMyScript. Your Bun process would then have access to the NVIM environment variable.

// my-plugin.tsimport{attach}from"bunvim";constSOCKET=process.env["NVIM"];if(!SOCKET)throwError("socket missing");constnvim=awaitattach({socket: SOCKET,client: {name: "my-plugin-name"},});

📖 API Reference

This module exports only one method attach and a bunch of TypeScript types. attach returns an Nvim object that can be used to interact with Neovim.

nvim.call(function: string, args: unknown[])

Used to call any of these functions. They're all typed. You should get function names autocompletion & warnings from TypeScript if the parameters don't match the expected types. Some function calls return a value, others don't.

constbufferContent=awaitnvim.call("nvim_buf_get_lines",[0,0,-1,true]);

nvim.channelId

RPC Channel ID.

constchannelId=nvim.channelId;awaitnvim.call("nvim_create_autocmd",[["CursorMove"],{desc: "Notify my-plugin",command: `lua vim.rpcnotify(${channelId}, "my-notification")`,},]);

nvim.onNotification(notification: string, callback: function)

Registers a handler for a specific RPC Notification.

Notifications must be typed before you declare a handler for them, or TypeScript will complain.

import{attach,typeBaseEvents,typeEventsMap}from"bunvim";// an interface to define your notifications and their argsinterfaceMyEventsextendsBaseEvents{requests: EventsMap;// default typenotifications: {// declare custom notification: "cursor_move",// that would be called with args: [row: number, col: number]"cursor_move": [row: number,col: number];};}// attach to neovimconstnvim=awaitattach<MyEvents>({ ... })letcount=0;// register a handler for the notification "cursor_move"nvim.onNotification("cursor_move",async([row,col])=>{// "row" and "col" are of type "number" as specified above// CAUTION:// it's up to you to make sure the handler receives the correct args,// bunvim doesn't do any validations// print row and col in neovimawaitnvim.call("nvim_exec_lua",[`print("row: ${row} - col: ${col}")`,[]]);// return `true` to remove handlerreturncount++>=5;});// multiple handlers can be registered for the same notificationnvim.onNotification("cursor_move",async([row,col])=>{// replace contents in current buffer lines 1 and 2awaitnvim.call("nvim_buf_set_lines",[0,0,2,true,[`row: ${row}`,`col: ${col}`]]);});constchannelId=nvim.channelId;// create autocommand to notify our plugin via `vim.rpcnotify`// whenever the cursor movesawaitnvim.call("nvim_create_autocmd",[["CursorHold","CursorHoldI"],{desc: "Notify on Cursor Move",command: `lua local cursor_pos = vim.api.nvim_win_get_cursor(0) local row = cursor_pos[1] local col = cursor_pos[2] vim.rpcnotify(${channelId}, "cursor_move", row, col)`,},]);

nvim.onRequest(request: string, callback: function)

Registers a handler for a specific RPC Request.

Requests must be typed before you declare a handler for them, or TypeScript will complain.

The difference between an RPC Notification and an RPC Request, is that requests block neovim until a response is returned. Notifications are non-blocking.

import{attach,typeBaseEvents,typeEventsMap}from"bunvim";import{gracefulShutdown}from"./utils.ts";// an interface to define your requests and their argsinterfaceMyEventsextendsBaseEvents{notifications: EventsMap;// default typerequests: {// declare custom request: "before_exit",// that would be called with args: [buffer_name: string]"before_exit": [buffer_name: string];};}// attach to neovimconstnvim=awaitattach<MyEvents>({ ... })// register a handler for the request "before_exit"nvim.onRequest("before_exit",async([buffer_name])=>{// "buffer_name" is of type "string" as specified above// CAUTION:// it's up to you to make sure the handler receives the correct args,// bunvim doesn't do any validations// this should actually never get called,// because this handler gets overwritten belowconsole.log("buffer_name: ",buffer_name);// we must return something to unblock neovimreturnnull;});// only one handler per request may be registered.// if you call `nvim.onRequest` for an already registered handler,// the older handler is replaced with the new one.nvim.onRequest("before_exit",async([buffer_name])=>{gracefulShutdown(buffer_name);returnnull;});constchannelId=nvim.channelId;// create autocommand to call our function via `vim.rpcrequest`// whenever neovim is about to closeawaitnvim.call("nvim_create_autocmd",[["VimLeavePre"],{desc: "RPC Request before exit",command: `lua local buffer_name = vim.api.nvim_buf_get_name(0) vim.rpcrequest(${channelId}, "before_exit", buffer_name)`,},]);

nvim.detach()

Closes connection with neovim.

nvim.detach();

nvim.logger

Instance of winston logger. May be undefined if logging was not enabled.

Used to log data to console and/or file. Does not log/print messages to Neovim.

See Logging.

// log functions sorted from highest to lowest priority:nvim.logger?.error("error message");nvim.logger?.warn("warn message");nvim.logger?.info("info message");nvim.logger?.http("http message");nvim.logger?.verbose("verbose message");nvim.logger?.debug("debug message");nvim.logger?.silly("silly message");

🖨️ Logging

To enable logging to Console and/or File, a logging.level must be specified when calling the attach method.

constnvim=awaitattach({socket: SOCKET,client: {name: "my-plugin-name"},logging: {level: "debug",// <= LOG LEVEL},});nvim.logger?.info("hello world");

Bunvim internally logs with logger.debug() and logger.error(). Set logging.level higher than debug to not display bunvim's internal logs when printing logs for your plugin.

Levels from highest to lowest priority:

  1. error
  2. warn
  3. info
  4. http
  5. verbose
  6. debug
  7. silly

Console

After setting a logging.level, you can see your logs live with the command bunvim logs and specifying the client.name you defined in your attach.

In a terminal, run the command:

# this process will listen for logs and print them to the console# Ctrl-C to stop process
bunx bunvim logs my-plugin-name

For more information about the CLI tool, run the command:

bunx bunvim --help

File

You can also write your logs to a file by specifying a path when calling the attach method:

constnvim=awaitattach({socket: SOCKET,client: {name: "my-plugin-name"},logging: {level: "debug",// <= LOG LEVELfile: "/tmp/my-plugin-name.log",// <= PATH TO LOG FILE ("~" is not expanded)},});

Neovim

If you want to log/print a message to the user in Neovim, use:

import{NVIM_LOG_LEVELS}from"bunvim";awaitnvim.call("nvim_notify",["some message",NVIM_LOG_LEVELS.INFO,{}]);

About

Neovim Bun client.

Topics

Resources

Stars

33 stars

Watchers

1 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Bunvim

Bunvim is a Bun client that allows you to interact with Neovim through RPC using TypeScript and JavaScript. If you're familiar with Neovim's Lua API, you'll find this client easy to use.

This client includes TypeScript definitions, generated from Neovim's api-metadata, describing API function signatures, including function parameters and return values. The Neovim version the bundled definitions were generated against is recorded in the file's header comment. If you're running a different Neovim version, you can regenerate them against your local Neovim with the command bunx bunvim types.

All functionality is implemented in one file. If you're looking for higher levels of abstraction, take a look at neovim/node-client and neoclide/neovim. Good luck.

✅ Requirements

📦 Installation

bun install bunvim

💻 Usage

For an example of a plugin using Bunvim, take a look at github-preview.nvim.

You should keep a tab open with Neovim API docs when working with Bunvim. Although this client includes generated TypeScript types, you'll find the detailed descriptions in the official docs very helpful if not necessary.

Create a script:

// my-plugin.tsimport{attach}from"bunvim";// RPC listening addressconstSOCKET="/tmp/bunvim.nvim.socket";constnvim=awaitattach({socket: SOCKET,client: {name: "my-plugin-name"},});// append "hello world" to current bufferawaitnvim.call("nvim_buf_set_lines",[0,-1,-1,true,["hello world"]]);// disable relative numbersawaitnvim.call("nvim_set_option_value",["relativenumber",false,{}]);// create a vertical splitawaitnvim.call("nvim_command",["vs"]);// print cursor position on cursor moveawaitnvim.call("nvim_create_autocmd",[["CursorHold","CursorHoldI"],{desc: "Print Cursor Position",command: `lua local cursor_pos = vim.api.nvim_win_get_cursor(0) vim.print(cursor_pos)`,},]);nvim.detach();

Initialize Neovim with the RPC listening address specified above:

nvim --listen /tmp/bunvim.nvim.socket

Execute your script from another terminal:

bun run my-plugin.ts

If your plugin is executed as a child process of Neovim:

-- somewhere in your neovim lua config fileslocalfunctionrun_script()
-- neovim sets the environment variable NVIM in all its child processes-- NVIM = the RPC listening address assigned to the current neovim instance-- neovim sets an RPC listening address if you don't manually specify one-- https://neovim.io/doc/user/builtin.html#jobstart-envvim.fn.jobstart("bun run my-plugin.ts", {
cwd=vim.fn.expand("~/path/to/plugin/"),
})
endvim.api.nvim_create_user_command("RunMyScript", run_script, {})

You could then open Neovim without manually specifying an RPC listening address, just nvim and then run the command :RunMyScript. Your Bun process would then have access to the NVIM environment variable.

// my-plugin.tsimport{attach}from"bunvim";constSOCKET=process.env["NVIM"];if(!SOCKET)throwError("socket missing");constnvim=awaitattach({socket: SOCKET,client: {name: "my-plugin-name"},});

📖 API Reference

This module exports only one method attach and a bunch of TypeScript types. attach returns an Nvim object that can be used to interact with Neovim.

nvim.call(function: string, args: unknown[])

Used to call any of these functions. They're all typed. You should get function names autocompletion & warnings from TypeScript if the parameters don't match the expected types. Some function calls return a value, others don't.

constbufferContent=awaitnvim.call("nvim_buf_get_lines",[0,0,-1,true]);

nvim.channelId

RPC Channel ID.

constchannelId=nvim.channelId;awaitnvim.call("nvim_create_autocmd",[["CursorMove"],{desc: "Notify my-plugin",command: `lua vim.rpcnotify(${channelId}, "my-notification")`,},]);

nvim.onNotification(notification: string, callback: function)

Registers a handler for a specific RPC Notification.

Notifications must be typed before you declare a handler for them, or TypeScript will complain.

import{attach,typeBaseEvents,typeEventsMap}from"bunvim";// an interface to define your notifications and their argsinterfaceMyEventsextendsBaseEvents{requests: EventsMap;// default typenotifications: {// declare custom notification: "cursor_move",// that would be called with args: [row: number, col: number]"cursor_move": [row: number,col: number];};}// attach to neovimconstnvim=awaitattach<MyEvents>({ ... })letcount=0;// register a handler for the notification "cursor_move"nvim.onNotification("cursor_move",async([row,col])=>{// "row" and "col" are of type "number" as specified above// CAUTION:// it's up to you to make sure the handler receives the correct args,// bunvim doesn't do any validations// print row and col in neovimawaitnvim.call("nvim_exec_lua",[`print("row: ${row} - col: ${col}")`,[]]);// return `true` to remove handlerreturncount++>=5;});// multiple handlers can be registered for the same notificationnvim.onNotification("cursor_move",async([row,col])=>{// replace contents in current buffer lines 1 and 2awaitnvim.call("nvim_buf_set_lines",[0,0,2,true,[`row: ${row}`,`col: ${col}`]]);});constchannelId=nvim.channelId;// create autocommand to notify our plugin via `vim.rpcnotify`// whenever the cursor movesawaitnvim.call("nvim_create_autocmd",[["CursorHold","CursorHoldI"],{desc: "Notify on Cursor Move",command: `lua local cursor_pos = vim.api.nvim_win_get_cursor(0) local row = cursor_pos[1] local col = cursor_pos[2] vim.rpcnotify(${channelId}, "cursor_move", row, col)`,},]);

nvim.onRequest(request: string, callback: function)

Registers a handler for a specific RPC Request.

Requests must be typed before you declare a handler for them, or TypeScript will complain.

The difference between an RPC Notification and an RPC Request, is that requests block neovim until a response is returned. Notifications are non-blocking.

import{attach,typeBaseEvents,typeEventsMap}from"bunvim";import{gracefulShutdown}from"./utils.ts";// an interface to define your requests and their argsinterfaceMyEventsextendsBaseEvents{notifications: EventsMap;// default typerequests: {// declare custom request: "before_exit",// that would be called with args: [buffer_name: string]"before_exit": [buffer_name: string];};}// attach to neovimconstnvim=awaitattach<MyEvents>({ ... })// register a handler for the request "before_exit"nvim.onRequest("before_exit",async([buffer_name])=>{// "buffer_name" is of type "string" as specified above// CAUTION:// it's up to you to make sure the handler receives the correct args,// bunvim doesn't do any validations// this should actually never get called,// because this handler gets overwritten belowconsole.log("buffer_name: ",buffer_name);// we must return something to unblock neovimreturnnull;});// only one handler per request may be registered.// if you call `nvim.onRequest` for an already registered handler,// the older handler is replaced with the new one.nvim.onRequest("before_exit",async([buffer_name])=>{gracefulShutdown(buffer_name);returnnull;});constchannelId=nvim.channelId;// create autocommand to call our function via `vim.rpcrequest`// whenever neovim is about to closeawaitnvim.call("nvim_create_autocmd",[["VimLeavePre"],{desc: "RPC Request before exit",command: `lua local buffer_name = vim.api.nvim_buf_get_name(0) vim.rpcrequest(${channelId}, "before_exit", buffer_name)`,},]);

nvim.detach()

Closes connection with neovim.

nvim.detach();

nvim.logger

Instance of winston logger. May be undefined if logging was not enabled.

Used to log data to console and/or file. Does not log/print messages to Neovim.

See Logging.

// log functions sorted from highest to lowest priority:nvim.logger?.error("error message");nvim.logger?.warn("warn message");nvim.logger?.info("info message");nvim.logger?.http("http message");nvim.logger?.verbose("verbose message");nvim.logger?.debug("debug message");nvim.logger?.silly("silly message");

🖨️ Logging

To enable logging to Console and/or File, a logging.level must be specified when calling the attach method.

constnvim=awaitattach({socket: SOCKET,client: {name: "my-plugin-name"},logging: {level: "debug",// <= LOG LEVEL},});nvim.logger?.info("hello world");

Bunvim internally logs with logger.debug() and logger.error(). Set logging.level higher than debug to not display bunvim's internal logs when printing logs for your plugin.

Levels from highest to lowest priority:

  1. error
  2. warn
  3. info
  4. http
  5. verbose
  6. debug
  7. silly

Console

After setting a logging.level, you can see your logs live with the command bunvim logs and specifying the client.name you defined in your attach.

In a terminal, run the command:

# this process will listen for logs and print them to the console# Ctrl-C to stop process
bunx bunvim logs my-plugin-name

For more information about the CLI tool, run the command:

bunx bunvim --help

File

You can also write your logs to a file by specifying a path when calling the attach method:

constnvim=awaitattach({socket: SOCKET,client: {name: "my-plugin-name"},logging: {level: "debug",// <= LOG LEVELfile: "/tmp/my-plugin-name.log",// <= PATH TO LOG FILE ("~" is not expanded)},});

Neovim

If you want to log/print a message to the user in Neovim, use:

import{NVIM_LOG_LEVELS}from"bunvim";awaitnvim.call("nvim_notify",["some message",NVIM_LOG_LEVELS.INFO,{}]);

About

Neovim Bun client.

Topics

Resources

Stars

33 stars

Watchers

1 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

Bunvim

Bunvim is a Bun client that allows you to interact with Neovim through RPC using TypeScript and JavaScript. If you're familiar with Neovim's Lua API, you'll find this client easy to use.

This client includes TypeScript definitions, generated from Neovim's api-metadata, describing API function signatures, including function parameters and return values. The Neovim version the bundled definitions were generated against is recorded in the file's header comment. If you're running a different Neovim version, you can regenerate them against your local Neovim with the command bunx bunvim types.

All functionality is implemented in one file. If you're looking for higher levels of abstraction, take a look at neovim/node-client and neoclide/neovim. Good luck.

✅ Requirements

📦 Installation

bun install bunvim

💻 Usage

For an example of a plugin using Bunvim, take a look at github-preview.nvim.

You should keep a tab open with Neovim API docs when working with Bunvim. Although this client includes generated TypeScript types, you'll find the detailed descriptions in the official docs very helpful if not necessary.

Create a script:

// my-plugin.tsimport{attach}from"bunvim";// RPC listening addressconstSOCKET="/tmp/bunvim.nvim.socket";constnvim=awaitattach({socket: SOCKET,client: {name: "my-plugin-name"},});// append "hello world" to current bufferawaitnvim.call("nvim_buf_set_lines",[0,-1,-1,true,["hello world"]]);// disable relative numbersawaitnvim.call("nvim_set_option_value",["relativenumber",false,{}]);// create a vertical splitawaitnvim.call("nvim_command",["vs"]);// print cursor position on cursor moveawaitnvim.call("nvim_create_autocmd",[["CursorHold","CursorHoldI"],{desc: "Print Cursor Position",command: `lua local cursor_pos = vim.api.nvim_win_get_cursor(0) vim.print(cursor_pos)`,},]);nvim.detach();

Initialize Neovim with the RPC listening address specified above:

nvim --listen /tmp/bunvim.nvim.socket

Execute your script from another terminal:

bun run my-plugin.ts

If your plugin is executed as a child process of Neovim:

-- somewhere in your neovim lua config fileslocalfunctionrun_script()
-- neovim sets the environment variable NVIM in all its child processes-- NVIM = the RPC listening address assigned to the current neovim instance-- neovim sets an RPC listening address if you don't manually specify one-- https://neovim.io/doc/user/builtin.html#jobstart-envvim.fn.jobstart("bun run my-plugin.ts", {
cwd=vim.fn.expand("~/path/to/plugin/"),
})
endvim.api.nvim_create_user_command("RunMyScript", run_script, {})

You could then open Neovim without manually specifying an RPC listening address, just nvim and then run the command :RunMyScript. Your Bun process would then have access to the NVIM environment variable.

// my-plugin.tsimport{attach}from"bunvim";constSOCKET=process.env["NVIM"];if(!SOCKET)throwError("socket missing");constnvim=awaitattach({socket: SOCKET,client: {name: "my-plugin-name"},});

📖 API Reference

This module exports only one method attach and a bunch of TypeScript types. attach returns an Nvim object that can be used to interact with Neovim.

nvim.call(function: string, args: unknown[])

Used to call any of these functions. They're all typed. You should get function names autocompletion & warnings from TypeScript if the parameters don't match the expected types. Some function calls return a value, others don't.

constbufferContent=awaitnvim.call("nvim_buf_get_lines",[0,0,-1,true]);

nvim.channelId

RPC Channel ID.

constchannelId=nvim.channelId;awaitnvim.call("nvim_create_autocmd",[["CursorMove"],{desc: "Notify my-plugin",command: `lua vim.rpcnotify(${channelId}, "my-notification")`,},]);

nvim.onNotification(notification: string, callback: function)

Registers a handler for a specific RPC Notification.

Notifications must be typed before you declare a handler for them, or TypeScript will complain.

import{attach,typeBaseEvents,typeEventsMap}from"bunvim";// an interface to define your notifications and their argsinterfaceMyEventsextendsBaseEvents{requests: EventsMap;// default typenotifications: {// declare custom notification: "cursor_move",// that would be called with args: [row: number, col: number]"cursor_move": [row: number,col: number];};}// attach to neovimconstnvim=awaitattach<MyEvents>({ ... })letcount=0;// register a handler for the notification "cursor_move"nvim.onNotification("cursor_move",async([row,col])=>{// "row" and "col" are of type "number" as specified above// CAUTION:// it's up to you to make sure the handler receives the correct args,// bunvim doesn't do any validations// print row and col in neovimawaitnvim.call("nvim_exec_lua",[`print("row: ${row} - col: ${col}")`,[]]);// return `true` to remove handlerreturncount++>=5;});// multiple handlers can be registered for the same notificationnvim.onNotification("cursor_move",async([row,col])=>{// replace contents in current buffer lines 1 and 2awaitnvim.call("nvim_buf_set_lines",[0,0,2,true,[`row: ${row}`,`col: ${col}`]]);});constchannelId=nvim.channelId;// create autocommand to notify our plugin via `vim.rpcnotify`// whenever the cursor movesawaitnvim.call("nvim_create_autocmd",[["CursorHold","CursorHoldI"],{desc: "Notify on Cursor Move",command: `lua local cursor_pos = vim.api.nvim_win_get_cursor(0) local row = cursor_pos[1] local col = cursor_pos[2] vim.rpcnotify(${channelId}, "cursor_move", row, col)`,},]);

nvim.onRequest(request: string, callback: function)

Registers a handler for a specific RPC Request.

Requests must be typed before you declare a handler for them, or TypeScript will complain.

The difference between an RPC Notification and an RPC Request, is that requests block neovim until a response is returned. Notifications are non-blocking.

import{attach,typeBaseEvents,typeEventsMap}from"bunvim";import{gracefulShutdown}from"./utils.ts";// an interface to define your requests and their argsinterfaceMyEventsextendsBaseEvents{notifications: EventsMap;// default typerequests: {// declare custom request: "before_exit",// that would be called with args: [buffer_name: string]"before_exit": [buffer_name: string];};}// attach to neovimconstnvim=awaitattach<MyEvents>({ ... })// register a handler for the request "before_exit"nvim.onRequest("before_exit",async([buffer_name])=>{// "buffer_name" is of type "string" as specified above// CAUTION:// it's up to you to make sure the handler receives the correct args,// bunvim doesn't do any validations// this should actually never get called,// because this handler gets overwritten belowconsole.log("buffer_name: ",buffer_name);// we must return something to unblock neovimreturnnull;});// only one handler per request may be registered.// if you call `nvim.onRequest` for an already registered handler,// the older handler is replaced with the new one.nvim.onRequest("before_exit",async([buffer_name])=>{gracefulShutdown(buffer_name);returnnull;});constchannelId=nvim.channelId;// create autocommand to call our function via `vim.rpcrequest`// whenever neovim is about to closeawaitnvim.call("nvim_create_autocmd",[["VimLeavePre"],{desc: "RPC Request before exit",command: `lua local buffer_name = vim.api.nvim_buf_get_name(0) vim.rpcrequest(${channelId}, "before_exit", buffer_name)`,},]);

nvim.detach()

Closes connection with neovim.

nvim.detach();

nvim.logger

Instance of winston logger. May be undefined if logging was not enabled.

Used to log data to console and/or file. Does not log/print messages to Neovim.

See Logging.

// log functions sorted from highest to lowest priority:nvim.logger?.error("error message");nvim.logger?.warn("warn message");nvim.logger?.info("info message");nvim.logger?.http("http message");nvim.logger?.verbose("verbose message");nvim.logger?.debug("debug message");nvim.logger?.silly("silly message");

🖨️ Logging

To enable logging to Console and/or File, a logging.level must be specified when calling the attach method.

constnvim=awaitattach({socket: SOCKET,client: {name: "my-plugin-name"},logging: {level: "debug",// <= LOG LEVEL},});nvim.logger?.info("hello world");

Bunvim internally logs with logger.debug() and logger.error(). Set logging.level higher than debug to not display bunvim's internal logs when printing logs for your plugin.

Levels from highest to lowest priority:

  1. error
  2. warn
  3. info
  4. http
  5. verbose
  6. debug
  7. silly

Console

After setting a logging.level, you can see your logs live with the command bunvim logs and specifying the client.name you defined in your attach.

In a terminal, run the command:

# this process will listen for logs and print them to the console# Ctrl-C to stop process
bunx bunvim logs my-plugin-name

For more information about the CLI tool, run the command:

bunx bunvim --help

File

You can also write your logs to a file by specifying a path when calling the attach method:

constnvim=awaitattach({socket: SOCKET,client: {name: "my-plugin-name"},logging: {level: "debug",// <= LOG LEVELfile: "/tmp/my-plugin-name.log",// <= PATH TO LOG FILE ("~" is not expanded)},});

Neovim

If you want to log/print a message to the user in Neovim, use:

import{NVIM_LOG_LEVELS}from"bunvim";awaitnvim.call("nvim_notify",["some message",NVIM_LOG_LEVELS.INFO,{}]);

About

Neovim Bun client.

Topics

Resources

Stars

33 stars

Watchers

1 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Bunvim

Bunvim is a Bun client that allows you to interact with Neovim through RPC using TypeScript and JavaScript. If you're familiar with Neovim's Lua API, you'll find this client easy to use.

This client includes TypeScript definitions, generated from Neovim's api-metadata, describing API function signatures, including function parameters and return values. The Neovim version the bundled definitions were generated against is recorded in the file's header comment. If you're running a different Neovim version, you can regenerate them against your local Neovim with the command bunx bunvim types.

All functionality is implemented in one file. If you're looking for higher levels of abstraction, take a look at neovim/node-client and neoclide/neovim. Good luck.

✅ Requirements

📦 Installation

bun install bunvim

💻 Usage

For an example of a plugin using Bunvim, take a look at github-preview.nvim.

You should keep a tab open with Neovim API docs when working with Bunvim. Although this client includes generated TypeScript types, you'll find the detailed descriptions in the official docs very helpful if not necessary.

Create a script:

// my-plugin.tsimport{attach}from"bunvim";// RPC listening addressconstSOCKET="/tmp/bunvim.nvim.socket";constnvim=awaitattach({socket: SOCKET,client: {name: "my-plugin-name"},});// append "hello world" to current bufferawaitnvim.call("nvim_buf_set_lines",[0,-1,-1,true,["hello world"]]);// disable relative numbersawaitnvim.call("nvim_set_option_value",["relativenumber",false,{}]);// create a vertical splitawaitnvim.call("nvim_command",["vs"]);// print cursor position on cursor moveawaitnvim.call("nvim_create_autocmd",[["CursorHold","CursorHoldI"],{desc: "Print Cursor Position",command: `lua local cursor_pos = vim.api.nvim_win_get_cursor(0) vim.print(cursor_pos)`,},]);nvim.detach();

Initialize Neovim with the RPC listening address specified above:

nvim --listen /tmp/bunvim.nvim.socket

Execute your script from another terminal:

bun run my-plugin.ts

If your plugin is executed as a child process of Neovim:

-- somewhere in your neovim lua config fileslocalfunctionrun_script()
-- neovim sets the environment variable NVIM in all its child processes-- NVIM = the RPC listening address assigned to the current neovim instance-- neovim sets an RPC listening address if you don't manually specify one-- https://neovim.io/doc/user/builtin.html#jobstart-envvim.fn.jobstart("bun run my-plugin.ts", {
cwd=vim.fn.expand("~/path/to/plugin/"),
})
endvim.api.nvim_create_user_command("RunMyScript", run_script, {})

You could then open Neovim without manually specifying an RPC listening address, just nvim and then run the command :RunMyScript. Your Bun process would then have access to the NVIM environment variable.

// my-plugin.tsimport{attach}from"bunvim";constSOCKET=process.env["NVIM"];if(!SOCKET)throwError("socket missing");constnvim=awaitattach({socket: SOCKET,client: {name: "my-plugin-name"},});

📖 API Reference

This module exports only one method attach and a bunch of TypeScript types. attach returns an Nvim object that can be used to interact with Neovim.

nvim.call(function: string, args: unknown[])

Used to call any of these functions. They're all typed. You should get function names autocompletion & warnings from TypeScript if the parameters don't match the expected types. Some function calls return a value, others don't.

constbufferContent=awaitnvim.call("nvim_buf_get_lines",[0,0,-1,true]);

nvim.channelId

RPC Channel ID.

constchannelId=nvim.channelId;awaitnvim.call("nvim_create_autocmd",[["CursorMove"],{desc: "Notify my-plugin",command: `lua vim.rpcnotify(${channelId}, "my-notification")`,},]);

nvim.onNotification(notification: string, callback: function)

Registers a handler for a specific RPC Notification.

Notifications must be typed before you declare a handler for them, or TypeScript will complain.

import{attach,typeBaseEvents,typeEventsMap}from"bunvim";// an interface to define your notifications and their argsinterfaceMyEventsextendsBaseEvents{requests: EventsMap;// default typenotifications: {// declare custom notification: "cursor_move",// that would be called with args: [row: number, col: number]"cursor_move": [row: number,col: number];};}// attach to neovimconstnvim=awaitattach<MyEvents>({ ... })letcount=0;// register a handler for the notification "cursor_move"nvim.onNotification("cursor_move",async([row,col])=>{// "row" and "col" are of type "number" as specified above// CAUTION:// it's up to you to make sure the handler receives the correct args,// bunvim doesn't do any validations// print row and col in neovimawaitnvim.call("nvim_exec_lua",[`print("row: ${row} - col: ${col}")`,[]]);// return `true` to remove handlerreturncount++>=5;});// multiple handlers can be registered for the same notificationnvim.onNotification("cursor_move",async([row,col])=>{// replace contents in current buffer lines 1 and 2awaitnvim.call("nvim_buf_set_lines",[0,0,2,true,[`row: ${row}`,`col: ${col}`]]);});constchannelId=nvim.channelId;// create autocommand to notify our plugin via `vim.rpcnotify`// whenever the cursor movesawaitnvim.call("nvim_create_autocmd",[["CursorHold","CursorHoldI"],{desc: "Notify on Cursor Move",command: `lua local cursor_pos = vim.api.nvim_win_get_cursor(0) local row = cursor_pos[1] local col = cursor_pos[2] vim.rpcnotify(${channelId}, "cursor_move", row, col)`,},]);

nvim.onRequest(request: string, callback: function)

Registers a handler for a specific RPC Request.

Requests must be typed before you declare a handler for them, or TypeScript will complain.

The difference between an RPC Notification and an RPC Request, is that requests block neovim until a response is returned. Notifications are non-blocking.

import{attach,typeBaseEvents,typeEventsMap}from"bunvim";import{gracefulShutdown}from"./utils.ts";// an interface to define your requests and their argsinterfaceMyEventsextendsBaseEvents{notifications: EventsMap;// default typerequests: {// declare custom request: "before_exit",// that would be called with args: [buffer_name: string]"before_exit": [buffer_name: string];};}// attach to neovimconstnvim=awaitattach<MyEvents>({ ... })// register a handler for the request "before_exit"nvim.onRequest("before_exit",async([buffer_name])=>{// "buffer_name" is of type "string" as specified above// CAUTION:// it's up to you to make sure the handler receives the correct args,// bunvim doesn't do any validations// this should actually never get called,// because this handler gets overwritten belowconsole.log("buffer_name: ",buffer_name);// we must return something to unblock neovimreturnnull;});// only one handler per request may be registered.// if you call `nvim.onRequest` for an already registered handler,// the older handler is replaced with the new one.nvim.onRequest("before_exit",async([buffer_name])=>{gracefulShutdown(buffer_name);returnnull;});constchannelId=nvim.channelId;// create autocommand to call our function via `vim.rpcrequest`// whenever neovim is about to closeawaitnvim.call("nvim_create_autocmd",[["VimLeavePre"],{desc: "RPC Request before exit",command: `lua local buffer_name = vim.api.nvim_buf_get_name(0) vim.rpcrequest(${channelId}, "before_exit", buffer_name)`,},]);

nvim.detach()

Closes connection with neovim.

nvim.detach();

nvim.logger

Instance of winston logger. May be undefined if logging was not enabled.

Used to log data to console and/or file. Does not log/print messages to Neovim.

See Logging.

// log functions sorted from highest to lowest priority:nvim.logger?.error("error message");nvim.logger?.warn("warn message");nvim.logger?.info("info message");nvim.logger?.http("http message");nvim.logger?.verbose("verbose message");nvim.logger?.debug("debug message");nvim.logger?.silly("silly message");

🖨️ Logging

To enable logging to Console and/or File, a logging.level must be specified when calling the attach method.

constnvim=awaitattach({socket: SOCKET,client: {name: "my-plugin-name"},logging: {level: "debug",// <= LOG LEVEL},});nvim.logger?.info("hello world");

Bunvim internally logs with logger.debug() and logger.error(). Set logging.level higher than debug to not display bunvim's internal logs when printing logs for your plugin.

Levels from highest to lowest priority:

  1. error
  2. warn
  3. info
  4. http
  5. verbose
  6. debug
  7. silly

Console

After setting a logging.level, you can see your logs live with the command bunvim logs and specifying the client.name you defined in your attach.

In a terminal, run the command:

# this process will listen for logs and print them to the console# Ctrl-C to stop process
bunx bunvim logs my-plugin-name

For more information about the CLI tool, run the command:

bunx bunvim --help

File

You can also write your logs to a file by specifying a path when calling the attach method:

constnvim=awaitattach({socket: SOCKET,client: {name: "my-plugin-name"},logging: {level: "debug",// <= LOG LEVELfile: "/tmp/my-plugin-name.log",// <= PATH TO LOG FILE ("~" is not expanded)},});

Neovim

If you want to log/print a message to the user in Neovim, use:

import{NVIM_LOG_LEVELS}from"bunvim";awaitnvim.call("nvim_notify",["some message",NVIM_LOG_LEVELS.INFO,{}]);

About

Neovim Bun client.

Topics

Resources

Stars

33 stars

Watchers

1 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Bunvim

Bunvim is a Bun client that allows you to interact with Neovim through RPC using TypeScript and JavaScript. If you're familiar with Neovim's Lua API, you'll find this client easy to use.

This client includes TypeScript definitions, generated from Neovim's api-metadata, describing API function signatures, including function parameters and return values. The Neovim version the bundled definitions were generated against is recorded in the file's header comment. If you're running a different Neovim version, you can regenerate them against your local Neovim with the command bunx bunvim types.

All functionality is implemented in one file. If you're looking for higher levels of abstraction, take a look at neovim/node-client and neoclide/neovim. Good luck.

✅ Requirements

📦 Installation

bun install bunvim

💻 Usage

For an example of a plugin using Bunvim, take a look at github-preview.nvim.

You should keep a tab open with Neovim API docs when working with Bunvim. Although this client includes generated TypeScript types, you'll find the detailed descriptions in the official docs very helpful if not necessary.

Create a script:

// my-plugin.tsimport{attach}from"bunvim";// RPC listening addressconstSOCKET="/tmp/bunvim.nvim.socket";constnvim=awaitattach({socket: SOCKET,client: {name: "my-plugin-name"},});// append "hello world" to current bufferawaitnvim.call("nvim_buf_set_lines",[0,-1,-1,true,["hello world"]]);// disable relative numbersawaitnvim.call("nvim_set_option_value",["relativenumber",false,{}]);// create a vertical splitawaitnvim.call("nvim_command",["vs"]);// print cursor position on cursor moveawaitnvim.call("nvim_create_autocmd",[["CursorHold","CursorHoldI"],{desc: "Print Cursor Position",command: `lua local cursor_pos = vim.api.nvim_win_get_cursor(0) vim.print(cursor_pos)`,},]);nvim.detach();

Initialize Neovim with the RPC listening address specified above:

nvim --listen /tmp/bunvim.nvim.socket

Execute your script from another terminal:

bun run my-plugin.ts

If your plugin is executed as a child process of Neovim:

-- somewhere in your neovim lua config fileslocalfunctionrun_script()
-- neovim sets the environment variable NVIM in all its child processes-- NVIM = the RPC listening address assigned to the current neovim instance-- neovim sets an RPC listening address if you don't manually specify one-- https://neovim.io/doc/user/builtin.html#jobstart-envvim.fn.jobstart("bun run my-plugin.ts", {
cwd=vim.fn.expand("~/path/to/plugin/"),
})
endvim.api.nvim_create_user_command("RunMyScript", run_script, {})

You could then open Neovim without manually specifying an RPC listening address, just nvim and then run the command :RunMyScript. Your Bun process would then have access to the NVIM environment variable.

// my-plugin.tsimport{attach}from"bunvim";constSOCKET=process.env["NVIM"];if(!SOCKET)throwError("socket missing");constnvim=awaitattach({socket: SOCKET,client: {name: "my-plugin-name"},});

📖 API Reference

This module exports only one method attach and a bunch of TypeScript types. attach returns an Nvim object that can be used to interact with Neovim.

nvim.call(function: string, args: unknown[])

Used to call any of these functions. They're all typed. You should get function names autocompletion & warnings from TypeScript if the parameters don't match the expected types. Some function calls return a value, others don't.

constbufferContent=awaitnvim.call("nvim_buf_get_lines",[0,0,-1,true]);

nvim.channelId

RPC Channel ID.

constchannelId=nvim.channelId;awaitnvim.call("nvim_create_autocmd",[["CursorMove"],{desc: "Notify my-plugin",command: `lua vim.rpcnotify(${channelId}, "my-notification")`,},]);

nvim.onNotification(notification: string, callback: function)

Registers a handler for a specific RPC Notification.

Notifications must be typed before you declare a handler for them, or TypeScript will complain.

import{attach,typeBaseEvents,typeEventsMap}from"bunvim";// an interface to define your notifications and their argsinterfaceMyEventsextendsBaseEvents{requests: EventsMap;// default typenotifications: {// declare custom notification: "cursor_move",// that would be called with args: [row: number, col: number]"cursor_move": [row: number,col: number];};}// attach to neovimconstnvim=awaitattach<MyEvents>({ ... })letcount=0;// register a handler for the notification "cursor_move"nvim.onNotification("cursor_move",async([row,col])=>{// "row" and "col" are of type "number" as specified above// CAUTION:// it's up to you to make sure the handler receives the correct args,// bunvim doesn't do any validations// print row and col in neovimawaitnvim.call("nvim_exec_lua",[`print("row: ${row} - col: ${col}")`,[]]);// return `true` to remove handlerreturncount++>=5;});// multiple handlers can be registered for the same notificationnvim.onNotification("cursor_move",async([row,col])=>{// replace contents in current buffer lines 1 and 2awaitnvim.call("nvim_buf_set_lines",[0,0,2,true,[`row: ${row}`,`col: ${col}`]]);});constchannelId=nvim.channelId;// create autocommand to notify our plugin via `vim.rpcnotify`// whenever the cursor movesawaitnvim.call("nvim_create_autocmd",[["CursorHold","CursorHoldI"],{desc: "Notify on Cursor Move",command: `lua local cursor_pos = vim.api.nvim_win_get_cursor(0) local row = cursor_pos[1] local col = cursor_pos[2] vim.rpcnotify(${channelId}, "cursor_move", row, col)`,},]);

nvim.onRequest(request: string, callback: function)

Registers a handler for a specific RPC Request.

Requests must be typed before you declare a handler for them, or TypeScript will complain.

The difference between an RPC Notification and an RPC Request, is that requests block neovim until a response is returned. Notifications are non-blocking.

import{attach,typeBaseEvents,typeEventsMap}from"bunvim";import{gracefulShutdown}from"./utils.ts";// an interface to define your requests and their argsinterfaceMyEventsextendsBaseEvents{notifications: EventsMap;// default typerequests: {// declare custom request: "before_exit",// that would be called with args: [buffer_name: string]"before_exit": [buffer_name: string];};}// attach to neovimconstnvim=awaitattach<MyEvents>({ ... })// register a handler for the request "before_exit"nvim.onRequest("before_exit",async([buffer_name])=>{// "buffer_name" is of type "string" as specified above// CAUTION:// it's up to you to make sure the handler receives the correct args,// bunvim doesn't do any validations// this should actually never get called,// because this handler gets overwritten belowconsole.log("buffer_name: ",buffer_name);// we must return something to unblock neovimreturnnull;});// only one handler per request may be registered.// if you call `nvim.onRequest` for an already registered handler,// the older handler is replaced with the new one.nvim.onRequest("before_exit",async([buffer_name])=>{gracefulShutdown(buffer_name);returnnull;});constchannelId=nvim.channelId;// create autocommand to call our function via `vim.rpcrequest`// whenever neovim is about to closeawaitnvim.call("nvim_create_autocmd",[["VimLeavePre"],{desc: "RPC Request before exit",command: `lua local buffer_name = vim.api.nvim_buf_get_name(0) vim.rpcrequest(${channelId}, "before_exit", buffer_name)`,},]);

nvim.detach()

Closes connection with neovim.

nvim.detach();

nvim.logger

Instance of winston logger. May be undefined if logging was not enabled.

Used to log data to console and/or file. Does not log/print messages to Neovim.

See Logging.

// log functions sorted from highest to lowest priority:nvim.logger?.error("error message");nvim.logger?.warn("warn message");nvim.logger?.info("info message");nvim.logger?.http("http message");nvim.logger?.verbose("verbose message");nvim.logger?.debug("debug message");nvim.logger?.silly("silly message");

🖨️ Logging

To enable logging to Console and/or File, a logging.level must be specified when calling the attach method.

constnvim=awaitattach({socket: SOCKET,client: {name: "my-plugin-name"},logging: {level: "debug",// <= LOG LEVEL},});nvim.logger?.info("hello world");

Bunvim internally logs with logger.debug() and logger.error(). Set logging.level higher than debug to not display bunvim's internal logs when printing logs for your plugin.

Levels from highest to lowest priority:

  1. error
  2. warn
  3. info
  4. http
  5. verbose
  6. debug
  7. silly

Console

After setting a logging.level, you can see your logs live with the command bunvim logs and specifying the client.name you defined in your attach.

In a terminal, run the command:

# this process will listen for logs and print them to the console# Ctrl-C to stop process
bunx bunvim logs my-plugin-name

For more information about the CLI tool, run the command:

bunx bunvim --help

File

You can also write your logs to a file by specifying a path when calling the attach method:

constnvim=awaitattach({socket: SOCKET,client: {name: "my-plugin-name"},logging: {level: "debug",// <= LOG LEVELfile: "/tmp/my-plugin-name.log",// <= PATH TO LOG FILE ("~" is not expanded)},});

Neovim

If you want to log/print a message to the user in Neovim, use:

import{NVIM_LOG_LEVELS}from"bunvim";awaitnvim.call("nvim_notify",["some message",NVIM_LOG_LEVELS.INFO,{}]);

About

Neovim Bun client.

Topics

Resources

Stars

33 stars

Watchers

1 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

Bunvim

Bunvim is a Bun client that allows you to interact with Neovim through RPC using TypeScript and JavaScript. If you're familiar with Neovim's Lua API, you'll find this client easy to use.

This client includes TypeScript definitions, generated from Neovim's api-metadata, describing API function signatures, including function parameters and return values. The Neovim version the bundled definitions were generated against is recorded in the file's header comment. If you're running a different Neovim version, you can regenerate them against your local Neovim with the command bunx bunvim types.

All functionality is implemented in one file. If you're looking for higher levels of abstraction, take a look at neovim/node-client and neoclide/neovim. Good luck.

✅ Requirements

📦 Installation

bun install bunvim

💻 Usage

For an example of a plugin using Bunvim, take a look at github-preview.nvim.

You should keep a tab open with Neovim API docs when working with Bunvim. Although this client includes generated TypeScript types, you'll find the detailed descriptions in the official docs very helpful if not necessary.

Create a script:

// my-plugin.tsimport{attach}from"bunvim";// RPC listening addressconstSOCKET="/tmp/bunvim.nvim.socket";constnvim=awaitattach({socket: SOCKET,client: {name: "my-plugin-name"},});// append "hello world" to current bufferawaitnvim.call("nvim_buf_set_lines",[0,-1,-1,true,["hello world"]]);// disable relative numbersawaitnvim.call("nvim_set_option_value",["relativenumber",false,{}]);// create a vertical splitawaitnvim.call("nvim_command",["vs"]);// print cursor position on cursor moveawaitnvim.call("nvim_create_autocmd",[["CursorHold","CursorHoldI"],{desc: "Print Cursor Position",command: `lua local cursor_pos = vim.api.nvim_win_get_cursor(0) vim.print(cursor_pos)`,},]);nvim.detach();

Initialize Neovim with the RPC listening address specified above:

nvim --listen /tmp/bunvim.nvim.socket

Execute your script from another terminal:

bun run my-plugin.ts

If your plugin is executed as a child process of Neovim:

-- somewhere in your neovim lua config fileslocalfunctionrun_script()
-- neovim sets the environment variable NVIM in all its child processes-- NVIM = the RPC listening address assigned to the current neovim instance-- neovim sets an RPC listening address if you don't manually specify one-- https://neovim.io/doc/user/builtin.html#jobstart-envvim.fn.jobstart("bun run my-plugin.ts", {
cwd=vim.fn.expand("~/path/to/plugin/"),
})
endvim.api.nvim_create_user_command("RunMyScript", run_script, {})

You could then open Neovim without manually specifying an RPC listening address, just nvim and then run the command :RunMyScript. Your Bun process would then have access to the NVIM environment variable.

// my-plugin.tsimport{attach}from"bunvim";constSOCKET=process.env["NVIM"];if(!SOCKET)throwError("socket missing");constnvim=awaitattach({socket: SOCKET,client: {name: "my-plugin-name"},});

📖 API Reference

This module exports only one method attach and a bunch of TypeScript types. attach returns an Nvim object that can be used to interact with Neovim.

nvim.call(function: string, args: unknown[])

Used to call any of these functions. They're all typed. You should get function names autocompletion & warnings from TypeScript if the parameters don't match the expected types. Some function calls return a value, others don't.

constbufferContent=awaitnvim.call("nvim_buf_get_lines",[0,0,-1,true]);

nvim.channelId

RPC Channel ID.

constchannelId=nvim.channelId;awaitnvim.call("nvim_create_autocmd",[["CursorMove"],{desc: "Notify my-plugin",command: `lua vim.rpcnotify(${channelId}, "my-notification")`,},]);

nvim.onNotification(notification: string, callback: function)

Registers a handler for a specific RPC Notification.

Notifications must be typed before you declare a handler for them, or TypeScript will complain.

import{attach,typeBaseEvents,typeEventsMap}from"bunvim";// an interface to define your notifications and their argsinterfaceMyEventsextendsBaseEvents{requests: EventsMap;// default typenotifications: {// declare custom notification: "cursor_move",// that would be called with args: [row: number, col: number]"cursor_move": [row: number,col: number];};}// attach to neovimconstnvim=awaitattach<MyEvents>({ ... })letcount=0;// register a handler for the notification "cursor_move"nvim.onNotification("cursor_move",async([row,col])=>{// "row" and "col" are of type "number" as specified above// CAUTION:// it's up to you to make sure the handler receives the correct args,// bunvim doesn't do any validations// print row and col in neovimawaitnvim.call("nvim_exec_lua",[`print("row: ${row} - col: ${col}")`,[]]);// return `true` to remove handlerreturncount++>=5;});// multiple handlers can be registered for the same notificationnvim.onNotification("cursor_move",async([row,col])=>{// replace contents in current buffer lines 1 and 2awaitnvim.call("nvim_buf_set_lines",[0,0,2,true,[`row: ${row}`,`col: ${col}`]]);});constchannelId=nvim.channelId;// create autocommand to notify our plugin via `vim.rpcnotify`// whenever the cursor movesawaitnvim.call("nvim_create_autocmd",[["CursorHold","CursorHoldI"],{desc: "Notify on Cursor Move",command: `lua local cursor_pos = vim.api.nvim_win_get_cursor(0) local row = cursor_pos[1] local col = cursor_pos[2] vim.rpcnotify(${channelId}, "cursor_move", row, col)`,},]);

nvim.onRequest(request: string, callback: function)

Registers a handler for a specific RPC Request.

Requests must be typed before you declare a handler for them, or TypeScript will complain.

The difference between an RPC Notification and an RPC Request, is that requests block neovim until a response is returned. Notifications are non-blocking.

import{attach,typeBaseEvents,typeEventsMap}from"bunvim";import{gracefulShutdown}from"./utils.ts";// an interface to define your requests and their argsinterfaceMyEventsextendsBaseEvents{notifications: EventsMap;// default typerequests: {// declare custom request: "before_exit",// that would be called with args: [buffer_name: string]"before_exit": [buffer_name: string];};}// attach to neovimconstnvim=awaitattach<MyEvents>({ ... })// register a handler for the request "before_exit"nvim.onRequest("before_exit",async([buffer_name])=>{// "buffer_name" is of type "string" as specified above// CAUTION:// it's up to you to make sure the handler receives the correct args,// bunvim doesn't do any validations// this should actually never get called,// because this handler gets overwritten belowconsole.log("buffer_name: ",buffer_name);// we must return something to unblock neovimreturnnull;});// only one handler per request may be registered.// if you call `nvim.onRequest` for an already registered handler,// the older handler is replaced with the new one.nvim.onRequest("before_exit",async([buffer_name])=>{gracefulShutdown(buffer_name);returnnull;});constchannelId=nvim.channelId;// create autocommand to call our function via `vim.rpcrequest`// whenever neovim is about to closeawaitnvim.call("nvim_create_autocmd",[["VimLeavePre"],{desc: "RPC Request before exit",command: `lua local buffer_name = vim.api.nvim_buf_get_name(0) vim.rpcrequest(${channelId}, "before_exit", buffer_name)`,},]);

nvim.detach()

Closes connection with neovim.

nvim.detach();

nvim.logger

Instance of winston logger. May be undefined if logging was not enabled.

Used to log data to console and/or file. Does not log/print messages to Neovim.

See Logging.

// log functions sorted from highest to lowest priority:nvim.logger?.error("error message");nvim.logger?.warn("warn message");nvim.logger?.info("info message");nvim.logger?.http("http message");nvim.logger?.verbose("verbose message");nvim.logger?.debug("debug message");nvim.logger?.silly("silly message");

🖨️ Logging

To enable logging to Console and/or File, a logging.level must be specified when calling the attach method.

constnvim=awaitattach({socket: SOCKET,client: {name: "my-plugin-name"},logging: {level: "debug",// <= LOG LEVEL},});nvim.logger?.info("hello world");

Bunvim internally logs with logger.debug() and logger.error(). Set logging.level higher than debug to not display bunvim's internal logs when printing logs for your plugin.

Levels from highest to lowest priority:

  1. error
  2. warn
  3. info
  4. http
  5. verbose
  6. debug
  7. silly

Console

After setting a logging.level, you can see your logs live with the command bunvim logs and specifying the client.name you defined in your attach.

In a terminal, run the command:

# this process will listen for logs and print them to the console# Ctrl-C to stop process
bunx bunvim logs my-plugin-name

For more information about the CLI tool, run the command:

bunx bunvim --help

File

You can also write your logs to a file by specifying a path when calling the attach method:

constnvim=awaitattach({socket: SOCKET,client: {name: "my-plugin-name"},logging: {level: "debug",// <= LOG LEVELfile: "/tmp/my-plugin-name.log",// <= PATH TO LOG FILE ("~" is not expanded)},});

Neovim

If you want to log/print a message to the user in Neovim, use:

import{NVIM_LOG_LEVELS}from"bunvim";awaitnvim.call("nvim_notify",["some message",NVIM_LOG_LEVELS.INFO,{}]);

About

Neovim Bun client.

Topics

Resources

Stars

33 stars

Watchers

1 watching

Forks

Releases

Used by

Contributors

Languages