Created
August 11, 2025 06:23
-
-
Save wkurth-dev/b89c5ea94dbffe5c47e05d22e9e93f57 to your computer and use it in GitHub Desktop.
nevim config for mac
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| -- ~/.config/nvim/init.lua | |
| -- Modern Neovim configuration following best practices | |
| -- ============================================================================ | |
| -- LEADER KEYS - Set these first, before any plugins or mappings | |
| -- ============================================================================ | |
| vim.g.mapleader = " " -- Use space as leader key | |
| vim.g.maplocalleader = " " -- Use space as local leader too | |
| -- ============================================================================ | |
| -- CORE EDITOR SETTINGS | |
| -- ============================================================================ | |
| local opt = vim.opt | |
| -- Line numbers and visual aids | |
| opt.number = true -- Show absolute line numbers | |
| opt.relativenumber = false -- Disabled - change to true if you prefer relative numbers | |
| opt.cursorline = true -- Highlight current line | |
| opt.signcolumn = "yes" -- Always show sign column (prevents text jumping) | |
| -- Colors and UI | |
| opt.termguicolors = true -- Enable 24-bit RGB colors | |
| opt.background = "dark" -- Tell Neovim we're using a dark theme | |
| -- Mouse and clipboard | |
| opt.mouse = "a" -- Enable mouse support in all modes | |
| opt.clipboard = "unnamedplus" -- Use system clipboard | |
| -- Indentation (your preferences - will be overridden by editorconfig/sleuth) | |
| opt.tabstop = 2 -- Visual width of tab character | |
| opt.shiftwidth = 2 -- Spaces for autoindent | |
| opt.expandtab = true -- Convert tabs to spaces | |
| opt.softtabstop = 2 -- Spaces inserted when pressing Tab | |
| opt.smartindent = true -- Smart autoindenting | |
| -- Search behavior | |
| opt.ignorecase = true -- Ignore case in search | |
| opt.smartcase = true -- Unless search contains uppercase | |
| opt.hlsearch = false -- Don't highlight search results | |
| opt.incsearch = true -- Show search matches as you type | |
| -- Better editing experience | |
| opt.wrap = false -- Don't wrap long lines | |
| opt.scrolloff = 8 -- Keep 8 lines above/below cursor | |
| opt.sidescrolloff = 8 -- Keep 8 columns left/right of cursor | |
| opt.updatetime = 300 -- Faster completion and diagnostics | |
| -- ============================================================================ | |
| -- BOOTSTRAP LAZY.NVIM PLUGIN MANAGER | |
| -- ============================================================================ | |
| local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim" | |
| if not (vim.uv or vim.loop).fs_stat(lazypath) then | |
| vim.fn.system({ | |
| "git", | |
| "clone", | |
| "--filter=blob:none", | |
| "https://github.com/folke/lazy.nvim.git", | |
| "--branch=stable", | |
| lazypath, | |
| }) | |
| end | |
| opt.rtp:prepend(lazypath) | |
| -- ============================================================================ | |
| -- PLUGIN SPECIFICATIONS | |
| -- ============================================================================ | |
| require("lazy").setup({ | |
| -- ======================================================================== | |
| -- SYNTAX HIGHLIGHTING & CODE PARSING | |
| -- ======================================================================== | |
| { | |
| "nvim-treesitter/nvim-treesitter", | |
| build = ":TSUpdate", | |
| config = function() | |
| require("nvim-treesitter.configs").setup({ | |
| ensure_installed = { | |
| "lua", | |
| "typescript", | |
| "javascript", | |
| "tsx", -- TypeScript JSX | |
| "html", | |
| "css", | |
| "rust", | |
| "go", | |
| "c_sharp", | |
| "bash", | |
| "json", | |
| "yaml", | |
| "markdown", | |
| "vim", -- For editing Neovim config | |
| "vimdoc", -- For Neovim help files | |
| }, | |
| sync_install = false, -- Install parsers synchronously (only applied to `ensure_installed`) | |
| auto_install = true, -- Automatically install missing parsers when entering buffer | |
| ignore_install = {}, -- List of parsers to ignore installing | |
| modules = {}, -- List of modules and their configuration | |
| highlight = { | |
| enable = true, | |
| additional_vim_regex_highlighting = false, | |
| }, | |
| indent = { enable = true }, | |
| -- Enable incremental selection | |
| incremental_selection = { | |
| enable = true, | |
| keymaps = { | |
| init_selection = "<C-space>", | |
| node_incremental = "<C-space>", | |
| scope_incremental = "<C-s>", | |
| node_decremental = "<M-space>", | |
| }, | |
| }, | |
| }) | |
| end, | |
| }, | |
| -- ======================================================================== | |
| -- COLORSCHEME | |
| -- ======================================================================== | |
| { | |
| "scottmckendry/cyberdream.nvim", | |
| lazy = false, -- Load immediately during startup | |
| priority = 1000, -- Load before other plugins | |
| config = function() | |
| require("cyberdream").setup({ | |
| transparent = false, | |
| italic_comments = true, | |
| hide_fillchars = true, | |
| borderless_telescope = false, | |
| }) | |
| vim.cmd.colorscheme("cyberdream") | |
| end, | |
| }, | |
| -- ======================================================================== | |
| -- EDITOR CONFIGURATION & BEHAVIOR | |
| -- ======================================================================== | |
| { | |
| "editorconfig/editorconfig-vim", | |
| lazy = false, | |
| desc = "Automatically apply project-specific editor settings", | |
| }, | |
| { | |
| "tpope/vim-sleuth", | |
| lazy = false, | |
| desc = "Automatically detect indentation style from existing code", | |
| }, | |
| -- ======================================================================== | |
| -- LSP (Language Server Protocol) & DIAGNOSTICS | |
| -- ======================================================================== | |
| { | |
| "neovim/nvim-lspconfig", | |
| dependencies = { | |
| "williamboman/mason.nvim", -- LSP installer | |
| "williamboman/mason-lspconfig.nvim", -- Bridge between mason and lspconfig | |
| }, | |
| config = function() | |
| -- Setup Mason (LSP installer) | |
| require("mason").setup() | |
| require("mason-lspconfig").setup({ | |
| ensure_installed = { | |
| "lua_ls", -- Lua | |
| "ts_ls", -- TypeScript/JavaScript | |
| "html", -- HTML | |
| "cssls", -- CSS | |
| "rust_analyzer", -- Rust | |
| "gopls", -- Go | |
| "omnisharp", -- C# | |
| "bashls", -- Bash/Zsh | |
| "eslint", -- ESLint language server | |
| }, | |
| }) | |
| -- Auto-install formatters and linters | |
| local mason_registry = require("mason-registry") | |
| local mason_formatters = { "stylua", "prettier", "shfmt", "eslint_d" } | |
| for _, formatter in ipairs(mason_formatters) do | |
| if not mason_registry.is_installed(formatter) then | |
| vim.cmd("MasonInstall " .. formatter) | |
| end | |
| end | |
| -- Configure Language Servers | |
| local lspconfig = require("lspconfig") | |
| -- Lua Language Server for Neovim development | |
| lspconfig.lua_ls.setup({ | |
| settings = { | |
| Lua = { | |
| runtime = { version = "LuaJIT" }, | |
| diagnostics = { | |
| globals = { "vim" }, -- Recognize 'vim' as valid global | |
| }, | |
| workspace = { | |
| library = vim.api.nvim_get_runtime_file("", true), | |
| checkThirdParty = false, | |
| }, | |
| telemetry = { enable = false }, | |
| }, | |
| }, | |
| }) | |
| -- TypeScript/JavaScript | |
| lspconfig.ts_ls.setup({ | |
| on_attach = function(client, bufnr) | |
| -- Check if ESLint is configured in this project | |
| local has_eslint_config = vim.fs.find({ | |
| "eslint.config.js", "eslint.config.mjs", "eslint.config.cjs", | |
| ".eslintrc.js", ".eslintrc.cjs", ".eslintrc.json", ".eslintrc", | |
| ".eslintrc.yml", ".eslintrc.yaml" | |
| }, { path = vim.fn.expand("%:p:h"), upward = true })[1] | |
| if has_eslint_config then | |
| -- If ESLint is present, disable TypeScript's overlapping diagnostics | |
| client.server_capabilities.diagnosticProvider = false | |
| vim.defer_fn(function() | |
| vim.diagnostic.reset(nil, bufnr) | |
| end, 100) | |
| end | |
| end, | |
| }) | |
| -- ESLint Language Server (handles both linting and formatting) | |
| lspconfig.eslint.setup({ | |
| -- Only attach ESLint if project has ESLint configuration | |
| on_attach = function(client, bufnr) | |
| -- Check if ESLint config exists in project | |
| local has_eslint_config = vim.fs.find({ | |
| "eslint.config.js", "eslint.config.mjs", "eslint.config.cjs", | |
| ".eslintrc.js", ".eslintrc.cjs", ".eslintrc.json", ".eslintrc", | |
| ".eslintrc.yml", ".eslintrc.yaml" | |
| }, { path = vim.fn.expand("%:p:h"), upward = true })[1] | |
| if not has_eslint_config then | |
| -- If no ESLint config found, detach the client | |
| vim.lsp.buf_detach_client(bufnr, client.id) | |
| return | |
| end | |
| end, | |
| settings = { | |
| codeAction = { | |
| disableRuleComment = { | |
| enable = true, | |
| location = "separateLine" | |
| }, | |
| showDocumentation = { | |
| enable = true | |
| } | |
| }, | |
| codeActionOnSave = { | |
| enable = false, -- We'll handle this via conform | |
| mode = "all" | |
| }, | |
| format = true, | |
| nodePath = "", | |
| onIgnoredFiles = "off", | |
| packageManager = "npm", | |
| quiet = false, | |
| rulesCustomizations = {}, | |
| run = "onType", | |
| useESLintClass = false, | |
| validate = "on", | |
| workingDirectory = { mode = "location" } | |
| } | |
| }) | |
| -- HTML | |
| lspconfig.html.setup({}) | |
| -- CSS | |
| lspconfig.cssls.setup({}) | |
| -- Rust | |
| lspconfig.rust_analyzer.setup({ | |
| settings = { | |
| ["rust-analyzer"] = { | |
| cargo = { allFeatures = true }, | |
| checkOnSave = { command = "clippy" }, | |
| }, | |
| }, | |
| }) | |
| -- Go | |
| lspconfig.gopls.setup({}) | |
| -- C# | |
| lspconfig.omnisharp.setup({}) | |
| -- Bash/Zsh | |
| lspconfig.bashls.setup({}) | |
| -- ==================================================================== | |
| -- LSP & DIAGNOSTIC KEYMAPS | |
| -- ==================================================================== | |
| local keymap = vim.keymap.set | |
| -- Diagnostics navigation | |
| keymap("n", "<leader>e", vim.diagnostic.open_float, { desc = "Show diagnostic message" }) | |
| keymap("n", "[d", function() vim.diagnostic.jump({ count = -1, float = true }) end, { desc = "Previous diagnostic" }) | |
| keymap("n", "]d", function() vim.diagnostic.jump({ count = 1, float = true }) end, { desc = "Next diagnostic" }) | |
| -- LSP actions | |
| keymap("n", "<leader>ca", vim.lsp.buf.code_action, { desc = "Code actions" }) | |
| end, | |
| }, | |
| -- ======================================================================== | |
| -- AUTOCOMPLETION | |
| -- ======================================================================== | |
| { | |
| "hrsh7th/nvim-cmp", | |
| dependencies = { | |
| "hrsh7th/cmp-nvim-lsp", -- LSP completion source | |
| "hrsh7th/cmp-buffer", -- Buffer text completion | |
| "hrsh7th/cmp-path", -- File path completion | |
| "hrsh7th/cmp-cmdline", -- Command line completion | |
| "L3MON4D3/LuaSnip", -- Snippet engine | |
| "saadparwaiz1/cmp_luasnip", -- Snippet completion source | |
| }, | |
| config = function() | |
| local cmp = require("cmp") | |
| local luasnip = require("luasnip") | |
| 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 = true }), | |
| -- Tab/Shift-Tab for completion navigation | |
| ["<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" }), | |
| }), | |
| -- Completion sources (order matters - higher priority first) | |
| sources = cmp.config.sources({ | |
| { name = "nvim_lsp" }, | |
| { name = "luasnip" }, | |
| { name = "buffer" }, | |
| { name = "path" }, | |
| }), | |
| }) | |
| -- Command line completion for : commands | |
| cmp.setup.cmdline(":", { | |
| mapping = cmp.mapping.preset.cmdline(), | |
| sources = cmp.config.sources({ | |
| { name = "path" }, | |
| { name = "cmdline" }, | |
| }), | |
| }) | |
| end, | |
| }, | |
| -- ======================================================================== | |
| -- CODE FORMATTING | |
| -- ======================================================================== | |
| { | |
| "stevearc/conform.nvim", | |
| opts = { | |
| formatters_by_ft = { | |
| lua = { "stylua" }, | |
| typescript = { "eslint_d", "prettier" }, -- Try ESLint first, fallback to prettier | |
| javascript = { "eslint_d", "prettier" }, -- Try ESLint first, fallback to prettier | |
| typescriptreact = { "eslint_d", "prettier" }, | |
| javascriptreact = { "eslint_d", "prettier" }, | |
| html = { "prettier" }, | |
| css = { "prettier" }, | |
| rust = { "rustfmt" }, | |
| go = { "gofmt" }, | |
| sh = { "shfmt" }, | |
| bash = { "shfmt" }, | |
| zsh = { "shfmt" }, | |
| json = { "prettier" }, | |
| yaml = { "prettier" }, | |
| markdown = { "prettier" }, | |
| }, | |
| -- Smart formatter selection | |
| formatters = { | |
| eslint_d = { | |
| condition = function(self, ctx) | |
| -- Only use eslint_d if project has ESLint config | |
| local has_eslint_config = vim.fs.find({ | |
| "eslint.config.js", "eslint.config.mjs", "eslint.config.cjs", | |
| ".eslintrc.js", ".eslintrc.cjs", ".eslintrc.json", ".eslintrc" | |
| }, { path = ctx.filename, upward = true })[1] | |
| return has_eslint_config ~= nil | |
| end, | |
| }, | |
| }, | |
| format_on_save = { | |
| timeout_ms = 500, | |
| lsp_fallback = true, -- Fall back to LSP formatting if no formatter | |
| }, | |
| }, | |
| keys = { | |
| { | |
| "<leader>f", | |
| function() | |
| require("conform").format({ async = true, lsp_fallback = true }) | |
| end, | |
| desc = "Format buffer", | |
| }, | |
| }, | |
| }, | |
| -- ======================================================================== | |
| -- FILE NAVIGATION | |
| -- ======================================================================== | |
| { | |
| "nvim-tree/nvim-tree.lua", | |
| dependencies = { | |
| "nvim-tree/nvim-web-devicons", -- File type icons | |
| }, | |
| config = function() | |
| -- Disable Neovim's built-in file explorer | |
| vim.g.loaded_netrw = 1 | |
| vim.g.loaded_netrwPlugin = 1 | |
| require("nvim-tree").setup({ | |
| sort = { | |
| sorter = "case_sensitive", | |
| }, | |
| view = { | |
| width = 30, | |
| relativenumber = true, | |
| }, | |
| renderer = { | |
| group_empty = true, | |
| icons = { | |
| show = { | |
| folder_arrow = true, | |
| }, | |
| git_placement = "before", | |
| glyphs = { | |
| git = { | |
| unstaged = "●", -- Modified files (instead of red X) | |
| staged = "✓", -- Staged files | |
| unmerged = "◐", -- Merge conflicts | |
| renamed = "➜", -- Renamed files | |
| untracked = "★", -- New files | |
| deleted = "✖", -- Deleted files | |
| ignored = "◌", -- Ignored files | |
| }, | |
| }, | |
| }, | |
| }, | |
| filters = { | |
| dotfiles = false, -- Show hidden files | |
| custom = { "^.git$" }, -- Hide .git folder | |
| }, | |
| }) | |
| end, | |
| keys = { | |
| { "<leader>tt", ":NvimTreeToggle<CR>", desc = "Toggle file tree" }, | |
| { "<leader>tf", ":NvimTreeFindFile<CR>", desc = "Find current file in tree" }, | |
| }, | |
| }, | |
| -- ======================================================================== | |
| -- GIT INTEGRATION & CHANGE TRACKING | |
| -- ======================================================================== | |
| { | |
| "lewis6991/gitsigns.nvim", | |
| event = { "BufReadPre", "BufNewFile" }, -- Load when opening any file | |
| config = function() | |
| require("gitsigns").setup({ | |
| signs = { | |
| add = { text = "│" }, -- Lines added | |
| change = { text = "│" }, -- Lines modified | |
| delete = { text = "_" }, -- Lines deleted | |
| topdelete = { text = "‾" }, -- Lines deleted at top | |
| changedelete = { text = "~" }, -- Lines changed and deleted | |
| untracked = { text = "┆" }, -- Untracked files | |
| }, | |
| signs_staged = { | |
| add = { text = "┃" }, -- Staged additions (thicker line) | |
| change = { text = "┃" }, -- Staged changes | |
| delete = { text = "▁" }, -- Staged deletions | |
| topdelete = { text = "▔" }, -- Staged deletions at top | |
| changedelete = { text = "≃" }, -- Staged changes and deletions | |
| }, | |
| signcolumn = true, -- Show signs in sign column | |
| numhl = false, -- Don't highlight line numbers | |
| linehl = false, -- Don't highlight entire lines | |
| word_diff = false, -- Don't show word-level diffs | |
| watch_gitdir = { | |
| follow_files = true | |
| }, | |
| auto_attach = true, -- Automatically attach to Git repos | |
| attach_to_untracked = false, -- Don't attach to untracked files initially | |
| current_line_blame = false, -- Don't show blame on current line (can be toggled) | |
| current_line_blame_opts = { | |
| virt_text = true, | |
| virt_text_pos = "eol", -- Show at end of line | |
| delay = 1000, -- Delay before showing blame | |
| ignore_whitespace = false, | |
| }, | |
| sign_priority = 6, -- Priority for sign placement | |
| update_debounce = 100, -- Debounce time for updates | |
| status_formatter = nil, -- Use default status formatter | |
| max_file_length = 40000, -- Don't attach to files longer than this | |
| preview_config = { | |
| border = "single", -- Border style for preview windows | |
| style = "minimal", | |
| relative = "cursor", | |
| row = 0, | |
| col = 1 | |
| }, | |
| }) | |
| end, | |
| keys = { | |
| -- Navigation between Git changes | |
| { "]c", function() | |
| if vim.wo.diff then | |
| vim.cmd.normal({"]c", bang = true}) | |
| else | |
| require("gitsigns").nav_hunk("next") | |
| end | |
| end, desc = "Next Git change" }, | |
| { "[c", function() | |
| if vim.wo.diff then | |
| vim.cmd.normal({"[c", bang = true}) | |
| else | |
| require("gitsigns").nav_hunk("prev") | |
| end | |
| end, desc = "Previous Git change" }, | |
| -- Git hunk actions (leader + h for "hunk") | |
| { "<leader>hs", ":Gitsigns stage_hunk<CR>", desc = "Stage hunk", mode = { "n", "v" } }, | |
| { "<leader>hr", ":Gitsigns reset_hunk<CR>", desc = "Reset hunk", mode = { "n", "v" } }, | |
| { "<leader>hS", function() require("gitsigns").stage_buffer() end, desc = "Stage entire buffer" }, | |
| { "<leader>hu", ":Gitsigns undo_stage_hunk<CR>", desc = "Undo stage hunk" }, | |
| { "<leader>hR", function() require("gitsigns").reset_buffer() end, desc = "Reset entire buffer" }, | |
| { "<leader>hp", ":Gitsigns preview_hunk<CR>", desc = "Preview hunk changes" }, | |
| { "<leader>hb", function() require("gitsigns").blame_line({full=true}) end, desc = "Show line blame" }, | |
| { "<leader>tb", ":Gitsigns toggle_current_line_blame<CR>", desc = "Toggle line blame display" }, | |
| { "<leader>hd", ":Gitsigns diffthis<CR>", desc = "Diff against index" }, | |
| { "<leader>hD", function() require("gitsigns").diffthis("~") end, desc = "Diff against last commit" }, | |
| { "<leader>td", ":Gitsigns toggle_deleted<CR>", desc = "Toggle show deleted lines" }, | |
| -- Text object for Git hunks (works with operators like d, y, etc.) | |
| { "ih", ":<C-U>Gitsigns select_hunk<CR>", desc = "Select hunk", mode = { "o", "x" } }, | |
| }, | |
| }, | |
| -- ======================================================================== | |
| -- Add future plugins here following the same pattern: | |
| -- { | |
| -- "author/plugin-name", | |
| -- dependencies = { ... }, | |
| -- config = function() ... end, | |
| -- keys = { ... }, | |
| -- }, | |
| -- ======================================================================== | |
| }) | |
| -- ============================================================================ | |
| -- ADDITIONAL KEYMAPS (non-plugin specific) | |
| -- ============================================================================ | |
| local keymap = vim.keymap.set | |
| -- Better window navigation | |
| keymap("n", "<C-h>", "<C-w>h", { desc = "Move to left window" }) | |
| keymap("n", "<C-j>", "<C-w>j", { desc = "Move to bottom window" }) | |
| keymap("n", "<C-k>", "<C-w>k", { desc = "Move to top window" }) | |
| keymap("n", "<C-l>", "<C-w>l", { desc = "Move to right window" }) | |
| -- Better escape from insert mode | |
| keymap("i", "jk", "<Esc>", { desc = "Exit insert mode" }) | |
| -- Clear search highlighting | |
| keymap("n", "<Esc>", ":nohlsearch<CR>", { desc = "Clear search highlights" }) | |
| -- Quick blame | |
| keymap("n", "<leader>b", function() require("gitsigns").blame({full=true}) end, { desc = "Git blame" }) | |
| -- ============================================================================ | |
| -- AUTOCOMMANDS (automatic behaviors) | |
| -- ============================================================================ | |
| local augroup = vim.api.nvim_create_augroup | |
| local autocmd = vim.api.nvim_create_autocmd | |
| -- Highlight yanked text briefly | |
| autocmd("TextYankPost", { | |
| group = augroup("highlight_yank", { clear = true }), | |
| pattern = "*", | |
| callback = function() | |
| vim.highlight.on_yank({ higroup = "IncSearch", timeout = 300 }) | |
| end, | |
| }) | |
| -- Remove trailing whitespace on save | |
| autocmd("BufWritePre", { | |
| group = augroup("trim_whitespace", { clear = true }), | |
| pattern = "*", | |
| command = [[%s/\s\+$//e]], | |
| }) | |
| -- ============================================================================ | |
| -- End of configuration | |
| -- ============================================================================ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment