-- -- CONFIGS -- vim.opt.termguicolors = true -- enable 24-bit RGB colors in the terminal -- Line numbers vim.opt.number = true -- show absolute line number on current line vim.opt.relativenumber = true -- show relative line numbers for all other lines vim.opt.cursorline = true -- highlight the row the cursor is on vim.opt.wrap = false -- do not wrap long lines vim.opt.scrolloff = 10 -- keep at least 10 lines above/below the cursor vim.opt.sidescrolloff = 10 -- keep at least 10 columns left/right of the cursor -- Indentation: 2-space soft tabs vim.opt.tabstop = 2 -- a counts as 2 columns vim.opt.shiftwidth = 2 -- >> / << shift by 2 columns vim.opt.softtabstop = 2 -- in insert mode inserts 2 spaces vim.opt.expandtab = true -- insert spaces instead of tab characters vim.opt.smartindent = true vim.opt.autoindent = true -- Search vim.opt.ignorecase = true -- case-insensitive search by default vim.opt.smartcase = true -- switch to case-sensitive when query contains an uppercase letter vim.opt.hlsearch = true -- highlight all matches vim.opt.incsearch = true -- show matches while typing the query -- UI vim.opt.showmatch = true -- briefly jump to matching bracket when one is inserted vim.opt.cmdheight = 1 -- always show the command line (1 row) vim.opt.showmode = false -- hide "-- INSERT --" etc.; the statusline shows the mode instead vim.opt.pumheight = 10 -- limit the completion popup to 10 items vim.opt.pumblend = 10 -- make the completion popup slightly transparent vim.opt.winblend = 0 -- floating windows are fully opaque vim.opt.conceallevel = 0 -- never hide markup characters (e.g. Markdown asterisks) vim.opt.concealcursor = "" -- never hide markup on the cursor line either vim.opt.synmaxcol = 300 -- stop syntax highlighting for columns beyond 300 (perf guard) vim.opt.fillchars = { eob = " " } -- replace the "~" placeholder on empty lines with a space -- Persistent undo: survive across sessions even after the buffer is closed local undodir = vim.fn.expand("~/.vim/undodir") if vim.fn.isdirectory(undodir) == 0 -- create undodir if nonexistent then vim.fn.mkdir(undodir, "p") end vim.opt.backup = false -- do not create a backup file vim.opt.writebackup = false -- do not keep a backup while overwriting a file vim.opt.swapfile = false -- do not create a swapfile vim.opt.undofile = true -- persist undo history to disk vim.opt.undodir = undodir -- where to store undo files vim.opt.updatetime = 300 -- write swap file (and trigger CursorHold) after 300 ms of inactivity vim.opt.timeoutlen = 500 -- wait 500 ms for a mapped key sequence to complete vim.opt.ttimeoutlen = 50 -- wait 50 ms for a terminal key code to complete vim.opt.autoread = true -- reload a file changed outside Neovim without prompting vim.opt.autowrite = false -- do not silently save on :make / buffer switch -- Buffer and editing behaviour vim.opt.hidden = true -- allow switching away from an unsaved buffer without closing it vim.opt.errorbells = false -- disable the error bell vim.opt.backspace = "indent,eol,start" -- allow backspace over auto-indent, line breaks, and insert start vim.opt.autochdir = false -- keep the working directory fixed (don't follow the open file) vim.opt.iskeyword:append("-") -- treat "foo-bar" as a single word for motions like w/b vim.opt.path:append("**") -- make :find search recursively into subdirectories vim.opt.selection = "inclusive" -- visual selection includes the character under the cursor vim.opt.mouse = "a" -- enable mouse support in all modes vim.opt.clipboard:append("unnamedplus") -- share the system clipboard (+ register) -- Over SSH, wl-clipboard is unavailable; use OSC 52 to write yanks to the local terminal clipboard. if os.getenv("SSH_TTY") then vim.g.clipboard = { name = "OSC 52", copy = { ["+"] = require("vim.ui.clipboard.osc52").copy("+"), ["*"] = require("vim.ui.clipboard.osc52").copy("*"), }, paste = { ["+"] = require("vim.ui.clipboard.osc52").paste("+"), ["*"] = require("vim.ui.clipboard.osc52").paste("*"), }, } end vim.opt.modifiable = true -- allow buffer modifications (safety default) vim.opt.encoding = "utf-8" -- internal character encoding -- Folding: driven by Tree-sitter when available; foldlevel = 99 means all folds start open vim.opt.foldmethod = "expr" vim.opt.foldexpr = "v:lua.vim.treesitter.foldexpr()" vim.opt.foldlevel = 99 -- a high value ensures every fold is open on buffer load -- Window splits vim.opt.splitbelow = true -- :split opens the new window below vim.opt.splitright = true -- :vsplit opens the new window to the right -- Miscellaneous vim.opt.wildmenu = true -- enable enhanced command-line completion vim.opt.wildmode = "longest:full,full" -- first Tab completes the longest common prefix and lists all; subsequent Tabs cycle vim.opt.diffopt:append("linematch:60") -- use the improved diff algorithm for blocks up to 60 lines vim.opt.redrawtime = 10000 -- allow up to 10 s for a full screen redraw (useful for large files) vim.opt.maxmempattern = 20000 -- raise the memory cap for pattern matching (avoids "pattern too complex" errors) -- -- KEYMAPS -- vim.g.mapleader = " " -- as the leader key vim.g.maplocalleader = " " -- as the local leader key -- j/k navigate display lines when there is no count prefix, so wrapped lines -- feel natural. With a count (e.g. 5j) they use real lines so relative jumps work. vim.keymap.set("n", "j", function() return vim.v.count == 0 and "gj" or "j" end, { expr = true, silent = true, desc = "Down (wrap-aware)" }) vim.keymap.set("n", "k", function() return vim.v.count == 0 and "gk" or "k" end, { expr = true, silent = true, desc = "Up (wrap-aware)" }) -- Keep search results and half-page jumps vertically centred vim.keymap.set("n", "n", "nzzzv", { desc = "Next search result (centered)" }) vim.keymap.set("n", "N", "Nzzzv", { desc = "Previous search result (centered)" }) vim.keymap.set("n", "", "zz", { desc = "Half page down (centered)" }) vim.keymap.set("n", "", "zz", { desc = "Half page up (centered)" }) -- Window navigation without the prefix vim.keymap.set("n", "", "h", { desc = "Move to left window" }) vim.keymap.set("n", "", "j", { desc = "Move to bottom window" }) vim.keymap.set("n", "", "k", { desc = "Move to top window" }) vim.keymap.set("n", "", "l", { desc = "Move to right window" }) -- Re-select visual block after indent so you can keep indenting with < or > vim.keymap.set("v", "<", "", ">gv", { desc = "Indent right and reselect" }) -- J joins lines but restores the cursor position (mz saves, `z restores) vim.keymap.set("n", "J", "mzJ`z", { desc = "Join lines and keep cursor position" }) vim.keymap.set("n", "H", "bprev", { desc = "Previous buffer" }) vim.keymap.set("n", "L", "bnext", { desc = "Next buffer" }) -- Toggle LSP diagnostics on/off for the current session vim.keymap.set("n", "td", function() vim.diagnostic.enable(not vim.diagnostic.is_enabled()) end, { desc = "Toggle diagnostics" }) -- Toggle inline virtual text only (keeps underlines and signs) vim.keymap.set("n", "tv", function() local current = vim.diagnostic.config().virtual_text vim.diagnostic.config({ virtual_text = not current }) end, { desc = "Toggle virtual text" }) -- -- AUTOCOMMANDS -- local augroup = vim.api.nvim_create_augroup("UserConfig", { clear = true }) -- Format on save via the EFM language server. -- Only runs when: the buffer is a real file, it is modifiable, it has a name, -- and efm is attached. This avoids spurious save prompts on scratch buffers. vim.api.nvim_create_autocmd("BufWritePre", { group = augroup, pattern = { "*.cs", "*.lua", "*.py", "*.go", "*.js", "*.jsx", "*.ts", "*.tsx", "*.json", "*.css", "*.scss", "*.html", "*.sh", "*.bash", }, callback = function(args) -- avoid formatting non-file buffers (helps prevent weird write prompts) if vim.bo[args.buf].buftype ~= "" then return end if not vim.bo[args.buf].modifiable then return end if vim.api.nvim_buf_get_name(args.buf) == "" then return end local has_efm = false for _, c in ipairs(vim.lsp.get_clients({ bufnr = args.buf })) do if c.name == "efm" then has_efm = true break end end if not has_efm then return end pcall(vim.lsp.buf.format, { bufnr = args.buf, timeout_ms = 2000, filter = function(c) return c.name == "efm" end, }) end, }) -- Flash the yanked region briefly so it is clear what was copied vim.api.nvim_create_autocmd("TextYankPost", { group = augroup, callback = function() vim.hl.on_yank() end, }) -- Restore the cursor to where it was when the file was last closed vim.api.nvim_create_autocmd("BufReadPost", { group = augroup, desc = "Restore last cursor position", callback = function() if vim.o.diff then -- except in diff mode return end local last_pos = vim.api.nvim_buf_get_mark(0, '"') -- {line, col} local last_line = vim.api.nvim_buf_line_count(0) local row = last_pos[1] if row < 1 or row > last_line then return end pcall(vim.api.nvim_win_set_cursor, 0, last_pos) end, }) -- Prose-friendly settings for Markdown, plain text, and commit messages vim.api.nvim_create_autocmd("FileType", { group = augroup, pattern = { "markdown", "text", "gitcommit" }, callback = function() vim.opt_local.wrap = true -- soft-wrap at window edge vim.opt_local.linebreak = true -- break at word boundaries, not mid-word vim.opt_local.spell = true -- enable spell-checking end, }) -- -- PLUGINS -- vim.pack.add({ "https://github.com/folke/which-key.nvim", -- shows pending keymap hints "https://github.com/folke/tokyonight.nvim", -- colorscheme "https://github.com/nvim-lualine/lualine.nvim", -- statusline "https://www.github.com/lewis6991/gitsigns.nvim", -- git hunk signs in the gutter "https://www.github.com/echasnovski/mini.nvim", -- collection of small plugins "https://www.github.com/ibhagwan/fzf-lua", -- fuzzy finder (files, grep, LSP, …) "https://www.github.com/neovim/nvim-lspconfig", -- provides cmd/root_dir/filetypes for each LSP server "https://github.com/mason-org/mason.nvim", -- LSP / tool installer UI "https://github.com/creativenull/efmls-configs-nvim", -- pre-built efm-langserver tool configs "https://github.com/WhoIsSethDaniel/mason-tool-installer.nvim", -- auto-install Mason tools on startup "https://github.com/folke/flash.nvim", -- enhanced f/t/s jumps with labels "https://github.com/L3MON4D3/LuaSnip", -- snippet engine (used by blink.cmp) { src = "https://github.com/saghen/blink.cmp", -- completion engine version = vim.version.range("1.*"), -- pin to any 1.x release }, { src = "https://github.com/nvim-treesitter/nvim-treesitter", branch = "main", build = ":TSUpdate", -- keep parser binaries up to date after install/update }, "https://github.com/nvim-treesitter/nvim-treesitter-textobjects", "https://github.com/HakonHarnes/img-clip.nvim", -- paste images from clipboard (no Python required) }) -- -- PLUGIN CONFIGS -- require("lualine").setup() vim.cmd.colorscheme("tokyonight") -- which-key: shows a popup of available keymaps after a delay. -- Groups give the prefixes human-readable titles in the popup. require("which-key").setup({}) require("which-key").add({ { "f", group = "Find" }, -- fzf-lua pickers (files, grep, LSP symbols, …) { "g", group = "Go to" }, -- LSP navigation (definition, split, …) { "c", group = "Code" }, -- code actions { "r", group = "Refactor" }, -- rename, extract, … { "t", group = "Toggle" }, -- toggle settings (diagnostics, …) { "o", group = "Organize" }, -- organize imports, sort, … { "d", group = "Diagnostics" }, -- diagnostic float / list { "n", group = "Next" }, -- jump to next item { "p", group = "Prev" }, -- jump to previous item { "]m", hidden = true }, { "[m", hidden = true }, { "]M", hidden = true }, { "[M", hidden = true }, }) -- Tree-sitter: start the parser for a filetype only when its grammar is -- already installed. Missing grammars are installed asynchronously on first -- launch so the editor never blocks at startup. local setup_treesitter = function() local treesitter = require("nvim-treesitter") treesitter.setup({}) local ensure_installed = { "vim", "vimdoc", "go", "html", "css", "javascript", "json", "lua", "markdown", "python", "typescript", "bash", "c_sharp", "nix", } local config = require("nvim-treesitter.config") -- Only install parsers that are not already present local already_installed = config.get_installed() local parsers_to_install = {} for _, parser in ipairs(ensure_installed) do if not vim.tbl_contains(already_installed, parser) then table.insert(parsers_to_install, parser) end end if #parsers_to_install > 0 then treesitter.install(parsers_to_install) end -- Start the Tree-sitter parser whenever a supported filetype is opened. -- Using an autocmd instead of the built-in auto_install avoids the -- blocking install prompt on first open. local group = vim.api.nvim_create_augroup("TreeSitterConfig", { clear = true }) vim.api.nvim_create_autocmd("FileType", { group = group, callback = function(args) if vim.list_contains(treesitter.get_installed(), vim.treesitter.language.get_lang(args.match)) then vim.treesitter.start(args.buf) end end, }) end setup_treesitter() require("nvim-treesitter-textobjects").setup({ move = { set_jumps = true } }) local move = require("nvim-treesitter-textobjects.move") vim.keymap.set({ "n", "x", "o" }, "]f", function() move.goto_next_start("@function.outer", "textobjects") end, { desc = "Next function" }) vim.keymap.set({ "n", "x", "o" }, "[f", function() move.goto_previous_start("@function.outer", "textobjects") end, { desc = "Prev function" }) vim.keymap.set({ "n", "x", "o" }, "]c", function() move.goto_next_start("@class.outer", "textobjects") end, { desc = "Next class" }) vim.keymap.set({ "n", "x", "o" }, "[c", function() move.goto_previous_start("@class.outer", "textobjects") end, { desc = "Prev class" }) -- fzf-lua: fuzzy finder keymaps require("fzf-lua").setup({ files = { actions = { ["ctrl-h"] = { require("fzf-lua").actions.toggle_hidden }, }, }, }) vim.keymap.set("n", "", function() require("fzf-lua").files() end, { desc = "Find files" }) vim.keymap.set("n", "fg", function() require("fzf-lua").live_grep() end, { desc = "FZF Live Grep" }) vim.keymap.set("n", "fb", function() require("fzf-lua").buffers() end, { desc = "FZF Buffers" }) vim.keymap.set("n", "fh", function() require("fzf-lua").help_tags() end, { desc = "FZF Help Tags" }) vim.keymap.set("n", "fx", function() require("fzf-lua").diagnostics_document() end, { desc = "FZF Diagnostics Document" }) vim.keymap.set("n", "fX", function() require("fzf-lua").diagnostics_workspace() end, { desc = "FZF Diagnostics Workspace" }) -- mini.nvim: a suite of small, focused plugins. Each module is opted in explicitly. require("mini.ai").setup({}) -- extended text objects (e.g. cia = inside any argument) require("mini.comment").setup({}) -- gcc / gc to toggle comments require("mini.move").setup({}) -- move lines/selections with Option+hjkl (macOS Option = Alt) require("mini.surround").setup({}) -- sa/sd/sr to add/delete/replace surrounding chars require("mini.cursorword").setup({}) -- highlight all occurrences of the word under the cursor require("mini.indentscope").setup({}) -- animated indent-scope indicator line require("mini.pairs").setup({}) -- auto-close brackets, quotes, etc. require("mini.trailspace").setup({}) -- highlight and trim trailing whitespace require("mini.notify").setup({}) -- non-blocking notification popups require("mini.icons").setup({}) -- icon provider (replaces nvim-web-devicons calls) -- flash.nvim: label-based jumps for f/t and anywhere on screen require("flash").setup({}) vim.keymap.set({ "n", "x", "o" }, "s", function() require("flash").jump() end, { desc = "Flash jump" }) vim.keymap.set({ "n", "x", "o" }, "S", function() require("flash").treesitter() end, { desc = "Flash treesitter" }) vim.keymap.set("o", "r", function() require("flash").remote() end, { desc = "Flash remote" }) vim.keymap.set({ "o", "x" }, "R", function() require("flash").treesitter_search() end, { desc = "Flash treesitter search" }) -- img-clip: paste images from clipboard into markdown files (no Python required) require("img-clip").setup({ default = { use_absolute_path = false, relative_to_current_file = true, file_name = function() return os.date("%Y-%m-%d_%H-%M-%S") end, }, filetypes = { markdown = { template = "![]($FILE_PATH)" }, }, }) vim.keymap.set({ "n", "x" }, "pi", "PasteImage", { desc = "Paste image" }) -- gitsigns: decorates the sign column with added/changed/removed hunk markers require("gitsigns").setup({ signs = { add = { text = "\u{2590}" }, -- ▏ change = { text = "\u{2590}" }, -- ▐ delete = { text = "\u{2590}" }, -- ◦ topdelete = { text = "\u{25e6}" }, -- ◦ changedelete = { text = "\u{25cf}" }, -- ● untracked = { text = "\u{25cb}" }, -- ○ }, signcolumn = true, current_line_blame = false, -- set to true to show inline git blame on the cursor line }) -- -- LSP - Too anoying to have in this file -- require("lsp-nix")