Neovim remote plugins allow you to extend Neovim functionality using JavaScript/TypeScript. This package provides both decorator-based and class-based approaches for creating plugins with commands, functions, and autocommands.
/**
* Class decorator for marking a class as a Neovim plugin.
* Automatically processes method decorators (@Command, @Function, @Autocmd).
*
* @param target - Plugin class
* @returns Decorated class
*/
function Plugin(target: any): any;
/**
* Class decorator with options for marking a class as a Neovim plugin.
*
* @param options - Plugin options
* @returns Decorator function
*/
function Plugin(options: PluginDecoratorOptions): (target: any) => any;
interface PluginDecoratorOptions {
/** Enable development mode (clears module cache on reload) */
dev?: boolean;
}Usage Examples:
import { Plugin, Command, Function as NvimFunction, Autocmd, NvimPlugin } from 'neovim';
// Basic plugin
@Plugin
export class MyPlugin {
// Plugin implementation
}
// Plugin with development mode
@Plugin({ dev: true })
export class DevPlugin {
// Development mode - module reloaded on each invocation
}
// Plugin without decorator (functional style)
export default function(plugin: NvimPlugin) {
// Manual registration
}/**
* Method decorator for registering Neovim commands.
* Commands are invoked from Neovim with :CommandName
*
* @param name - Command name (will be invoked as :Name)
* @param options - Command options
* @returns Method decorator
*/
function Command(name: string, options?: CommandOptions): MethodDecorator;
interface CommandOptions {
/** Force synchronous execution (default: false) */
sync?: boolean;
/** Range specification (see :help :command-range) */
range?: string;
/** Number of arguments: '0', '1', '*', '?', '+' (see :help :command-nargs) */
nargs?: string;
/** Completion type (see :help :command-complete) */
complete?: string;
}Usage Examples:
import { Plugin, Command } from 'neovim';
@Plugin
export class CommandExamples {
// Simple command (async)
@Command('HelloWorld')
async helloWorld() {
console.log('Hello from command!');
}
// Synchronous command
@Command('SyncCommand', { sync: true })
syncCommand() {
return 'Immediate result';
}
// Command with arguments
@Command('Echo', { nargs: '*' })
async echo(args: string[]) {
const message = args.join(' ');
console.log('Echo:', message);
}
// Command with specific arg count
@Command('Add', { nargs: '2' })
async add(args: string[]) {
const [a, b] = args.map(Number);
return a + b;
}
// Command with range
@Command('ProcessRange', { range: '', nargs: '0' })
async processRange(args: string[], range: [number, number]) {
const [start, end] = range;
console.log(`Processing lines ${start} to ${end}`);
}
// Command with completion
@Command('OpenFile', { nargs: '1', complete: 'file' })
async openFile(args: string[]) {
const filename = args[0];
console.log('Opening:', filename);
}
}/**
* Method decorator for registering Neovim functions.
* Functions are invoked from Neovim with :call FunctionName(args)
*
* @param name - Function name (will be invoked as FunctionName())
* @param options - Function options
* @returns Method decorator
*/
function Function(name: string, options?: NvimFunctionOptions): MethodDecorator;
interface NvimFunctionOptions {
/** Force synchronous execution (default: false) */
sync?: boolean;
/** Range parameters */
range?: [number, number];
/** VimL expression to evaluate and pass as argument */
eval?: string;
}Usage Examples:
import { Plugin, Function as NvimFunction } from 'neovim';
@Plugin
export class FunctionExamples {
// Simple function
@NvimFunction('GetTimestamp')
async getTimestamp() {
return Date.now();
}
// Synchronous function
@NvimFunction('Double', { sync: true })
double(args: number[]) {
return args[0] * 2;
}
// Function with eval
@NvimFunction('CurrentLine', { eval: 'line(".")' })
async currentLine(args: [number]) {
const lineNum = args[0];
console.log('Current line:', lineNum);
return lineNum;
}
// Function processing multiple args
@NvimFunction('Sum')
async sum(args: number[]) {
return args.reduce((acc, val) => acc + val, 0);
}
// Function with range
@NvimFunction('ProcessRange', { range: [0, 0] })
async processRange(args: any[], range: [number, number]) {
const [start, end] = range;
return `Lines ${start} to ${end}`;
}
}
// Invoke from Neovim:
// :call GetTimestamp()
// :echo Double(21)
// :let result = Sum(1, 2, 3, 4)/**
* Method decorator for registering autocommands.
* Autocommands are triggered automatically by Neovim events.
*
* @param name - Event name (e.g., 'BufEnter', 'BufWritePre')
* @param options - Autocmd options
* @returns Method decorator
*/
function Autocmd(name: string, options: AutocmdOptions): MethodDecorator;
interface AutocmdOptions {
/** File pattern (required, see :help autocmd-pattern) */
pattern: string;
/** VimL expression to evaluate and pass as argument */
eval?: string;
/** Force synchronous execution (default: false) */
sync?: boolean;
}Usage Examples:
import { Plugin, Autocmd, NvimPlugin } from 'neovim';
@Plugin
export class AutocmdExamples {
private nvim: Neovim;
constructor(nvim: Neovim) {
this.nvim = nvim;
}
// Trigger on buffer enter
@Autocmd('BufEnter', { pattern: '*' })
async onBufEnter(filename: string) {
console.log('Entered buffer:', filename);
}
// Trigger for specific file types
@Autocmd('BufEnter', { pattern: '*.ts' })
async onTypescriptFile(filename: string) {
console.log('TypeScript file:', filename);
await this.nvim.command('set filetype=typescript');
}
// Trigger before save
@Autocmd('BufWritePre', { pattern: '*' })
async beforeSave(filename: string) {
console.log('About to save:', filename);
// Auto-format, lint, etc.
}
// Trigger after save
@Autocmd('BufWritePost', { pattern: '*.js' })
async afterJsSave(filename: string) {
console.log('Saved JavaScript file:', filename);
// Run tests, build, etc.
}
// Synchronous autocmd
@Autocmd('BufReadPre', { pattern: '*.md', sync: true })
beforeReadMarkdown(filename: string) {
console.log('Reading markdown:', filename);
}
// Autocmd with eval
@Autocmd('CursorMoved', { pattern: '*', eval: 'line(".")' })
async onCursorMoved(args: [number]) {
const lineNum = args[0];
// React to cursor position
}
}/**
* Plugin container class that manages command, function, and autocmd handlers.
* Used for manual plugin registration without decorators.
*/
class NvimPlugin {
/** Path to plugin file */
filename: string;
/** Neovim instance */
nvim: Neovim;
/** Plugin instance object */
instance: any;
/** Development mode flag */
dev: boolean;
/** Always reinitialize flag */
alwaysInit: boolean;
/** Registered autocmd handlers */
autocmds: { [index: string]: Handler };
/** Registered command handlers */
commands: { [index: string]: Handler };
/** Registered function handlers */
functions: { [index: string]: Handler };
/** All handler specifications */
specs: Spec[];
/** Whether to cache the plugin module */
shouldCacheModule: boolean;
/**
* Creates a new plugin instance.
*
* @param filename - Path to the plugin file
* @param plugin - Plugin class or factory function
* @param nvim - Neovim instance
*/
constructor(filename: string, plugin: any, nvim: Neovim);
/**
* Set plugin options.
*
* @param options - Plugin options
*/
setOptions(options: NvimPluginOptions): void;
/**
* Register an autocommand handler.
*
* @param name - Event name
* @param fn - Handler function or [object, method]
* @param options - Autocmd options
*/
registerAutocmd(name: string, fn: Function | [any, Function], options: AutocmdOptions): void;
/**
* Register a command handler.
*
* @param name - Command name
* @param fn - Handler function or [object, method]
* @param options - Command options
*/
registerCommand(name: string, fn: Function | [any, Function], options?: CommandOptions): void;
/**
* Register a function handler.
*
* @param name - Function name
* @param fn - Handler function or [object, method]
* @param options - Function options
*/
registerFunction(name: string, fn: Function | [any, Function], options?: NvimFunctionOptions): void;
/**
* Handle incoming RPC request.
*
* @param name - Handler name
* @param type - Handler type ('autocmd', 'command', 'function')
* @param args - Handler arguments
* @returns Handler result
*/
handleRequest(name: string, type: string, args: any[]): Promise<any>;
}
interface NvimPluginOptions {
/** Development mode (reload on each invocation) */
dev?: boolean;
/** Always reinitialize plugin instance */
alwaysInit?: boolean;
}
interface Handler {
fn: Function;
spec: Spec;
}
interface Spec {
type: string;
name: string;
sync?: boolean;
opts?: object;
}Usage Examples:
import { NvimPlugin, Neovim } from 'neovim';
// Functional plugin style (no decorators)
export default function(plugin: NvimPlugin) {
// Set options
plugin.setOptions({ dev: true });
// Register command
plugin.registerCommand('MyCommand', async (args: string[]) => {
console.log('Command executed with:', args);
}, { nargs: '*' });
// Register function
plugin.registerFunction('MyFunction', async (args: any[]) => {
return args[0] * 2;
}, { sync: false });
// Register autocmd
plugin.registerAutocmd('BufEnter', async (filename: string) => {
console.log('Buffer entered:', filename);
}, { pattern: '*.js' });
// Access Neovim instance
const nvim = plugin.nvim;
// Register with object method
const handler = {
async process(args: any[]) {
const buf = await nvim.buffer;
await buf.append(['Processed!']);
}
};
plugin.registerCommand('Process', [handler, handler.process], { nargs: '0' });
}/**
* Load a plugin from file path.
* Used by plugin host to discover and load remote plugins.
*
* @param filename - Path to plugin file or directory
* @param nvim - Neovim instance
* @param options - Load options
* @returns NvimPlugin instance or null on error
*/
function loadPlugin(filename: string, nvim: Neovim, options?: LoadPluginOptions): NvimPlugin | null;
interface LoadPluginOptions {
/** Whether to cache the module (default: true) */
cache?: boolean;
}Usage Examples:
import { loadPlugin, attach } from 'neovim';
import * as child_process from 'node:child_process';
const nvim_proc = child_process.spawn('nvim', ['--embed']);
const nvim = attach({ proc: nvim_proc });
// Load plugin
const plugin = loadPlugin('./my-plugin.js', nvim);
if (plugin) {
console.log('Plugin loaded:', plugin.filename);
console.log('Commands:', Object.keys(plugin.commands));
console.log('Functions:', Object.keys(plugin.functions));
console.log('Autocmds:', Object.keys(plugin.autocmds));
// Trigger command manually
await plugin.handleRequest('MyCommand', 'command', [['arg1', 'arg2']]);
}
// Load plugin without caching (for development)
const devPlugin = loadPlugin('./my-plugin.js', nvim, { cache: false });import { Plugin, Command, Function as NvimFunction, Autocmd, Neovim } from 'neovim';
@Plugin
export class HelloPlugin {
private nvim: Neovim;
constructor(nvim: Neovim) {
this.nvim = nvim;
}
@Command('Hello', { nargs: '0' })
async hello() {
await this.nvim.outWriteLine('Hello from plugin!');
}
@NvimFunction('AddNumbers')
async addNumbers(args: number[]) {
return args.reduce((a, b) => a + b, 0);
}
@Autocmd('BufEnter', { pattern: '*.txt' })
async onTextFile(filename: string) {
await this.nvim.command('setlocal spell');
}
}import { Plugin, Command, Function as NvimFunction, Autocmd, Neovim, Buffer } from 'neovim';
interface Note {
timestamp: number;
content: string;
}
@Plugin({ dev: true })
export class NotesPlugin {
private nvim: Neovim;
private notes: Map<string, Note[]> = new Map();
constructor(nvim: Neovim) {
this.nvim = nvim;
}
@Command('NoteAdd', { nargs: '*' })
async addNote(args: string[]) {
const content = args.join(' ');
const buf = await this.nvim.buffer;
const bufName = await buf.name;
if (!this.notes.has(bufName)) {
this.notes.set(bufName, []);
}
const notes = this.notes.get(bufName)!;
notes.push({
timestamp: Date.now(),
content
});
await this.nvim.outWriteLine(`Note added: ${content}`);
}
@Command('NoteList', { nargs: '0' })
async listNotes() {
const buf = await this.nvim.buffer;
const bufName = await buf.name;
const notes = this.notes.get(bufName) || [];
if (notes.length === 0) {
await this.nvim.outWriteLine('No notes for this buffer');
return;
}
await this.nvim.outWriteLine(`Notes for ${bufName}:`);
for (const note of notes) {
const date = new Date(note.timestamp).toLocaleString();
await this.nvim.outWriteLine(` [${date}] ${note.content}`);
}
}
@NvimFunction('NoteCount')
async noteCount() {
const buf = await this.nvim.buffer;
const bufName = await buf.name;
const notes = this.notes.get(bufName) || [];
return notes.length;
}
@Autocmd('BufDelete', { pattern: '*' })
async onBufferDelete(filename: string) {
this.notes.delete(filename);
}
}import { Plugin, Command, Autocmd, Neovim, Buffer } from 'neovim';
interface Diagnostic {
line: number;
col: number;
message: string;
severity: 'error' | 'warning' | 'info';
}
@Plugin
export class DiagnosticPlugin {
private nvim: Neovim;
private namespace: number | null = null;
private vtextNamespace: number | null = null;
constructor(nvim: Neovim) {
this.nvim = nvim;
}
async ensureNamespaces() {
if (this.namespace === null) {
this.namespace = await this.nvim.createNamespace('diagnostics');
this.vtextNamespace = await this.nvim.createNamespace('diagnostics-vtext');
}
}
@Command('DiagnosticsShow', { nargs: '0' })
async showDiagnostics() {
await this.ensureNamespaces();
const buf = await this.nvim.buffer;
const lines = await buf.lines;
// Simple linter: find TODO and FIXME
const diagnostics: Diagnostic[] = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line.includes('TODO')) {
const col = line.indexOf('TODO');
diagnostics.push({
line: i,
col,
message: 'TODO found',
severity: 'info'
});
}
if (line.includes('FIXME')) {
const col = line.indexOf('FIXME');
diagnostics.push({
line: i,
col,
message: 'FIXME found',
severity: 'warning'
});
}
if (line.includes('ERROR')) {
const col = line.indexOf('ERROR');
diagnostics.push({
line: i,
col,
message: 'Error marker found',
severity: 'error'
});
}
}
// Clear old diagnostics
await buf.clearNamespace({ nsId: this.namespace! });
await buf.clearNamespace({ nsId: this.vtextNamespace! });
// Show diagnostics
for (const diag of diagnostics) {
const hlGroup = {
error: 'ErrorMsg',
warning: 'WarningMsg',
info: 'InfoMsg'
}[diag.severity];
await buf.addHighlight({
hlGroup,
line: diag.line,
colStart: diag.col,
colEnd: diag.col + 10,
srcId: this.namespace!
});
await buf.setVirtualText(this.vtextNamespace!, diag.line, [
[` ${diag.message}`, hlGroup]
]);
}
await this.nvim.outWriteLine(`Found ${diagnostics.length} diagnostics`);
}
@Command('DiagnosticsClear', { nargs: '0' })
async clearDiagnostics() {
await this.ensureNamespaces();
const buf = await this.nvim.buffer;
await buf.clearNamespace({ nsId: this.namespace! });
await buf.clearNamespace({ nsId: this.vtextNamespace! });
await this.nvim.outWriteLine('Diagnostics cleared');
}
@Autocmd('BufWritePost', { pattern: '*' })
async onSave(filename: string) {
await this.showDiagnostics();
}
}import { NvimPlugin, Neovim } from 'neovim';
export default function(plugin: NvimPlugin) {
const nvim = plugin.nvim;
// Development mode
plugin.setOptions({ dev: true });
// Command: Open scratch buffer
plugin.registerCommand('Scratch', async () => {
const buf = await nvim.createBuffer(false, true);
await nvim.command(`buffer ${buf.id}`);
await buf.setOption('buftype', 'nofile');
await buf.setOption('bufhidden', 'hide');
await buf.setLines(['# Scratch Buffer', '', 'Start typing...']);
}, { nargs: '0' });
// Function: Get buffer info
plugin.registerFunction('BufferInfo', async () => {
const buf = await nvim.buffer;
const name = await buf.name;
const lines = await buf.length;
const modified = await buf.getOption('modified');
return {
name,
lines,
modified
};
});
// Autocmd: Auto-trim trailing whitespace
plugin.registerAutocmd('BufWritePre', async (filename) => {
const buf = await nvim.buffer;
const lines = await buf.lines;
const trimmed = lines.map(line => line.replace(/\s+$/, ''));
await buf.setLines(trimmed);
}, { pattern: '*.js,*.ts,*.py' });
}Install neovim package globally:
npm install -g neovimCreate plugin directory structure:
~/.config/nvim/rplugin/node/
├── my-plugin/
│ ├── package.json
│ ├── index.js
│ └── node_modules/Plugin must export default function:
export default function(plugin: NvimPlugin) {
// Register handlers
}Update remote plugin manifest:
:UpdateRemotePluginsRestart Neovim
// index.ts - plugin entry point
import { Plugin, Command, NvimPlugin, Neovim } from 'neovim';
// Using decorators
@Plugin
export class MyPlugin {
constructor(private nvim: Neovim) {}
@Command('MyCommand')
async myCommand() {
// Implementation
}
}
// Or using functional style
export default function(plugin: NvimPlugin) {
plugin.registerCommand('MyCommand', async () => {
// Implementation
});
}{
"name": "my-neovim-plugin",
"version": "1.0.0",
"main": "./index.js",
"engines": {
"node": ">=10"
},
"dependencies": {
"neovim": "^5.4.0"
}
}neovim package globally for remote plugins: npm install -g neovimrplugin/node/ directory on Neovim's runtimepath:UpdateRemotePlugins after adding/changing pluginsdev: true option during development to reload on each invocationsync: true for blocking behaviorpattern option:CommandName:call FunctionName(args)plugin.nvim to access Neovim API in functional pluginsFunction export is renamed as NvimFunction to avoid name conflictsalwaysInit: true to reinitialize on each call[object, method] tuples for method bindingloadPlugin() function by the plugin host