if vim.fn.has("nvim-0.12") == 0 then vim.notify("This config expects Neovim nightly / 0.12+", vim.log.levels.WARN) end vim.g.mapleader = " " vim.g.maplocalleader = " " local opt = vim.opt local map = vim.keymap.set -- ============================================================================ -- Core options -- ============================================================================ opt.number = true opt.relativenumber = true opt.mouse = "a" opt.clipboard = "unnamedplus" opt.swapfile = false opt.backup = false opt.writebackup = false opt.undofile = true opt.ignorecase = true opt.smartcase = true opt.incsearch = true opt.hlsearch = false opt.termguicolors = true opt.signcolumn = "yes" opt.cursorline = true opt.splitbelow = true opt.splitright = true opt.wrap = false opt.linebreak = true opt.breakindent = true opt.scrolloff = 8 opt.sidescrolloff = 8 opt.updatetime = 200 opt.timeoutlen = 300 opt.confirm = true opt.showmode = false opt.laststatus = 3 opt.pumheight = 12 opt.expandtab = true opt.smartindent = true opt.shiftround = true opt.tabstop = 2 opt.softtabstop = 2 opt.shiftwidth = 2 opt.list = true opt.listchars = { tab = "» ", trail = "·", nbsp = "␣" } opt.fillchars = { eob = " " } opt.completeopt = { "menu", "menuone", "noselect", "popup", "fuzzy" } opt.winborder = "single" opt.pumborder = "single" opt.winbar = "%=%m %t %=" opt.showtabline = 0 -- ============================================================================ -- Basic keymaps -- ============================================================================ map("n", "", "nohlsearch", { silent = true, desc = "Clear search highlight" }) -- j/k over display lines when no count is given map({ "n", "x" }, "j", "v:count == 0 ? 'gj' : 'j'", { expr = true, silent = true, desc = "Down (display line)", }) map({ "n", "x" }, "k", "v:count == 0 ? 'gk' : 'k'", { expr = true, silent = true, desc = "Up (display line)", }) -- Save on Ctrl-s map("n", "", "update", { silent = true, desc = "Save" }) map("i", "", "updategi", { silent = true, desc = "Save" }) map("v", "", "updategv", { silent = true, desc = "Save" }) -- Window navigation on Ctrl-hjkl map("n", "", "h", { silent = true, desc = "Window left" }) map("n", "", "j", { silent = true, desc = "Window down" }) map("n", "", "k", { silent = true, desc = "Window up" }) map("n", "", "l", { silent = true, desc = "Window right" }) map("t", "", [[h]], { silent = true, desc = "Window left" }) map("t", "", [[j]], { silent = true, desc = "Window down" }) map("t", "", [[k]], { silent = true, desc = "Window up" }) map("t", "", [[l]], { silent = true, desc = "Window right" }) -- Buffer cycling map("n", "H", "bprevious", { silent = true, desc = "Previous buffer" }) map("n", "L", "bnext", { silent = true, desc = "Next buffer" }) -- ============================================================================ -- Buffer helpers -- ============================================================================ local function listed_buffers() return vim.tbl_filter(function(buf) return vim.api.nvim_buf_is_valid(buf) and vim.bo[buf].buflisted end, vim.api.nvim_list_bufs()) end local function delete_buffer(buf, opts) opts = opts or {} if not buf or buf == 0 then buf = vim.api.nvim_get_current_buf() end local cmd = opts.wipe and (opts.force and "bwipeout!" or "bwipeout") or (opts.force and "bdelete!" or "bdelete") pcall(vim.cmd, cmd .. " " .. buf) end local function delete_other_buffers(force) local current = vim.api.nvim_get_current_buf() for _, buf in ipairs(listed_buffers()) do if buf ~= current then delete_buffer(buf, { force = force }) end end end local function delete_all_buffers(force) for _, buf in ipairs(listed_buffers()) do delete_buffer(buf, { force = force }) end end map("n", "bb", "buffer #", { silent = true, desc = "Last buffer" }) map("n", "bd", function() delete_buffer(0, { force = false }) end, { desc = "Delete buffer" }) map("n", "bD", function() delete_buffer(0, { force = true }) end, { desc = "Force delete buffer" }) map("n", "bw", function() delete_buffer(0, { wipe = true }) end, { desc = "Wipe buffer" }) map("n", "bo", function() delete_other_buffers(false) end, { desc = "Delete other buffers" }) map("n", "ba", function() delete_all_buffers(false) end, { desc = "Delete all buffers" }) -- ============================================================================ -- netrw stays the default file browser -- ============================================================================ vim.g.netrw_banner = 1 vim.g.netrw_liststyle = 3 vim.g.netrw_winsize = 30 vim.g.netrw_keepdir = 0 map("n", "E", "Ex", { silent = true, desc = "Open netrw" }) vim.api.nvim_create_autocmd("VimEnter", { callback = function() if vim.fn.argc() == 0 then vim.cmd("Explore") end end, }) -- ============================================================================ -- Built-in package manager (nightly) -- ============================================================================ -- Optional post-install hooks for plugins that usually need one-time setup. vim.api.nvim_create_autocmd("PackChanged", { callback = function(ev) local data = ev.data or {} local spec = data.spec or {} local name = spec.name local kind = data.kind if kind ~= "install" and kind ~= "update" then return end if name == "nvim-treesitter" then vim.schedule(function() pcall(vim.cmd.packadd, "nvim-treesitter") pcall(function() require("nvim-treesitter.install").update({ with_sync = true })() end) end) end if name == "markdown-preview.nvim" then vim.schedule(function() pcall(vim.cmd.packadd, "markdown-preview.nvim") pcall(function() vim.fn["mkdp#util#install"]() end) end) end end, }) vim.pack.add({ { src = "https://github.com/nvim-lua/plenary.nvim" }, { src = "https://github.com/MunifTanjim/nui.nvim" }, { src = "https://github.com/nvim-tree/nvim-web-devicons" }, -- Themes { src = "https://github.com/oskarnurm/koda.nvim" }, { src = "https://github.com/vague-theme/vague.nvim" }, { src = "https://github.com/nendix/zen.nvim" }, { src = "https://github.com/rebelot/kanagawa.nvim" }, -- Editing / syntax / completion { src = "https://github.com/nvim-treesitter/nvim-treesitter" }, { src = "https://github.com/saghen/blink.cmp" }, { src = "https://github.com/L3MON4D3/LuaSnip" }, { src = "https://github.com/rafamadriz/friendly-snippets" }, { src = "https://gitlab.com/repetitivesin/madol.nvim" }, -- LSP configs (for vim.lsp.enable) { src = "https://github.com/neovim/nvim-lspconfig" }, -- Navigation / search { src = "https://github.com/ibhagwan/fzf-lua" }, { src = "https://github.com/nvim-neo-tree/neo-tree.nvim" }, { src = "https://github.com/folke/flash.nvim" }, { src = "https://github.com/leath-dub/snipe.nvim" }, { src = "https://github.com/folke/which-key.nvim" }, { src = "https://github.com/mikavilpas/yazi.nvim" }, -- Git { src = "https://github.com/tpope/vim-fugitive" }, { src = "https://github.com/lewis6991/gitsigns.nvim" }, { src = "https://github.com/kdheepak/lazygit.nvim" }, -- Text objects / editing { src = "https://github.com/kylechui/nvim-surround" }, { src = "https://github.com/windwp/nvim-autopairs" }, { src = "https://github.com/abecodes/tabout.nvim" }, -- UI { src = "https://github.com/nvim-lualine/lualine.nvim" }, { src = "https://github.com/chentoast/marks.nvim" }, -- Writing / docs { src = "https://github.com/chomosuke/typst-preview.nvim" }, { src = "https://github.com/iamcco/markdown-preview.nvim" }, { src = "https://github.com/MeanderingProgrammer/render-markdown.nvim" }, -- AI { src = "https://github.com/nickjvandyke/opencode.nvim" }, { src = "https://github.com/zbirenbaum/copilot.lua" }, }, { confirm = false, load = true }) -- ============================================================================ -- Plugin globals before setup -- ============================================================================ vim.g.mkdp_auto_start = 0 vim.g.mkdp_auto_close = 1 vim.g.mkdp_filetypes = { "markdown" } -- Let native LSP own LSP features; opencode still works for its own UI/actions. vim.g.opencode_opts = { lsp = { enabled = false, }, } vim.o.autoread = true -- ============================================================================ -- Themes -- ============================================================================ require("koda").setup({}) require("vague").setup({}) require("kanagawa").setup({ compile = false, background = { dark = "wave", light = "lotus", }, }) require("zen").setup({}) vim.cmd.colorscheme("vague") -- ============================================================================ -- which-key -- ============================================================================ local wk = require("which-key") wk.setup({ preset = "helix" }) wk.add({ { "b", group = "buffer" }, { "f", group = "find" }, { "g", group = "git" }, { "h", group = "hunks" }, { "l", group = "lsp" }, { "m", group = "marks" }, { "o", group = "opencode" }, { "t", group = "tree" }, { "p", group = "preview" }, { "u", group = "toggles" }, }) -- ============================================================================ -- Treesitter -- ============================================================================ require("nvim-treesitter").setup({ ensure_installed = { "bash", "c", "cpp", "css", "diff", "html", "javascript", "json", "json5", "latex", "lua", "luadoc", "markdown", "markdown_inline", "python", "query", "regex", "rust", "toml", "tsx", "typst", "typescript", "vim", "vimdoc", "yaml", }, auto_install = true, highlight = { enable = true }, indent = { enable = true }, }) -- ============================================================================ -- LuaSnip -- ============================================================================ local ls = require("luasnip") ls.config.setup({ history = true, update_events = "TextChanged,TextChangedI", delete_check_events = "TextChanged", enable_autosnippets = true, }) require("luasnip.loaders.from_vscode").lazy_load() require("madol").setup({ rmarkdown = { snippets = { ["greek-tex"] = true, ["greek-unicode"] = false } } }) map({ "i", "s" }, "", function() if ls.choice_active() then ls.change_choice(1) end end, { desc = "LuaSnip next choice" }) map({ "i", "s" }, "", function() if ls.choice_active() then ls.change_choice(-1) end end, { desc = "LuaSnip previous choice" }) map({ "i", "s" }, "", function() if ls.expand_or_jumpable() then ls.expand_or_jump() end end, { desc = "LuaSnip expand/jump" }) map({ "i", "s" }, "", function() if ls.jumpable(-1) then ls.jump(-1) end end, { desc = "LuaSnip jump back" }) -- ============================================================================ -- blink.cmp -- ============================================================================ require("blink.cmp").setup({ snippets = { preset = "luasnip", }, completion = { menu = { auto_show = true }, ghost_text = { enabled = false }, documentation = { auto_show = true, auto_show_delay_ms = 200, }, list = { selection = { preselect = true, auto_insert = false, }, }, }, keymap = { preset = "none", [""] = { "show", "show_documentation", "hide_documentation" }, [""] = { "hide" }, [""] = { "scroll_documentation_up" }, [""] = { "scroll_documentation_down" }, [""] = { "accept", "fallback" }, [""] = { "select_next", "fallback" }, [""] = { "select_prev", "fallback" }, }, sources = { default = { "lsp", "path", "snippets", "buffer" }, }, fuzzy = { implementation = "prefer_rust_with_warning", prebuilt_binaries = { force_version = "v1.9.1", } }, }) -- ============================================================================ -- Copilot -- Included, but practically "off by default" because auto_trigger = false. -- Toggle it on/off with uc. -- ============================================================================ vim.api.nvim_create_autocmd("User", { pattern = "BlinkCmpMenuOpen", callback = function() vim.b.copilot_suggestion_hidden = true end, }) vim.api.nvim_create_autocmd("User", { pattern = "BlinkCmpMenuClose", callback = function() vim.b.copilot_suggestion_hidden = false end, }) require("copilot").setup({ panel = { enabled = false, }, suggestion = { enabled = true, auto_trigger = false, hide_during_completion = true, keymap = { accept = "", next = "", prev = "", dismiss = "", toggle_auto_trigger = false, }, }, }) map("n", "ac", function() require("copilot.suggestion").toggle_auto_trigger() end, { desc = "Toggle Copilot auto-trigger" }) map("n", "ap", function() require("copilot.panel").toggle() end, { desc = "Toggle Copilot panel" }) -- ============================================================================ -- autopairs / tabout / surround -- ============================================================================ require("nvim-autopairs").setup({ check_ts = true, }) require("tabout").setup({ tabkey = "", backwards_tabkey = "", act_as_tab = true, act_as_shift_tab = false, default_tab = "", default_shift_tab = "", enable_backwards = true, completion = false, ignore_beginning = true, tabouts = { { open = "'", close = "'" }, { open = '"', close = '"' }, { open = "`", close = "`" }, { open = "(", close = ")" }, { open = "[", close = "]" }, { open = "{", close = "}" }, }, }) require("nvim-surround").setup({}) -- ============================================================================ -- flash.nvim -- ============================================================================ require("flash").setup({}) map({ "n", "x", "o" }, "s", function() require("flash").jump() end, { desc = "Flash" }) map({ "n", "x", "o" }, "S", function() require("flash").treesitter() end, { desc = "Flash Treesitter" }) map("o", "r", function() require("flash").remote() end, { desc = "Remote Flash" }) map({ "o", "x" }, "R", function() require("flash").treesitter_search() end, { desc = "Treesitter Search" }) map("c", "", function() require("flash").toggle() end, { desc = "Toggle Flash Search" }) -- ============================================================================ -- marks.nvim -- ============================================================================ require("marks").setup({ default_mappings = true, builtin_marks = { ".", "<", ">", "^" }, cyclic = true, force_write_shada = false, refresh_interval = 250, }) map("n", "mb", "MarksListBuf", { silent = true, desc = "Marks in buffer" }) map("n", "ma", "MarksListAll", { silent = true, desc = "All marks" }) -- ============================================================================ -- snipe.nvim -- ============================================================================ require("snipe").setup({ ui = { position = "center", preselect_current = true, text_align = "file-first", }, hints = { dictionary = "sadflewcmpghio", }, navigate = { cancel_snipe = "", close_buffer = "D", open_vsplit = "V", open_split = "H", }, sort = "last", }) map("n", "", function() require("snipe").open_buffer_menu() end, { desc = "Snipe buffers" }) -- ============================================================================ -- Markdown / Typst -- ============================================================================ require("render-markdown").setup({ file_types = { "markdown" }, render_modes = { "n", "c", "t" }, }) require("typst-preview").setup({ follow_cursor = true, }) -- Typst map("n", "ptp", "TypstPreview", { silent = true, desc = "Typst preview" }) map("n", "ptP", "TypstPreviewToggle", { silent = true, desc = "Toggle Typst preview" }) -- Markdown map("n", "pmp", "MarkdownPreview", { silent = true, desc = "Markdown preview" }) map("n", "pmr", "RenderMarkdownToggle", { silent = true, desc = "Toggle render-markdown" }) -- ============================================================================ -- fzf-lua -- ============================================================================ require("fzf-lua").setup({ actions = { files = { ["enter"] = require("fzf-lua.actions").file_edit, } } }) map("n", "ff", function() require("fzf-lua").files() end, { desc = "Files" }) map("n", "db", function() require("fzf-lua").buffers() end, { desc = "Buffers" }) map("n", "bl", function() require("fzf-lua").buffers() end, { desc = "List buffers" }) map("n", "ft", function() require("fzf-lua").live_grep() end, { desc = "Live grep" }) map("n", "fs", function() require("fzf-lua").lsp_document_symbols() end, { desc = "Document symbols" }) map("n", "/", function() require("fzf-lua").grep_curbuf() end, { desc = "Grep current buffer" }) map("n", "fh", function() require("fzf-lua").help_tags() end, { desc = "Help tags" }) -- useful extras map("n", "fr", function() require("fzf-lua").oldfiles() end, { desc = "Recent files" }) map("n", "fg", function() require("fzf-lua").git_files() end, { desc = "Git files" }) map("n", "fc", function() require("fzf-lua").commands() end, { desc = "Commands" }) map("n", "fk", function() require("fzf-lua").keymaps() end, { desc = "Keymaps" }) map("n", "fd", function() require("fzf-lua").diagnostics_document() end, { desc = "Document diagnostics" }) map("n", "fD", function() require("fzf-lua").diagnostics_workspace() end, { desc = "Workspace diagnostics" }) map("n", "fw", function() require("fzf-lua").grep_cword() end, { desc = "Grep word under cursor" }) -- ============================================================================ -- neo-tree -- netrw remains default; neo-tree is opt-in on n -- ============================================================================ require("neo-tree").setup({ close_if_last_window = true, enable_git_status = true, enable_diagnostics = true, filesystem = { hijack_netrw_behavior = "disabled", follow_current_file = { enabled = true }, use_libuv_file_watcher = true, }, window = { width = 80, }, }) map("n", "e", "Neotree position=right toggle", { silent = true, desc = "Neo-tree", }) -- ============================================================================ -- Git -- ============================================================================ require("gitsigns").setup({ signs = { add = { text = "▎" }, change = { text = "▎" }, delete = { text = "" }, topdelete = { text = "" }, changedelete = { text = "▎" }, untracked = { text = "▎" }, }, on_attach = function(bufnr) local gs = package.loaded.gitsigns local function bufmap(mode, lhs, rhs, desc) vim.keymap.set(mode, lhs, rhs, { buffer = bufnr, silent = true, desc = desc }) end bufmap("n", "]h", gs.next_hunk, "Next hunk") bufmap("n", "[h", gs.prev_hunk, "Prev hunk") bufmap("n", "hs", gs.stage_hunk, "Stage hunk") bufmap("n", "hr", gs.reset_hunk, "Reset hunk") bufmap("n", "hS", gs.stage_buffer, "Stage buffer") bufmap("n", "hR", gs.reset_buffer, "Reset buffer") bufmap("n", "hp", gs.preview_hunk, "Preview hunk") bufmap("n", "hb", function() gs.blame_line({ full = true }) end, "Blame line") bufmap("n", "hd", gs.diffthis, "Diff this") bufmap("n", "hQ", gs.setqflist, "Hunks to quickfix") end, }) map("n", "gg", "LazyGit", { silent = true, desc = "LazyGit" }) map("n", "gs", "Git", { silent = true, desc = "Git status" }) map("n", "gb", "Git blame", { silent = true, desc = "Git blame" }) map("n", "gp", "Git push", { silent = true, desc = "Git push" }) map("n", "gl", "Git pull", { silent = true, desc = "Git pull" }) map("n", "gc", "Git commit", { silent = true, desc = "Git commit" }) -- ============================================================================ -- opencode.nvim -- ============================================================================ map({ "n", "x" }, "oa", function() require("opencode").ask("@this: ", { submit = true }) end, { desc = "Ask opencode" }) map({ "n", "x" }, "os", function() require("opencode").select() end, { desc = "Select opencode action" }) map({ "n", "t" }, "oo", function() require("opencode").toggle() end, { desc = "Toggle opencode" }) map({ "n", "x" }, "or", function() return require("opencode").operator("@this ") end, { expr = true, desc = "Send range to opencode" }) map("n", "ol", function() return require("opencode").operator("@this ") .. "_" end, { expr = true, desc = "Send line to opencode" }) map("n", "ou", function() require("opencode").command("session.half.page.up") end, { desc = "Opencode scroll up" }) map("n", "od", function() require("opencode").command("session.half.page.down") end, { desc = "Opencode scroll down" }) -- ============================================================================ -- LSP (native nightly flow) -- No Mason. Install server binaries yourself and keep them on PATH. -- ============================================================================ local capabilities = require("blink.cmp").get_lsp_capabilities() vim.diagnostic.config({ underline = true, severity_sort = true, update_in_insert = false, virtual_text = { spacing = 2, source = "if_many", }, float = { border = "rounded", source = "if_many", }, }) -- Global defaults for all enabled servers vim.lsp.config("*", { capabilities = capabilities, root_markers = { ".git" }, }) -- Per-server overrides vim.lsp.config("lua_ls", { settings = { Lua = { diagnostics = { globals = { "vim" }, }, workspace = { checkThirdParty = false, }, telemetry = { enable = false, }, }, }, }) -- Adjust this list to the servers you actually have installed. local servers = { "gopls", "lua_ls", "tinymist", "marksman", } for _, server in ipairs(servers) do vim.lsp.enable(server) end vim.api.nvim_create_autocmd("LspAttach", { callback = function(ev) local bufnr = ev.buf local function lmap(lhs, rhs, desc, mode) vim.keymap.set(mode or "n", lhs, rhs, { buffer = bufnr, silent = true, desc = desc }) end lmap("gd", vim.lsp.buf.definition, "Goto definition") lmap("gD", vim.lsp.buf.declaration, "Goto declaration") lmap("gi", vim.lsp.buf.implementation, "Goto implementation") lmap("gt", vim.lsp.buf.type_definition, "Goto type definition") lmap("gr", function() require("fzf-lua").lsp_references() end, "References") lmap("K", vim.lsp.buf.hover, "Hover") lmap("gK", vim.lsp.buf.signature_help, "Signature help") lmap("la", vim.lsp.buf.code_action, "Code action", { "n", "x" }) lmap("lr", vim.lsp.buf.rename, "Rename") lmap("lf", function() vim.lsp.buf.format({ async = true }) end, "Format") lmap("ld", vim.diagnostic.open_float, "Line diagnostics") lmap("ls", function() require("fzf-lua").lsp_document_symbols() end, "Document symbols") lmap("lS", function() require("fzf-lua").lsp_live_workspace_symbols() end, "Workspace symbols") lmap("[d", function() vim.diagnostic.jump({ count = -1, float = true }) end, "Prev diagnostic") lmap("]d", function() vim.diagnostic.jump({ count = 1, float = true }) end, "Next diagnostic") end, }) -- ============================================================================ -- lualine -- ============================================================================ local function opencode_statusline() local ok, opencode = pcall(require, "opencode") if ok and opencode.statusline then return opencode.statusline() end return "" end require("lualine").setup({ options = { section_separators = "", component_separators = "", globalstatus = true, theme = "auto", }, sections = { lualine_a = { "mode" }, lualine_b = { { "branch", icon = "" }, "diff" }, lualine_c = { { "diagnostics" }, { "filename", path = 1, symbols = { modified = " ●", readonly = " " } }, }, lualine_x = { "encoding", "fileformat", "filetype", opencode_statusline }, lualine_y = { "progress" }, lualine_z = { "location" }, }, })