vim

Log | Files | Refs

commit c72fd19497bf7ce0668dec11eadeb7821e342d02
Author: Chris Roberts <chris.roberts@learningunix.net>
Date:   Wed,  6 May 2026 18:47:09 -0500

Initial Neovim configuration

Full Neovim 0.12 setup with LSP, autocompletion, auto-pairs, linting,
and formatting for Python, Bash, YAML, Ansible, and OpenTofu.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Diffstat:
ACLAUDE.md | 93+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainit.lua | 21+++++++++++++++++++++
Alazy-lock.json | 29+++++++++++++++++++++++++++++
Alua/core/autocmds.lua | 33+++++++++++++++++++++++++++++++++
Alua/core/keymaps.lua | 47+++++++++++++++++++++++++++++++++++++++++++++++
Alua/core/options.lua | 45+++++++++++++++++++++++++++++++++++++++++++++
Alua/plugins/autopairs.lua | 23+++++++++++++++++++++++
Alua/plugins/completion.lua | 92+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alua/plugins/lint.lua | 47+++++++++++++++++++++++++++++++++++++++++++++++
Alua/plugins/lsp.lua | 93+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alua/plugins/treesitter.lua | 17+++++++++++++++++
Alua/plugins/ui.lua | 62++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
12 files changed, 602 insertions(+), 0 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md @@ -0,0 +1,93 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What this repo is + +Neovim configuration for daily use with Python, Bash/Shell, Ansible, OpenTofu, and YAML. +This directory is symlinked to `~/.config/nvim/`. + +## Install prerequisites + +```bash +# Core +sudo pacman -S neovim nodejs npm + +# System linters and formatters (not managed by Mason) +sudo pacman -S shellcheck shfmt python-lsp-server python-ruff ansible-lint yamllint + +# tree-sitter CLI (required by nvim-treesitter to compile yaml/toml/json/hcl parsers) +# Neovim 0.12 bundles parsers for bash, lua, python, markdown, vim, vimdoc — no CLI needed for those +sudo pacman -S tree-sitter-cli + +# Optional: stylua (Lua formatter for this config) +sudo pacman -S stylua +``` + +## First launch + +Open `nvim` — lazy.nvim bootstraps itself, installs all plugins, then Mason installs LSP servers. +Run `:MasonInstall prettier` manually if YAML formatting is needed. + +## Structure + +``` +init.lua -- entry point: loads core modules, bootstraps lazy.nvim +lua/core/ + options.lua -- vim options (tabs, search, clipboard, etc.) + keymaps.lua -- all non-plugin keymaps + autocmds.lua -- filetype detection (yaml.ansible), yank highlight, trim whitespace +lua/plugins/ + treesitter.lua -- syntax highlighting (python, bash, yaml, hcl, etc.) + lsp.lua -- mason + mason-lspconfig + nvim-lspconfig (all servers) + completion.lua -- nvim-cmp + LuaSnip + friendly-snippets + autopairs.lua -- nvim-autopairs (auto-close brackets/quotes) + lint.lua -- nvim-lint (linting) + conform.nvim (formatting) + ui.lua -- catppuccin, lualine, nvim-tree, telescope, gitsigns, which-key +``` + +## LSP servers (auto-installed by Mason) + +| Language | Server | Notes | +|---|---|---| +| Python | `pylsp` | Uses ruff plugin for linting | +| Bash/Shell | `bashls` | Requires Node | +| YAML | `yamlls` | Schema store enabled | +| Ansible | `ansiblels` | Activates on `yaml.ansible` filetype | +| OpenTofu/Terraform | `terraformls` | Works with both | +| Lua | `lua_ls` | Configured for Neovim globals | + +## Ansible file detection + +Files matching `*/tasks/**/*.yml`, `*/handlers/**/*.yml`, `*/roles/**/*.yml`, `*/playbooks/**/*.yml`, +`*/group_vars/**`, `*/host_vars/**`, `site.yml`, `playbook.yml` are automatically set to the +`yaml.ansible` filetype, which activates `ansiblels` instead of `yamlls`. + +For project-level detection, create a `.ansible-lint` file at the project root. + +## Key mappings (leader = Space) + +| Key | Action | +|---|---| +| `<leader>ff` | Find files (Telescope) | +| `<leader>fg` | Live grep | +| `<leader>fb` | Open buffers | +| `<leader>e` | Toggle file explorer | +| `<leader>w` | Save | +| `<leader>q` | Quit | +| `<leader>cf` | Format file | +| `<leader>rn` | Rename symbol (LSP) | +| `<leader>ca` | Code action (LSP) | +| `<leader>d` | Show diagnostic float | +| `[d` / `]d` | Prev/next diagnostic | +| `gd` | Go to definition | +| `K` | Hover documentation | +| `<Tab>` | Next completion / jump snippet | + +## Adding new language support + +1. Add the LSP server name to `ensure_installed` in `lua/plugins/lsp.lua` +2. Add a `lspconfig.<server>.setup()` call in the same file +3. Add linter entries to `lint.linters_by_ft` in `lua/plugins/lint.lua` +4. Add formatter entries to `formatters_by_ft` in `lua/plugins/lint.lua` +5. Add the treesitter grammar to `ensure_installed` in `lua/plugins/treesitter.lua` diff --git a/init.lua b/init.lua @@ -0,0 +1,21 @@ +vim.g.mapleader = " " +vim.g.maplocalleader = " " + +require("core.options") +require("core.keymaps") +require("core.autocmds") + +-- Bootstrap lazy.nvim +local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim" +if not vim.uv.fs_stat(lazypath) then + vim.fn.system({ + "git", "clone", "--filter=blob:none", + "https://github.com/folke/lazy.nvim.git", + "--branch=stable", lazypath, + }) +end +vim.opt.rtp:prepend(lazypath) + +require("lazy").setup("plugins", { + change_detection = { notify = false }, +}) diff --git a/lazy-lock.json b/lazy-lock.json @@ -0,0 +1,29 @@ +{ + "LuaSnip": { "branch": "master", "commit": "a62e1083a3cfe8b6b206e7d3d33a51091df25357" }, + "catppuccin": { "branch": "main", "commit": "426dbebe06b5c69fd846ceb17b42e12f890aedf1" }, + "cmp-buffer": { "branch": "main", "commit": "b74fab3656eea9de20a9b8116afa3cfc4ec09657" }, + "cmp-cmdline": { "branch": "main", "commit": "d126061b624e0af6c3a556428712dd4d4194ec6d" }, + "cmp-nvim-lsp": { "branch": "main", "commit": "cbc7b02bb99fae35cb42f514762b89b5126651ef" }, + "cmp-path": { "branch": "main", "commit": "c642487086dbd9a93160e1679a1327be111cbc25" }, + "cmp_luasnip": { "branch": "master", "commit": "98d9cb5c2c38532bd9bdb481067b20fea8f32e90" }, + "conform.nvim": { "branch": "master", "commit": "dca1a190aa85f9065979ef35802fb77131911106" }, + "fidget.nvim": { "branch": "main", "commit": "889e2e96edef4e144965571d46f7a77bcc4d0ddf" }, + "friendly-snippets": { "branch": "main", "commit": "6cd7280adead7f586db6fccbd15d2cac7e2188b9" }, + "gitsigns.nvim": { "branch": "main", "commit": "dd3f588bacbeb041be6facf1742e42097f62165d" }, + "indent-blankline.nvim": { "branch": "master", "commit": "d28a3f70721c79e3c5f6693057ae929f3d9c0a03" }, + "lazy.nvim": { "branch": "main", "commit": "306a05526ada86a7b30af95c5cc81ffba93fef97" }, + "lualine.nvim": { "branch": "master", "commit": "131a558e13f9f28b15cd235557150ccb23f89286" }, + "mason-lspconfig.nvim": { "branch": "main", "commit": "0c2823e0418f3d9230ff8b201c976e84de1cb401" }, + "mason.nvim": { "branch": "main", "commit": "cb8445f8ce85d957416c106b780efd51c6298f89" }, + "nvim-autopairs": { "branch": "master", "commit": "59bce2eef357189c3305e25bc6dd2d138c1683f5" }, + "nvim-cmp": { "branch": "main", "commit": "a1d504892f2bc56c2e79b65c6faded2fd21f3eca" }, + "nvim-lint": { "branch": "master", "commit": "eab58b48eb11d7745c11c505e0f3057165902461" }, + "nvim-lspconfig": { "branch": "master", "commit": "31026a13eefb20681124706a79fc1df6bf11ab27" }, + "nvim-tree.lua": { "branch": "master", "commit": "f9bfc0059eca24546b69a7006110463da4808f8f" }, + "nvim-treesitter": { "branch": "main", "commit": "4916d6592ede8c07973490d9322f187e07dfefac" }, + "nvim-web-devicons": { "branch": "master", "commit": "4fc505ac7bd7692824a142e96e5f529c133862f8" }, + "plenary.nvim": { "branch": "master", "commit": "74b06c6c75e4eeb3108ec01852001636d85a932b" }, + "telescope-fzf-native.nvim": { "branch": "main", "commit": "b25b749b9db64d375d782094e2b9dce53ad53a40" }, + "telescope.nvim": { "branch": "master", "commit": "f04ab730b8f9c6bf3f54a206d0dcddfd70c52d59" }, + "which-key.nvim": { "branch": "main", "commit": "3aab2147e74890957785941f0c1ad87d0a44c15a" } +} diff --git a/lua/core/autocmds.lua b/lua/core/autocmds.lua @@ -0,0 +1,33 @@ +-- Detect Ansible YAML files and set yaml.ansible filetype +-- so ansiblels activates instead of (or alongside) yamlls +vim.api.nvim_create_autocmd({ "BufNewFile", "BufRead" }, { + pattern = { + "*/tasks/**/*.yml", "*/tasks/**/*.yaml", + "*/handlers/**/*.yml", "*/handlers/**/*.yaml", + "*/roles/**/*.yml", "*/roles/**/*.yaml", + "*/playbooks/**/*.yml", "*/playbooks/**/*.yaml", + "*/group_vars/**", "*/host_vars/**", + "**/site.yml", "**/site.yaml", + "**/playbook.yml", "**/playbook.yaml", + }, + callback = function() + vim.bo.filetype = "yaml.ansible" + end, +}) + +-- Highlight on yank +vim.api.nvim_create_autocmd("TextYankPost", { + callback = function() + vim.highlight.on_yank() + end, +}) + +-- Remove trailing whitespace on save +vim.api.nvim_create_autocmd("BufWritePre", { + pattern = { "*.py", "*.sh", "*.yml", "*.yaml", "*.tf", "*.lua" }, + callback = function() + local pos = vim.api.nvim_win_get_cursor(0) + vim.cmd([[%s/\s\+$//e]]) + vim.api.nvim_win_set_cursor(0, pos) + end, +}) diff --git a/lua/core/keymaps.lua b/lua/core/keymaps.lua @@ -0,0 +1,47 @@ +local map = vim.keymap.set + +-- Split navigation +map("n", "<C-h>", "<C-w>h") +map("n", "<C-l>", "<C-w>l") +map("n", "<C-j>", "<C-w>j") +map("n", "<C-k>", "<C-w>k") + +-- Natural line movement +map({ "n", "x" }, "j", "v:count == 0 ? 'gj' : 'j'", { expr = true, silent = true }) +map({ "n", "x" }, "k", "v:count == 0 ? 'gk' : 'k'", { expr = true, silent = true }) + +-- Escape clears search highlight +map("n", "<Esc>", "<cmd>nohlsearch<CR>") + +-- File ops +map("n", "<leader>w", "<cmd>write<CR>", { desc = "Save" }) +map("n", "<leader>q", "<cmd>quit<CR>", { desc = "Quit" }) + +-- Buffer navigation +map("n", "<S-h>", "<cmd>bprevious<CR>", { desc = "Prev buffer" }) +map("n", "<S-l>", "<cmd>bnext<CR>", { desc = "Next buffer" }) + +-- Stay in visual mode after indent +map("v", "<", "<gv") +map("v", ">", ">gv") + +-- Move lines +map("n", "<A-j>", "<cmd>m .+1<CR>==") +map("n", "<A-k>", "<cmd>m .-2<CR>==") +map("v", "<A-j>", ":m '>+1<CR>gv=gv") +map("v", "<A-k>", ":m '<-2<CR>gv=gv") + +-- Telescope +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" }) +map("n", "<leader>fr", "<cmd>Telescope oldfiles<CR>", { desc = "Recent files" }) + +-- Diagnostics +map("n", "<leader>d", vim.diagnostic.open_float, { desc = "Show diagnostic" }) +map("n", "[d", vim.diagnostic.goto_prev, { desc = "Prev diagnostic" }) +map("n", "]d", vim.diagnostic.goto_next, { desc = "Next diagnostic" }) +map("n", "<leader>dl", vim.diagnostic.setloclist, { desc = "Diagnostic list" }) + +-- File explorer +map("n", "<leader>e", "<cmd>NvimTreeToggle<CR>", { desc = "Explorer" }) diff --git a/lua/core/options.lua b/lua/core/options.lua @@ -0,0 +1,45 @@ +local opt = vim.opt + +opt.number = true +opt.relativenumber = true + +opt.tabstop = 2 +opt.shiftwidth = 2 +opt.expandtab = true +opt.autoindent = true +opt.smartindent = true + +opt.wrap = false +opt.scrolloff = 8 + +opt.ignorecase = true +opt.smartcase = true +opt.hlsearch = false +opt.incsearch = true + +opt.termguicolors = true +opt.signcolumn = "yes" +opt.cursorline = true +opt.updatetime = 250 +opt.timeoutlen = 300 + +opt.mouse = "a" +opt.clipboard = "unnamedplus" +opt.splitbelow = true +opt.splitright = true +opt.undofile = true +opt.swapfile = false +opt.backup = false + +opt.completeopt = "menu,menuone,noselect" +opt.encoding = "utf-8" +opt.fileencoding = "utf-8" + +-- Python indentation (PEP 8) +vim.api.nvim_create_autocmd("FileType", { + pattern = "python", + callback = function() + vim.opt_local.tabstop = 4 + vim.opt_local.shiftwidth = 4 + end, +}) diff --git a/lua/plugins/autopairs.lua b/lua/plugins/autopairs.lua @@ -0,0 +1,23 @@ +return { + { + "windwp/nvim-autopairs", + event = "InsertEnter", + dependencies = { "hrsh7th/nvim-cmp" }, + config = function() + require("nvim-autopairs").setup({ + check_ts = true, -- use treesitter to avoid pairing inside strings/comments + ts_config = { + python = { "string", "comment" }, + bash = { "string" }, + }, + fast_wrap = { + map = "<M-e>", -- Alt-e wraps the word under cursor in a pair + }, + }) + + -- Auto-insert closing bracket when confirming a completion item + local cmp_autopairs = require("nvim-autopairs.completion.cmp") + require("cmp").event:on("confirm_done", cmp_autopairs.on_confirm_done()) + end, + }, +} diff --git a/lua/plugins/completion.lua b/lua/plugins/completion.lua @@ -0,0 +1,92 @@ +return { + { + "hrsh7th/nvim-cmp", + event = "InsertEnter", + dependencies = { + "hrsh7th/cmp-nvim-lsp", + "hrsh7th/cmp-buffer", + "hrsh7th/cmp-path", + "hrsh7th/cmp-cmdline", + "L3MON4D3/LuaSnip", + "saadparwaiz1/cmp_luasnip", + "rafamadriz/friendly-snippets", -- snippet collection for many languages + }, + config = function() + local cmp = require("cmp") + local luasnip = require("luasnip") + + require("luasnip.loaders.from_vscode").lazy_load() + + cmp.setup({ + snippet = { + expand = function(args) + luasnip.lsp_expand(args.body) + end, + }, + + mapping = cmp.mapping.preset.insert({ + ["<C-b>"] = cmp.mapping.scroll_docs(-4), + ["<C-f>"] = cmp.mapping.scroll_docs(4), + ["<C-Space>"] = cmp.mapping.complete(), + ["<C-e>"] = cmp.mapping.abort(), + ["<CR>"] = cmp.mapping.confirm({ select = false }), + + ["<Tab>"] = cmp.mapping(function(fallback) + if cmp.visible() then + cmp.select_next_item() + elseif luasnip.expand_or_jumpable() then + luasnip.expand_or_jump() + else + fallback() + end + end, { "i", "s" }), + + ["<S-Tab>"] = cmp.mapping(function(fallback) + if cmp.visible() then + cmp.select_prev_item() + elseif luasnip.jumpable(-1) then + luasnip.jump(-1) + else + fallback() + end + end, { "i", "s" }), + }), + + sources = cmp.config.sources({ + { name = "nvim_lsp" }, + { name = "luasnip" }, + { name = "buffer" }, + { name = "path" }, + }), + + formatting = { + format = function(entry, item) + local source_labels = { + nvim_lsp = "[LSP]", + luasnip = "[Snip]", + buffer = "[Buf]", + path = "[Path]", + } + item.menu = source_labels[entry.source.name] or "" + return item + end, + }, + }) + + -- Completion in search + cmp.setup.cmdline({ "/", "?" }, { + mapping = cmp.mapping.preset.cmdline(), + sources = { { name = "buffer" } }, + }) + + -- Completion in command line + cmp.setup.cmdline(":", { + mapping = cmp.mapping.preset.cmdline(), + sources = cmp.config.sources({ + { name = "path" }, + { name = "cmdline" }, + }), + }) + end, + }, +} diff --git a/lua/plugins/lint.lua b/lua/plugins/lint.lua @@ -0,0 +1,47 @@ +return { + -- Async linting + { + "mfussenegger/nvim-lint", + event = { "BufWritePost", "BufReadPost", "InsertLeave" }, + config = function() + local lint = require("lint") + + lint.linters_by_ft = { + python = { "ruff" }, + sh = { "shellcheck" }, + bash = { "shellcheck" }, + yaml = { "yamllint" }, + ["yaml.ansible"] = { "ansible_lint" }, + terraform = { "tflint" }, + } + + vim.api.nvim_create_autocmd({ "BufWritePost", "InsertLeave", "BufReadPost" }, { + callback = function() + lint.try_lint() + end, + }) + end, + }, + + -- Formatting + { + "stevearc/conform.nvim", + event = "BufWritePre", + cmd = "ConformInfo", + opts = { + formatters_by_ft = { + python = { "ruff_format" }, + sh = { "shfmt" }, + bash = { "shfmt" }, + yaml = { "prettier" }, + lua = { "stylua" }, + terraform = { "terraform_fmt" }, + ["yaml.ansible"] = { "prettier" }, + }, + format_on_save = { + timeout_ms = 500, + lsp_fallback = true, + }, + }, + }, +} diff --git a/lua/plugins/lsp.lua b/lua/plugins/lsp.lua @@ -0,0 +1,93 @@ +return { + { "williamboman/mason.nvim", opts = {} }, + + -- Kept as a dep: mason-lspconfig uses it for server cmd/filetype definitions. + -- We do NOT call require('lspconfig').server.setup() — that API is deprecated + -- in Neovim 0.11+. Configuration goes through vim.lsp.config() below. + { "neovim/nvim-lspconfig" }, + + { + "williamboman/mason-lspconfig.nvim", + dependencies = { + "williamboman/mason.nvim", + "neovim/nvim-lspconfig", + "hrsh7th/cmp-nvim-lsp", + }, + config = function() + -- Keymaps set once per buffer when any LSP server attaches + vim.api.nvim_create_autocmd("LspAttach", { + callback = function(args) + local bufnr = args.buf + local m = function(keys, func, desc) + vim.keymap.set("n", keys, func, { buffer = bufnr, desc = desc }) + end + m("gd", vim.lsp.buf.definition, "Go to definition") + m("gD", vim.lsp.buf.declaration, "Go to declaration") + m("gr", vim.lsp.buf.references, "References") + m("gi", vim.lsp.buf.implementation, "Go to implementation") + m("K", vim.lsp.buf.hover, "Hover docs") + m("<leader>rn", vim.lsp.buf.rename, "Rename") + m("<leader>ca", vim.lsp.buf.code_action, "Code action") + m("<leader>cf", function() + require("conform").format({ async = true, lsp_fallback = true }) + end, "Format file") + end, + }) + + -- Apply nvim-cmp capabilities to every server + vim.lsp.config("*", { + capabilities = require("cmp_nvim_lsp").default_capabilities(), + }) + + -- Per-server settings via native vim.lsp.config (Neovim 0.11+) + vim.lsp.config("pylsp", { + settings = { + pylsp = { + plugins = { + ruff = { enabled = true }, + pyflakes = { enabled = false }, + pycodestyle = { enabled = false }, + mccabe = { enabled = false }, + }, + }, + }, + }) + + vim.lsp.config("yamlls", { + filetypes = { "yaml" }, -- ansiblels handles yaml.ansible + settings = { + yaml = { + validate = true, + schemaStore = { enable = true }, + }, + }, + }) + + vim.lsp.config("ansiblels", { + filetypes = { "yaml.ansible" }, + }) + + vim.lsp.config("lua_ls", { + settings = { + Lua = { + diagnostics = { globals = { "vim" } }, + workspace = { checkThirdParty = false }, + telemetry = { enable = false }, + }, + }, + }) + + -- Install servers; mason-lspconfig calls vim.lsp.enable() for each one + require("mason-lspconfig").setup({ + ensure_installed = { + "pylsp", -- Python + "bashls", -- Bash/Shell (requires node) + "yamlls", -- YAML (requires node) + "ansiblels", -- Ansible (requires node) + "terraformls", -- OpenTofu / Terraform + "lua_ls", -- Lua + }, + }) + end, + }, +} diff --git a/lua/plugins/treesitter.lua b/lua/plugins/treesitter.lua @@ -0,0 +1,17 @@ +return { + { + "nvim-treesitter/nvim-treesitter", + build = ":TSUpdate", + -- Neovim 0.12 bundles parsers for: bash, lua, python, markdown, vim, vimdoc. + -- nvim-treesitter only needs to install what's missing. + -- Requires tree-sitter-cli: sudo pacman -S tree-sitter-cli + config = function() + require("nvim-treesitter").install({ + "yaml", + "toml", + "json", + "hcl", -- Terraform / OpenTofu .tf files + }) + end, + }, +} diff --git a/lua/plugins/ui.lua b/lua/plugins/ui.lua @@ -0,0 +1,62 @@ +return { + -- Colorscheme + { + "catppuccin/nvim", + name = "catppuccin", + priority = 1000, + config = function() + vim.cmd.colorscheme("catppuccin-mocha") + end, + }, + + -- Statusline + { + "nvim-lualine/lualine.nvim", + dependencies = { "nvim-tree/nvim-web-devicons", "catppuccin/nvim" }, + opts = { options = { theme = "catppuccin" } }, + }, + + -- File explorer + { + "nvim-tree/nvim-tree.lua", + dependencies = { "nvim-tree/nvim-web-devicons" }, + opts = { + view = { width = 30 }, + renderer = { group_empty = true }, + }, + }, + + -- Fuzzy finder + { + "nvim-telescope/telescope.nvim", + 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, + }, + + -- LSP loading indicator (bottom-right) + { "j-hui/fidget.nvim", opts = {} }, + + -- Indent guides + { + "lukas-reineke/indent-blankline.nvim", + main = "ibl", + opts = {}, + }, + + -- Git decorations in the sign column + { "lewis6991/gitsigns.nvim", opts = {} }, + + -- Popup showing available keybindings after pressing leader + { + "folke/which-key.nvim", + event = "VeryLazy", + opts = {}, + }, +}