-- Published at https://www.mfserna.dev/nvim — install with -- curl -fsSL https://www.mfserna.dev/nvim/install.sh | sh -- Toolchain paths that only exist on some machines. local java_home = "/opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home" if vim.uv.fs_stat(java_home) then vim.env.JAVA_HOME = java_home vim.env.PATH = java_home .. "/bin:" .. vim.env.PATH end local go_bin = vim.fn.expand("~/go/bin") if vim.uv.fs_stat(go_bin) then vim.env.PATH = go_bin .. ":" .. vim.env.PATH end vim.g.mapleader = " " -- 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({ -- Theme { "morhetz/gruvbox", priority = 1000, config = function() vim.cmd("colorscheme gruvbox") end, }, -- Auto-pairs { "windwp/nvim-autopairs", event = "InsertEnter", config = function() local npairs = require("nvim-autopairs") npairs.setup({ check_ts = true }) local cmp_autopairs = require("nvim-autopairs.completion.cmp") require("cmp").event:on("confirm_done", cmp_autopairs.on_confirm_done()) end, }, -- Buffer tabs { "akinsho/bufferline.nvim", dependencies = { "nvim-tree/nvim-web-devicons" }, config = function() require("bufferline").setup({}) end, }, -- Status line { "nvim-lualine/lualine.nvim", dependencies = { "nvim-tree/nvim-web-devicons" }, config = function() require("lualine").setup({ sections = { lualine_a = { "mode" }, lualine_b = { "branch", "diff", "diagnostics" }, lualine_c = { "filename" }, lualine_x = { "encoding", "filetype" }, lualine_y = { "progress" }, lualine_z = { "location" }, }, }) end, }, -- File tree { "nvim-tree/nvim-tree.lua", dependencies = { "nvim-tree/nvim-web-devicons" }, config = function() require("nvim-tree").setup({ view = { width = 30 }, filters = { dotfiles = false }, git = { enable = true }, }) end, }, { "nvim-tree/nvim-web-devicons" }, -- Treesitter { "nvim-treesitter/nvim-treesitter", branch = "main", lazy = false, build = ":TSUpdate", config = function() require("nvim-treesitter").install({ "c", "cpp", "cmake", "make" }) -- The `main` branch does not start highlighting on its own. vim.api.nvim_create_autocmd("FileType", { pattern = { "c", "cpp", "objc", "objcpp", "cuda", "cmake", "make" }, callback = function() pcall(vim.treesitter.start) end, }) end, }, -- LSP { "neovim/nvim-lspconfig" }, { "williamboman/mason.nvim", config = function() require("mason").setup() end, }, { "williamboman/mason-lspconfig.nvim", dependencies = { "williamboman/mason.nvim", "neovim/nvim-lspconfig" }, config = function() require("mason-lspconfig").setup({ ensure_installed = { "lua_ls", "clangd", "pyright", "rust_analyzer", "hls", "html", "ts_ls", "cssls", "jdtls", "gopls", }, automatic_installation = true, }) end, }, -- Java LSP { "mfussenegger/nvim-jdtls" }, -- Rust { "mrcjkb/rustaceanvim", version = "^5", lazy = false, init = function() vim.g.rustaceanvim = { server = { capabilities = require("cmp_nvim_lsp").default_capabilities(), default_settings = { ["rust-analyzer"] = { checkOnSave = true, check = { command = "clippy" }, cargo = { allFeatures = true }, procMacro = { enable = true }, inlayHints = { lifetimeElisionHints = { enable = "always" }, }, }, }, }, } end, }, -- Cargo.toml dependency management { "saecki/crates.nvim", event = { "BufRead Cargo.toml" }, config = function() local crates = require("crates") crates.setup({ completion = { cmp = { enabled = true }, }, }) end, }, -- Snippets { "L3MON4D3/LuaSnip", version = "v2.*", build = "make install_jsregexp", dependencies = { "rafamadriz/friendly-snippets" }, config = function() require("luasnip.loaders.from_vscode").lazy_load() end, }, -- Autocompletion { "hrsh7th/nvim-cmp", dependencies = { "hrsh7th/cmp-nvim-lsp", "hrsh7th/cmp-buffer", "hrsh7th/cmp-path", "hrsh7th/cmp-cmdline", "saadparwaiz1/cmp_luasnip", "L3MON4D3/LuaSnip", }, config = function() local cmp = require("cmp") local luasnip = require("luasnip") cmp.setup({ snippet = { expand = function(args) luasnip.lsp_expand(args.body) end, }, window = { completion = cmp.config.window.bordered(), documentation = cmp.config.window.bordered(), }, mapping = cmp.mapping.preset.insert({ [""] = cmp.mapping.scroll_docs(-4), [""] = cmp.mapping.scroll_docs(4), [""] = cmp.mapping.complete(), [""] = cmp.mapping.abort(), [""] = cmp.mapping.confirm({ select = true }), [""] = 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" }, }), }) cmp.setup.cmdline({ "/", "?" }, { mapping = cmp.mapping.preset.cmdline(), sources = { { name = "buffer" } }, }) cmp.setup.cmdline(":", { mapping = cmp.mapping.preset.cmdline(), sources = cmp.config.sources( { { name = "path" } }, { { name = "cmdline" } } ), }) end, }, -- Telescope { "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({ defaults = { file_ignore_patterns = { "node_modules", ".git/", "target/", "build/" }, layout_strategy = "horizontal", layout_config = { prompt_position = "top" }, sorting_strategy = "ascending", }, pickers = { find_files = { hidden = true }, }, }) pcall(telescope.load_extension, "fzf") end, }, -- Git signs { "lewis6991/gitsigns.nvim", config = function() require("gitsigns").setup({ signs = { add = { text = "│" }, change = { text = "│" }, delete = { text = "_" }, topdelete = { text = "‾" }, changedelete = { text = "~" }, }, on_attach = function(bufnr) local gs = require("gitsigns") local function bmap(mode, l, r, desc) vim.keymap.set(mode, l, r, { buffer = bufnr, desc = desc }) end bmap("n", "]h", gs.next_hunk, "Next hunk") bmap("n", "[h", gs.prev_hunk, "Prev hunk") bmap("n", "gp", gs.preview_hunk, "Preview hunk") bmap("n", "gs", gs.stage_hunk, "Stage hunk") bmap("n", "gr", gs.reset_hunk, "Reset hunk") bmap("n", "gu", gs.undo_stage_hunk, "Undo stage hunk") bmap("n", "gS", gs.stage_buffer, "Stage buffer") bmap("n", "gR", gs.reset_buffer, "Reset buffer") bmap("n", "gb", gs.blame_line, "Blame line") bmap("n", "gB", function() gs.blame_line({ full = true }) end, "Blame line (full)") bmap("n", "gd", gs.diffthis, "Diff this") end, }) end, }, -- Terminal { "akinsho/toggleterm.nvim", version = "*", config = function() require("toggleterm").setup({ size = function(term) if term.direction == "horizontal" then return 15 elseif term.direction == "vertical" then return vim.o.columns * 0.4 end end, open_mapping = [[]], direction = "horizontal", shade_terminals = true, float_opts = { border = "rounded" }, }) end, }, -- Commenting { "numToStr/Comment.nvim", config = function() require("Comment").setup() end, }, -- Indent guides { "lukas-reineke/indent-blankline.nvim", main = "ibl", config = function() require("ibl").setup({ indent = { char = "│" }, scope = { enabled = true, show_start = false, show_end = false }, }) end, }, -- Which-key { "folke/which-key.nvim", event = "VeryLazy", config = function() local wk = require("which-key") wk.setup({ delay = 300, }) wk.add({ { "f", group = "Find" }, { "g", group = "Git" }, { "l", group = "LSP" }, { "c", group = "Cursor CLI" }, { "r", group = "Rust" }, }) end, }, }) -- LSP config (Neovim 0.11+ native API) local servers = { "lua_ls", "clangd", "pyright", "hls", "html", "ts_ls", "cssls", "gopls" } local cmp_capabilities = require("cmp_nvim_lsp").default_capabilities() for _, server in ipairs(servers) do local settings = {} local cmd = nil if server == "lua_ls" then settings = { Lua = { diagnostics = { globals = { "vim" } } } } elseif server == "clangd" then cmd = { "clangd", "--background-index", "--clang-tidy", "--header-insertion=iwyu", "--completion-style=detailed", "--function-arg-placeholders", "--fallback-style=llvm", } elseif server == "gopls" then settings = { gopls = { analyses = { unusedparams = true, shadow = true, nilness = true, unusedwrite = true, }, staticcheck = true, }, } end vim.lsp.config(server, { cmd = cmd, capabilities = cmp_capabilities, settings = settings, }) end vim.lsp.enable(servers) -- BAML (https://boundaryml.com) — `brew install baml`, LSP ships with the toolchain. -- Highlighting comes from LSP semantic tokens; syntax/baml.vim is only a fallback. vim.filetype.add({ extension = { baml = "baml" } }) vim.lsp.config("baml", { cmd = { "baml", "lsp" }, filetypes = { "baml" }, root_markers = { "baml.toml", "baml_src" }, capabilities = cmp_capabilities, }) vim.lsp.enable("baml") vim.api.nvim_create_autocmd("FileType", { pattern = "baml", callback = function(ev) vim.bo[ev.buf].commentstring = "// %s" vim.bo[ev.buf].shiftwidth = 2 vim.bo[ev.buf].tabstop = 2 vim.bo[ev.buf].softtabstop = 2 local function bmap(l, r, desc) vim.keymap.set("n", l, r, { buffer = ev.buf, desc = desc }) end bmap("lt", "!baml test", "BAML: run tests") bmap("lc", "!baml check", "BAML: check") end, }) -- LSP keybindings (attached per-buffer when an LSP connects) vim.api.nvim_create_autocmd("LspAttach", { callback = function(ev) local buf = ev.buf local function bmap(mode, l, r, desc) vim.keymap.set(mode, l, r, { buffer = buf, desc = desc }) end bmap("n", "gd", vim.lsp.buf.definition, "Go to definition") bmap("n", "gD", vim.lsp.buf.declaration, "Go to declaration") bmap("n", "gi", vim.lsp.buf.implementation, "Go to implementation") bmap("n", "gr", vim.lsp.buf.references, "Find references") bmap("n", "K", vim.lsp.buf.hover, "Hover docs") bmap("n", "lk", vim.lsp.buf.signature_help, "Signature help") bmap("i", "", vim.lsp.buf.signature_help, "Signature help") bmap("n", "lr", vim.lsp.buf.rename, "Rename symbol") bmap("n", "la", vim.lsp.buf.code_action, "Code action") bmap("n", "lf", function() vim.lsp.buf.format({ async = true }) end, "Format buffer") bmap("n", "ls", vim.lsp.buf.document_symbol, "Document symbols") bmap("n", "lw", vim.lsp.buf.workspace_symbol, "Workspace symbols") bmap("n", "li", "LspInfo", "LSP info") -- Semantic token highlight links local links = { ["@lsp.type.class"] = "@type", ["@lsp.type.enum"] = "@type", ["@lsp.type.interface"] = "@type", ["@lsp.type.type"] = "@type", ["@lsp.type.typeParameter"] = "@type", ["@lsp.type.parameter"] = "@variable", ["@lsp.type.property"] = "@property", ["@lsp.type.variable"] = "@variable", ["@lsp.typemod.variable.readonly"] = "@constant", ["@lsp.typemod.variable.global"] = "@constant", } for new, old in pairs(links) do vim.api.nvim_set_hl(0, new, { link = old, default = true }) end end, }) -- Java LSP via nvim-jdtls vim.api.nvim_create_autocmd("FileType", { pattern = "java", callback = function() local jdtls = require("jdtls") local root_markers = { ".git", "mvnw", "gradlew", "pom.xml", "build.gradle" } local root_dir = require("jdtls.setup").find_root(root_markers) if root_dir == "" then return end local workspace_dir = vim.fn.stdpath("data") .. "/jdtls-workspace/" .. vim.fn.fnamemodify(root_dir, ":p:h:t") vim.fn.mkdir(workspace_dir, "p") local jdtls_bin = vim.fn.stdpath("data") .. "/mason/bin/jdtls" jdtls.start_or_attach({ cmd = { jdtls_bin, "-data", workspace_dir }, root_dir = root_dir, capabilities = cmp_capabilities, }) end, }) -- C/C++ keybindings (clangd commands) vim.api.nvim_create_autocmd("FileType", { pattern = { "c", "cpp", "objc", "objcpp" }, callback = function(ev) vim.keymap.set("n", "lh", "LspClangdSwitchSourceHeader", { buffer = ev.buf, desc = "Switch header/source" }) end, }) -- Rust keybindings (rustaceanvim commands) vim.api.nvim_create_autocmd("FileType", { pattern = "rust", callback = function(ev) local buf = ev.buf local function bmap(mode, l, r, desc) vim.keymap.set(mode, l, r, { buffer = buf, desc = desc }) end bmap("n", "rr", function() vim.cmd.RustLsp("runnables") end, "Runnables") bmap("n", "rd", function() vim.cmd.RustLsp("debuggables") end, "Debuggables") bmap("n", "re", function() vim.cmd.RustLsp("expandMacro") end, "Expand macro") bmap("n", "rc", function() vim.cmd.RustLsp("openCargo") end, "Open Cargo.toml") bmap("n", "rp", function() vim.cmd.RustLsp("parentModule") end, "Parent module") bmap("n", "rj", function() vim.cmd.RustLsp("joinLines") end, "Join lines") bmap("n", "ra", function() vim.cmd.RustLsp("codeAction") end, "Code action (Rust)") bmap("n", "rh", function() vim.cmd.RustLsp({ "hover", "actions" }) end, "Hover actions") bmap("n", "J", function() vim.cmd.RustLsp("joinLines") end, "Rust join lines") end, }) -- Cursor CLI integration local function cursor_open_file() local f = vim.fn.expand("%:p") if f ~= "" then vim.cmd("!cursor '" .. f .. "'") else print("No file open to send to Cursor") end end local function cursor_open_project() vim.cmd("!cursor '" .. vim.fn.getcwd() .. "'") end local function cursor_diff_files() local file1 = vim.fn.input("First file: ") local file2 = vim.fn.input("Second file: ") if file1 ~= "" and file2 ~= "" then vim.cmd("!cursor --diff '" .. file1 .. "' '" .. file2 .. "'") end end local function cursor_tunnel_status() vim.cmd("!cursor tunnel status") end local function cursor_tunnel_start() vim.cmd("!cursor tunnel") end local function cursor_help() print("Cursor CLI: co=file cp=project cd=diff ct=tunnel-status cs=tunnel-start ch=help") end -- Keybindings local map = vim.keymap.set -- File tree map("n", "e", "NvimTreeFocus", { desc = "Focus file tree" }) map("n", "t", "NvimTreeToggle", { desc = "Toggle file tree" }) -- Diagnostics map("n", "[d", vim.diagnostic.goto_prev, { desc = "Prev diagnostic" }) map("n", "]d", vim.diagnostic.goto_next, { desc = "Next diagnostic" }) map("n", "q", vim.diagnostic.setloclist, { desc = "Diagnostics to loclist" }) -- Telescope local builtin = require("telescope.builtin") map("n", "ff", builtin.find_files, { desc = "Find files" }) map("n", "fg", builtin.live_grep, { desc = "Live grep" }) map("n", "fb", builtin.buffers, { desc = "Buffers" }) map("n", "fh", builtin.help_tags, { desc = "Help tags" }) map("n", "fr", builtin.oldfiles, { desc = "Recent files" }) map("n", "fs", builtin.lsp_document_symbols, { desc = "Document symbols" }) map("n", "fw", builtin.lsp_workspace_symbols, { desc = "Workspace symbols" }) map("n", "fd", builtin.diagnostics, { desc = "Diagnostics" }) map("n", "fc", builtin.git_commits, { desc = "Git commits" }) map("n", "fG", builtin.git_status, { desc = "Git status" }) -- Buffers map("n", "", "BufferLineCycleNext", { desc = "Next buffer" }) map("n", "", "BufferLineCyclePrev", { desc = "Prev buffer" }) map("n", "x", "bdelete", { desc = "Close buffer" }) -- Terminal (C-\ opens, these are extras) map("n", "th", "ToggleTerm direction=horizontal", { desc = "Terminal horizontal" }) map("n", "tv", "ToggleTerm direction=vertical", { desc = "Terminal vertical" }) map("n", "tf", "ToggleTerm direction=float", { desc = "Terminal float" }) map("t", "", [[]], { desc = "Exit terminal mode" }) -- Window navigation (works from editor, tree, and terminal) map("n", "", "h", { desc = "Focus window left" }) map("n", "", "j", { desc = "Focus window down" }) map("n", "", "k", { desc = "Focus window up" }) map("n", "", "l", { desc = "Focus window right" }) map("t", "", [[h]], { desc = "Focus window left" }) map("t", "", [[j]], { desc = "Focus window down" }) map("t", "", [[k]], { desc = "Focus window up" }) map("t", "", [[l]], { desc = "Focus window right" }) -- Cursor CLI map("n", "co", cursor_open_file, { desc = "Open file in Cursor" }) map("n", "cp", cursor_open_project, { desc = "Open project in Cursor" }) map("n", "cd", cursor_diff_files, { desc = "Diff files in Cursor" }) map("n", "ct", cursor_tunnel_status, { desc = "Tunnel status" }) map("n", "cs", cursor_tunnel_start, { desc = "Start tunnel" }) map("n", "ch", cursor_help, { desc = "Cursor CLI help" }) -- Snippets (jump forward/backward in snippet placeholders) local luasnip = require("luasnip") map({ "i", "s" }, "", function() if luasnip.expand_or_jumpable() then luasnip.expand_or_jump() end end, { desc = "Snippet next placeholder" }) map({ "i", "s" }, "", function() if luasnip.jumpable(-1) then luasnip.jump(-1) end end, { desc = "Snippet prev placeholder" }) -- Options vim.opt.number = true vim.opt.relativenumber = true vim.opt.spell = false vim.opt.termguicolors = true vim.opt.tabstop = 4 vim.opt.shiftwidth = 4 vim.opt.softtabstop = 4 vim.opt.expandtab = true vim.opt.foldmethod = "expr" vim.opt.foldexpr = "v:lua.vim.treesitter.foldexpr()" vim.opt.foldenable = true vim.opt.foldlevel = 99 vim.opt.foldlevelstart = 99 vim.opt.updatetime = 250 vim.opt.signcolumn = "yes" vim.opt.cursorline = true vim.opt.scrolloff = 8 vim.opt.sidescrolloff = 8 vim.opt.splitright = true vim.opt.splitbelow = true vim.opt.ignorecase = true vim.opt.smartcase = true vim.opt.clipboard = "unnamedplus" vim.opt.undofile = true vim.opt.mouse = "a" vim.opt.showmode = false vim.cmd("syntax on") -- Custom highlight overrides (survive colorscheme reloads) local function apply_custom_highlights() vim.api.nvim_set_hl(0, "Comment", { fg = "#3d995d", italic = true }) vim.api.nvim_set_hl(0, "@comment", { link = "Comment" }) vim.api.nvim_set_hl(0, "@keyword", { fg = "#FF8700", bold = true }) vim.api.nvim_set_hl(0, "@string", { fg = "#D7AF87" }) end apply_custom_highlights() vim.api.nvim_create_autocmd("ColorScheme", { callback = apply_custom_highlights }) -- Diagnostics vim.diagnostic.config({ virtual_text = false, underline = true, update_in_insert = false, severity_sort = true, }) vim.api.nvim_create_autocmd("CursorHold", { callback = function() vim.diagnostic.open_float({ focusable = false, border = "rounded", source = true }) end, }) -- Auto-clear search highlight once the cursor leaves a match vim.api.nvim_create_autocmd("CursorMoved", { group = vim.api.nvim_create_augroup("auto_hlsearch", { clear = true }), callback = function() if vim.v.hlsearch == 1 and vim.fn.searchcount().exact_match == 0 then vim.schedule(function() vim.cmd.nohlsearch() end) end end, })