Neovim Integration

Neovim exposes a rich Lua API. Master it to build powerful configurations and plugins.

vim Namespace

vim.opt (Options)

-- Set options
vim.opt.number = true
vim.opt.relativenumber = true
vim.opt.tabstop = 4
vim.opt.shiftwidth = 4
vim.opt.expandtab = true
vim.opt.smartindent = true
vim.opt.wrap = false
vim.opt.ignorecase = true
vim.opt.smartcase = true
vim.opt.termguicolors = true
vim.opt.signcolumn = "yes"
vim.opt.updatetime = 50
vim.opt.colorcolumn = "80"

-- Get option value
local tabstop = vim.opt.tabstop:get()

-- List options (append/remove)
vim.opt.wildignore:append({ "*.pyc", "__pycache__" })
vim.opt.wildignore:remove("*.pyc")

-- Map options
vim.opt.listchars = { tab = "» ", trail = "·", nbsp = "␣" }

-- Check if option is set
if vim.opt.number:get() then
    print("Numbers enabled")
end

vim.g (Global Variables)

-- Set global variables (like let g:var in Vimscript)
vim.g.mapleader = " "
vim.g.maplocalleader = " "

-- Plugin variables
vim.g.loaded_netrw = 1
vim.g.loaded_netrwPlugin = 1

-- Get variable
local leader = vim.g.mapleader

-- Unset
vim.g.some_variable = nil

vim.bo / vim.wo (Buffer/Window Options)

-- Buffer-local options
vim.bo.filetype = "lua"
vim.bo.buftype = "nofile"

-- Window-local options
vim.wo.wrap = false
vim.wo.number = true

-- For specific buffer/window
vim.bo[bufnr].modifiable = false
vim.wo[winnr].cursorline = true

vim.fn (Vimscript Functions)

-- Call Vimscript functions
local home = vim.fn.expand("~")
local exists = vim.fn.filereadable("/etc/hosts")
local cwd = vim.fn.getcwd()
local lines = vim.fn.readfile("/etc/hosts")

-- System commands
local result = vim.fn.system("ls -la")
local lines = vim.fn.systemlist("ls -la")

-- Input
local name = vim.fn.input("Enter name: ")
local confirmed = vim.fn.confirm("Delete?", "&Yes\n&No") == 1

-- Clipboard
vim.fn.setreg("+", "copied text")
local clipboard = vim.fn.getreg("+")

Keymaps

vim.keymap.set

local map = vim.keymap.set

-- Basic mapping
map("n", "<leader>w", "<cmd>w<CR>", { desc = "Save file" })
map("n", "<leader>q", "<cmd>q<CR>", { desc = "Quit" })

-- With callback
map("n", "<leader>e", function()
    vim.cmd("Explore")
end, { desc = "Open explorer" })

-- Multiple modes
map({ "n", "v" }, "<leader>y", '"+y', { desc = "Yank to clipboard" })

-- Buffer-local
map("n", "<leader>f", function()
    vim.lsp.buf.format()
end, { buffer = true, desc = "Format buffer" })

-- Options
map("n", "j", "gj", {
    noremap = true,  -- Don't remap
    silent = true,   -- Don't echo
    expr = false,    -- Not an expression
    desc = "Move down visual line"
})

-- Delete mapping
vim.keymap.del("n", "<leader>w")

Keymap Patterns

-- Leader mappings
local function setup_keymaps()
    local map = vim.keymap.set

    -- File operations
    map("n", "<leader>ff", "<cmd>Telescope find_files<CR>", { desc = "Find files" })
    map("n", "<leader>fg", "<cmd>Telescope live_grep<CR>", { desc = "Live grep" })
    map("n", "<leader>fb", "<cmd>Telescope buffers<CR>", { desc = "Buffers" })

    -- LSP
    map("n", "gd", vim.lsp.buf.definition, { desc = "Go to definition" })
    map("n", "gr", vim.lsp.buf.references, { desc = "References" })
    map("n", "K", vim.lsp.buf.hover, { desc = "Hover" })
    map("n", "<leader>ca", vim.lsp.buf.code_action, { desc = "Code action" })
    map("n", "<leader>rn", vim.lsp.buf.rename, { desc = "Rename" })

    -- Diagnostics
    map("n", "[d", vim.diagnostic.goto_prev, { desc = "Previous diagnostic" })
    map("n", "]d", vim.diagnostic.goto_next, { desc = "Next diagnostic" })
    map("n", "<leader>d", vim.diagnostic.open_float, { desc = "Show diagnostic" })
end

Autocommands

vim.api.nvim_create_autocmd

-- Single autocommand
vim.api.nvim_create_autocmd("BufWritePre", {
    pattern = "*.lua",
    callback = function()
        vim.lsp.buf.format()
    end,
    desc = "Format Lua on save"
})

-- Multiple events
vim.api.nvim_create_autocmd({ "BufEnter", "BufWinEnter" }, {
    pattern = "*.md",
    callback = function()
        vim.opt_local.wrap = true
        vim.opt_local.spell = true
    end,
})

-- With group (for clearing)
local group = vim.api.nvim_create_augroup("MyGroup", { clear = true })

vim.api.nvim_create_autocmd("FileType", {
    group = group,
    pattern = "python",
    callback = function(args)
        vim.bo[args.buf].tabstop = 4
    end,
})

-- Clear group
vim.api.nvim_clear_autocmds({ group = "MyGroup" })

Common Autocommands

local augroup = vim.api.nvim_create_augroup
local autocmd = vim.api.nvim_create_autocmd

-- Highlight on yank
autocmd("TextYankPost", {
    group = augroup("HighlightYank", { clear = true }),
    callback = function()
        vim.highlight.on_yank({ timeout = 200 })
    end,
})

-- Remove trailing whitespace
autocmd("BufWritePre", {
    group = augroup("TrimWhitespace", { clear = true }),
    pattern = "*",
    command = [[%s/\s\+$//e]],
})

-- Restore cursor position
autocmd("BufReadPost", {
    group = augroup("RestoreCursor", { clear = true }),
    callback = function()
        local mark = vim.api.nvim_buf_get_mark(0, '"')
        local line_count = vim.api.nvim_buf_line_count(0)
        if mark[1] > 0 and mark[1] <= line_count then
            vim.api.nvim_win_set_cursor(0, mark)
        end
    end,
})

-- Auto-resize splits
autocmd("VimResized", {
    group = augroup("AutoResize", { clear = true }),
    command = "wincmd =",
})

User Commands

vim.api.nvim_create_user_command

-- Simple command
vim.api.nvim_create_user_command("Hello", function()
    print("Hello, World!")
end, { desc = "Say hello" })

-- With arguments
vim.api.nvim_create_user_command("Greet", function(opts)
    print("Hello, " .. opts.args)
end, {
    nargs = 1,  -- Exactly one argument
    desc = "Greet someone"
})

-- With completion
vim.api.nvim_create_user_command("Config", function(opts)
    vim.cmd("edit ~/.config/nvim/" .. opts.args)
end, {
    nargs = 1,
    complete = function()
        return { "init.lua", "lua/plugins/init.lua", "lua/config/keymaps.lua" }
    end,
})

-- Range command
vim.api.nvim_create_user_command("FormatRange", function(opts)
    vim.lsp.buf.format({
        range = {
            ["start"] = { opts.line1, 0 },
            ["end"] = { opts.line2, 0 },
        }
    })
end, { range = true })

-- Buffer-local command
vim.api.nvim_buf_create_user_command(0, "LocalCmd", function()
    print("Buffer-local command")
end, {})

vim.api (Low-Level API)

Buffer Operations

-- Get current buffer
local bufnr = vim.api.nvim_get_current_buf()

-- Get buffer lines
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)

-- Set buffer lines
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, {"line1", "line2"})

-- Get buffer name
local name = vim.api.nvim_buf_get_name(bufnr)

-- Create new buffer
local new_buf = vim.api.nvim_create_buf(true, false)  -- listed, scratch

-- Delete buffer
vim.api.nvim_buf_delete(bufnr, { force = true })

Window Operations

-- Get current window
local winnr = vim.api.nvim_get_current_win()

-- Get window buffer
local bufnr = vim.api.nvim_win_get_buf(winnr)

-- Set window buffer
vim.api.nvim_win_set_buf(winnr, bufnr)

-- Get cursor position
local pos = vim.api.nvim_win_get_cursor(winnr)  -- {row, col}

-- Set cursor
vim.api.nvim_win_set_cursor(winnr, { 10, 0 })

-- Create split
vim.api.nvim_command("vsplit")
local new_win = vim.api.nvim_get_current_win()

Floating Windows

-- Create floating window
local buf = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_lines(buf, 0, -1, false, {
    "Floating window content",
    "Line 2",
    "Line 3"
})

local width = 40
local height = 10
local win = vim.api.nvim_open_win(buf, true, {
    relative = "editor",
    width = width,
    height = height,
    col = (vim.o.columns - width) / 2,
    row = (vim.o.lines - height) / 2,
    style = "minimal",
    border = "rounded",
})

-- Close floating window
vim.api.nvim_win_close(win, true)

vim.notify (Notifications)

-- Basic notification
vim.notify("File saved")

-- With level
vim.notify("Warning!", vim.log.levels.WARN)
vim.notify("Error occurred", vim.log.levels.ERROR)
vim.notify("Info message", vim.log.levels.INFO)

-- Levels: TRACE, DEBUG, INFO, WARN, ERROR, OFF

-- With title (requires nvim-notify or similar)
vim.notify("Message", vim.log.levels.INFO, { title = "My Plugin" })

vim.schedule

-- Schedule function to run in main loop
-- Required when calling API from callbacks
vim.schedule(function()
    vim.api.nvim_buf_set_lines(0, 0, -1, false, {"Updated"})
end)

-- Deferred execution
vim.defer_fn(function()
    print("After 1 second")
end, 1000)

-- vim.schedule_wrap for callbacks
local safe_callback = vim.schedule_wrap(function()
    vim.notify("Safe to call API here")
end)

Plugin Development

Plugin Structure

-- lua/myplugin/init.lua
local M = {}

M.config = {
    enabled = true,
    option = "default"
}

function M.setup(opts)
    M.config = vim.tbl_deep_extend("force", M.config, opts or {})

    if not M.config.enabled then
        return
    end

    M._setup_keymaps()
    M._setup_commands()
    M._setup_autocmds()
end

function M._setup_keymaps()
    vim.keymap.set("n", "<leader>mp", M.action, { desc = "My plugin action" })
end

function M._setup_commands()
    vim.api.nvim_create_user_command("MyPlugin", M.action, {})
end

function M._setup_autocmds()
    local group = vim.api.nvim_create_augroup("MyPlugin", { clear = true })
    vim.api.nvim_create_autocmd("BufEnter", {
        group = group,
        callback = M._on_buf_enter,
    })
end

function M.action()
    vim.notify("Plugin action!", vim.log.levels.INFO, { title = "MyPlugin" })
end

function M._on_buf_enter()
    -- Handle buffer enter
end

return M

Usage

-- In user config
require("myplugin").setup({
    enabled = true,
    option = "custom"
})

Next Module

Practical Patterns - Real patterns from domus-nvim.