Skip to content

Lua Plugin API

John Donaghy edited this page Mar 11, 2026 · 1 revision

Lua Plugin API

VimCode embeds Lua 5.4 (via mlua, fully vendored — no system Lua required). Plugins have access to the vimcode global object.

Registration functions

Call these at the top level of your script (during load time):

-- Register a custom command (callable via :MyCommand args)
vimcode.command("MyCommand", function(args)
    -- args is a string containing everything after the command name
    vimcode.message("Got: " .. args)
end)

-- Register an event hook
vimcode.on("save", function(path)
    vimcode.message("Saved: " .. path)
end)

-- Register a key mapping
-- Modes: "n" (normal), "i" (insert), "v" (visual), "c" (command)
vimcode.keymap("n", "<leader>h", function()
    vimcode.message("Hello from keymap!")
end)

Core functions

vimcode.message(text)           -- Display a status bar message
vimcode.cwd()                   -- Get current working directory (string)
vimcode.command_run(cmd)        -- Execute a VimCode command (e.g., "w", "q", "split")

Buffer functions (vimcode.buf.*)

All line numbers are 1-indexed.

vimcode.buf.lines()                       -- All buffer lines as a table
vimcode.buf.line(n)                       -- Get line n (returns string or nil)
vimcode.buf.set_line(n, text)             -- Replace line n with text (undoable)
vimcode.buf.insert_line(n, text)          -- Insert new line before position n
vimcode.buf.delete_line(n)                -- Delete line n
vimcode.buf.line_count()                  -- Total number of lines
vimcode.buf.path()                        -- File path (string or nil for unnamed buffers)
vimcode.buf.cursor()                      -- Returns {line=N, col=M} (1-indexed)
vimcode.buf.set_cursor(line, col)         -- Move cursor to position (clamped)
vimcode.buf.annotate_line(n, text)        -- Add virtual text annotation to line n
vimcode.buf.clear_annotations()           -- Clear all line annotations
vimcode.buf.open_scratch(name, content, opts)  -- Open a scratch buffer
  -- opts (optional table): readonly=bool, filetype=string, split="vertical"|"horizontal"

Settings functions (vimcode.opt.*)

vimcode.opt.get("tabstop")      -- Query a setting value (returns string)
vimcode.opt.set("tabstop", "4") -- Set a setting value (applied after callback)

Available settings include: number, relativenumber, tabstop, shiftwidth, expandtab, autoindent, wrap, hlsearch, ignorecase, smartcase, scrolloff, cursorline, colorcolumn, textwidth, splitbelow, splitright, colorscheme, and more.

State functions (vimcode.state.*)

vimcode.state.mode()                              -- Current mode: "Normal", "Insert", "Visual", etc.
vimcode.state.filetype()                          -- Buffer language ID: "rust", "python", etc.
vimcode.state.register("a")                       -- Get register: {content="...", linewise=false} or nil
vimcode.state.set_register("a", "text", false)    -- Set register (char, content, linewise)
vimcode.state.mark("a")                           -- Get mark position: {line=N, col=M} or nil

Git functions (vimcode.git.*)

-- Blame
vimcode.git.blame_line(10)       -- {hash, author, date, relative_date, message, not_committed} or nil
vimcode.git.blame_file()         -- [{hash, author, ...}, ...] for entire buffer

-- File history
vimcode.git.log_file(20)         -- [{hash, message}, ...] for current file
vimcode.git.file_log_detailed(20)-- [{hash, author, date, message, stat}, ...]
vimcode.git.line_log(10, 20, 50) -- Commits touching line range [10,20], limit 50

-- Repository
vimcode.git.log(100)             -- Repo-wide commit log, limit 100
vimcode.git.show("abc123")       -- Full commit details (string or nil)
vimcode.git.diff_ref("main")     -- Diff against ref (string or nil)
vimcode.git.repo_root()          -- Repository root path (string or nil)
vimcode.git.branch()             -- Current branch name (string or nil)
vimcode.git.branches()           -- [{name, tracking, is_current}, ...]

-- Stash
vimcode.git.stash_list()         -- [{index, message, branch}, ...]
vimcode.git.stash_push("msg")    -- Push to stash (returns string)
vimcode.git.stash_pop(0)         -- Pop stash entry (returns string)
vimcode.git.stash_show(0)        -- Stash diff (string or nil)

Async shell execution

Run shell commands in a background thread with results delivered via event hooks:

-- Basic usage
vimcode.async_shell("git status", "my_result_event")

-- With options
vimcode.async_shell("grep -n pattern", "search_done", {
    stdin = "input text",   -- Optional: pipe to stdin
    cwd = "/path/to/dir"    -- Optional: working directory
})

-- Handle the result
vimcode.on("my_result_event", function(output)
    vimcode.message("Result: " .. output)
end)

Panel API (vimcode.panel.*)

Extensions can register custom sidebar panels that appear in the activity bar.

-- Register a custom sidebar panel (call at load time)
vimcode.panel.register("my_panel", {
    icon = "X",                            -- Single character for activity bar icon
    sections = {"Section A", "Section B"}, -- Named collapsible sections
    on_focus = function()                  -- Called when panel gains focus
        -- Populate sections here
    end
})

-- Populate a section with items
vimcode.panel.set_items("my_panel", "Section A", {
    {label = "Item 1", hint = "description", icon = "*", style = "normal"},
    {label = "Item 2", hint = "extra info",  icon = "+", style = "dim"},
    {label = "Item 3", hint = "important",   icon = "!", style = "bold"},
})

-- Parse event argument from panel_select/panel_action hooks
local info = vimcode.panel.parse_event(arg)
-- info = {panel="my_panel", section="Section A", index=0, label="Item 1", key="a"}

Panel navigation keys

Key Action
j / k Navigate items
Tab Expand/collapse section
Enter Fire panel_select event for current item
q / Escape Unfocus panel
Other keys Fire panel_action event with the key

Panel events

Event Argument When
panel_focus panel name Panel gains focus in sidebar
panel_select "panel:section:index:label" Enter pressed on item
panel_action "key:panel:section:index:label" Other key pressed on item

Item styles

Style Effect
"normal" Default foreground color
"dim" Muted/grey text
"bold" Highlighted/bright text

Comment style override

Override comment syntax for a language:

vimcode.set_comment_style("haskell", {
    line = "--",
    block_open = "{-",
    block_close = "-}"
})

Events reference

Event Argument When
save file path Before buffer is written to disk
BufWrite file path After buffer is written to disk
open file path File opened in editor
BufNew file path New buffer created
BufEnter file path Buffer/window entered
cursor_move "line,col" Cursor moves (Normal mode only)
VimEnter "" Editor initialization complete
InsertEnter mode name Entered Insert mode
InsertLeave mode name Left Insert mode
ModeChanged "Old->New" Any mode change (e.g., "Normal->Insert")
panel_focus panel name Extension panel gains focus
panel_select "panel:section:index:label" Enter pressed on extension panel item
panel_action "key:panel:section:index:label" Other key pressed on extension panel item
Custom shell output async_shell() callback event

Complete examples

Word count extension

-- ~/.config/vimcode/extensions/wordcount/wordcount.lua

vimcode.command("WordCount", function(_)
    local lines = vimcode.buf.lines()
    local count = 0
    for _, line in ipairs(lines) do
        for _ in line:gmatch("%S+") do
            count = count + 1
        end
    end
    vimcode.message("Word count: " .. count)
end)

vimcode.on("save", function(_)
    local lines = vimcode.buf.lines()
    local count = 0
    for _, line in ipairs(lines) do
        for _ in line:gmatch("%S+") do
            count = count + 1
        end
    end
    vimcode.message("Saved (" .. count .. " words)")
end)

With manifest:

name = "wordcount"
display_name = "Word Count"
description = "Word counting commands and save-time word count display"
scripts = ["wordcount.lua"]

Inline git blame

-- blame.lua — show inline blame annotations as you move the cursor

vimcode.on("cursor_move", function(pos)
    local line, _ = pos:match("(%d+),(%d+)")
    line = tonumber(line)
    if not line then return end

    local blame = vimcode.git.blame_line(line)
    if blame and not blame.not_committed then
        local text = blame.author .. "" .. blame.relative_date .. "" .. blame.message
        vimcode.buf.clear_annotations()
        vimcode.buf.annotate_line(line, text)
    else
        vimcode.buf.clear_annotations()
    end
end)

Auto-format on save

-- autoformat.lua — run LSP formatter on save for specific filetypes

local format_filetypes = { rust = true, python = true, go = true, javascript = true }

vimcode.on("save", function(path)
    local ft = vimcode.state.filetype()
    if format_filetypes[ft] then
        vimcode.command_run("Lformat")
    end
end)

Plugin management commands

:Plugin list               " Show all loaded plugins
:Plugin reload             " Reload all plugins from disk
:Plugin enable <name>      " Enable a disabled plugin
:Plugin disable <name>     " Disable a plugin (persisted in settings)

Security

Plugins have unrestricted file and process access (same trust model as Neovim). Only install plugins you trust.