Drill 04: Plugin Development

Module structure, lazy.nvim plugin specs, and configuration patterns.

Reference code for understanding domus-nvim structure.

Run This Drill

bash ~/atelier/_bibliotheca/domus-captures/docs/modules/ROOT/examples/lua-drills/04-plugins.sh

Drill Script

#!/bin/bash
# LUA DRILL 04: PLUGIN DEVELOPMENT
# Topics: Module structure, lazy.nvim patterns, configuration

echo "=================================================================="
echo "             LUA DRILL 04: PLUGIN DEVELOPMENT                    "
echo "=================================================================="
echo ""
echo "Understanding Neovim plugin/config structure"
echo ""

# ---------------------------------------------------------------------------
echo "------------------------------------------------------------------"
echo "DRILL 4.1: MODULE STRUCTURE"
echo "How require() works in Neovim"
echo "------------------------------------------------------------------"
echo ""
cat << 'NVIMEOF'
-- Neovim looks for modules in runtimepath/lua/

-- ~/.config/nvim/lua/mymodule.lua
-- Can be required as:
require('mymodule')

-- ~/.config/nvim/lua/mymodule/init.lua
-- Can also be required as:
require('mymodule')

-- ~/.config/nvim/lua/mymodule/submodule.lua
-- Required as:
require('mymodule.submodule')

-- TYPICAL DOMUS-NVIM STRUCTURE:
-- lua/
-- β”œβ”€β”€ domus/
-- β”‚   β”œβ”€β”€ init.lua           -- require('domus')
-- β”‚   β”œβ”€β”€ options.lua        -- require('domus.options')
-- β”‚   β”œβ”€β”€ keymaps.lua        -- require('domus.keymaps')
-- β”‚   └── plugins/
-- β”‚       β”œβ”€β”€ init.lua       -- require('domus.plugins')
-- β”‚       β”œβ”€β”€ specs/         -- plugin specs for lazy.nvim
-- β”‚       β”‚   β”œβ”€β”€ editor.lua
-- β”‚       β”‚   β”œβ”€β”€ ui.lua
-- β”‚       β”‚   └── lsp.lua
-- β”‚       └── config/        -- plugin configurations
-- β”‚           β”œβ”€β”€ lsp/
-- β”‚           β”‚   └── init.lua
-- β”‚           └── treesitter.lua

-- In init.lua:
require('domus')  -- loads lua/domus/init.lua
NVIMEOF
echo ""

# ---------------------------------------------------------------------------
echo "------------------------------------------------------------------"
echo "DRILL 4.2: LAZY.NVIM PLUGIN SPECS"
echo "The modern plugin manager"
echo "------------------------------------------------------------------"
echo ""
cat << 'NVIMEOF'
-- ~/.config/nvim/lua/domus/plugins/specs/editor.lua

return {
    -- Simple plugin (just install)
    "tpope/vim-sleuth",

    -- Plugin with configuration
    {
        "folke/which-key.nvim",
        event = "VeryLazy",  -- lazy load
        config = function()
            require("which-key").setup({})
        end,
    },

    -- Plugin with opts (auto-calls setup)
    {
        "numToStr/Comment.nvim",
        event = {"BufReadPre", "BufNewFile"},
        opts = {
            -- passed to require('Comment').setup(opts)
        },
    },

    -- Plugin with dependencies
    {
        "nvim-telescope/telescope.nvim",
        branch = "0.1.x",
        dependencies = {
            "nvim-lua/plenary.nvim",
            {"nvim-telescope/telescope-fzf-native.nvim", build = "make"},
        },
        config = function()
            local telescope = require("telescope")
            telescope.setup({})
            telescope.load_extension("fzf")
        end,
    },

    -- Conditional loading
    {
        "epwalsh/obsidian.nvim",
        cond = vim.fn.isdirectory("/data/data/com.termux") == 0,  -- disable on Termux
        ft = "markdown",
        opts = {
            workspaces = {{name = "notes", path = "~/notes"}},
        },
    },

    -- Plugin with keymaps
    {
        "lewis6991/gitsigns.nvim",
        event = {"BufReadPre", "BufNewFile"},
        opts = {
            on_attach = function(bufnr)
                local gs = package.loaded.gitsigns
                local keymap = vim.keymap.set

                keymap('n', ']h', gs.next_hunk, {buffer = bufnr, desc = 'Next hunk'})
                keymap('n', '[h', gs.prev_hunk, {buffer = bufnr, desc = 'Prev hunk'})
            end
        },
    },
}
NVIMEOF
echo ""

# ---------------------------------------------------------------------------
echo "------------------------------------------------------------------"
echo "DRILL 4.3: CONFIG FUNCTION PATTERNS"
echo "How to configure plugins"
echo "------------------------------------------------------------------"
echo ""
cat << 'NVIMEOF'
-- Pattern 1: Inline config
{
    "plugin/name",
    config = function()
        require("plugin").setup({
            option1 = true,
            option2 = "value",
        })
    end,
}

-- Pattern 2: opts (cleaner for simple config)
{
    "plugin/name",
    opts = {
        option1 = true,
        option2 = "value",
    },
}

-- Pattern 3: External config file
{
    "plugin/name",
    config = function()
        require("domus.plugins.config.plugin")
    end,
}

-- Then in lua/domus/plugins/config/plugin.lua:
local M = {}

M.setup = function()
    require("plugin").setup({
        option1 = true,
    })
end

M.setup()
return M

-- Pattern 4: opts function (access to plugin defaults)
{
    "plugin/name",
    opts = function(_, opts)
        opts.option1 = true
        opts.custom = {nested = true}
        return opts
    end,
}
NVIMEOF
echo ""

# ---------------------------------------------------------------------------
echo "------------------------------------------------------------------"
echo "DRILL 4.4: LAZY LOADING"
echo "Load plugins only when needed"
echo "------------------------------------------------------------------"
echo ""
cat << 'NVIMEOF'
-- Load on events
{
    "plugin/name",
    event = "VeryLazy",              -- after UI loads
    event = "BufReadPre",            -- before reading buffer
    event = {"BufReadPre", "BufNewFile"},
    event = "InsertEnter",           -- when entering insert mode
}

-- Load on filetype
{
    "rust-lang/rust.vim",
    ft = "rust",
}

-- Load on command
{
    "tpope/vim-fugitive",
    cmd = {"Git", "Gstatus", "Gblame"},
}

-- Load on keymap
{
    "nvim-telescope/telescope.nvim",
    keys = {
        {"<leader>ff", "<cmd>Telescope find_files<cr>", desc = "Find files"},
        {"<leader>fg", "<cmd>Telescope live_grep<cr>", desc = "Live grep"},
    },
}

-- Load as dependency (loaded when parent loads)
{
    "hrsh7th/nvim-cmp",
    dependencies = {
        "hrsh7th/cmp-nvim-lsp",  -- loaded with nvim-cmp
        "hrsh7th/cmp-buffer",
    },
}

-- Conditional (don't load at all if false)
{
    "plugin/name",
    cond = vim.fn.has("gui_running") == 1,
    cond = function() return vim.fn.executable("node") == 1 end,
}
NVIMEOF
echo ""

# ---------------------------------------------------------------------------
echo "------------------------------------------------------------------"
echo "DRILL 4.5: WRITING A SIMPLE PLUGIN"
echo "Module that provides functionality"
echo "------------------------------------------------------------------"
echo ""
cat << 'NVIMEOF'
-- lua/domus/plugins/config/custom/init.lua

local M = {}

M.defaults = {
    prefix = "[INFO]",
    log_level = "info",
}

M.options = {}

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

M.log = function(msg)
    print(M.options.prefix .. " " .. msg)
end

M.get_stats = function()
    return {
        buffers = #vim.api.nvim_list_bufs(),
        windows = #vim.api.nvim_list_wins(),
        cwd = vim.fn.getcwd(),
    }
end

-- Create commands
vim.api.nvim_create_user_command("MyLog", function(opts)
    M.log(opts.args)
end, {nargs = 1})

vim.api.nvim_create_user_command("MyStats", function()
    print(vim.inspect(M.get_stats()))
end, {})

return M

-- Usage:
-- require('domus.plugins.config.custom').setup({prefix = "[DEBUG]"})
-- require('domus.plugins.config.custom').log("Hello!")
-- :MyLog test message
-- :MyStats
NVIMEOF
echo ""

# ---------------------------------------------------------------------------
echo "------------------------------------------------------------------"
echo "YOUR TURN - TRY THESE:"
echo "------------------------------------------------------------------"
echo ""
echo "1. Check lazy.nvim loaded plugins:"
echo "   :lua print(vim.inspect(require('lazy').plugins()))"
echo ""
echo "2. Reload a module:"
echo "   :lua package.loaded['mymodule'] = nil; require('mymodule')"
echo ""
echo "3. Create simple command:"
echo "   :lua vim.api.nvim_create_user_command('Test', function() print('Works!') end, {})"
echo "   :Test"
echo ""
echo "------------------------------------------------------------------"
echo "KEY TAKEAWAYS:"
echo "1. Modules in lua/ dir, require('name') or require('dir.name')"
echo "2. lazy.nvim: opts = {} auto-calls setup(opts)"
echo "3. Use events/ft/cmd/keys for lazy loading"
echo "4. cond = false to skip loading entirely"
echo "5. Return M pattern for module exports"
echo "------------------------------------------------------------------"