commit a45929aff6d1269bf83756d96c15f3ecaf3d608c Author: xfy Date: Thu May 28 10:49:24 2026 +0800 Initial Neovim config - Add init.lua entry point - Add lua/ modules: options, keymaps, autocmds, usercmds, pack, treesitter, lsp - Add .gitignore - Add nvim-pack-lock.json for plugin version locking Co-Authored-By: Claude Opus 4.7 (1M context) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..950a1c4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.omc/ +others/ diff --git a/init.lua b/init.lua new file mode 100644 index 0000000..9098ce1 --- /dev/null +++ b/init.lua @@ -0,0 +1,9 @@ +require("vim._core.ui2").enable({}) + +require("options") +require("keymaps") +require("autocmds") +require("usercmds") +require("pack") +require("treesitter") +require("lsp") diff --git a/lua/autocmds.lua b/lua/autocmds.lua new file mode 100644 index 0000000..856cfc1 --- /dev/null +++ b/lua/autocmds.lua @@ -0,0 +1,36 @@ +local group = vim.api.nvim_create_augroup("UserAutocmds", { clear = true }) + +-- 恢复上次编辑位置 +vim.api.nvim_create_autocmd("BufReadPost", { + group = group, + callback = function() + local mark = vim.api.nvim_buf_get_mark(0, '"') + local row = mark[1] + local ft = vim.bo.filetype + + if row > 1 and row <= vim.fn.line("$") then + if ft ~= "commit" and ft ~= "gitcommit" and not ft:match("xxd") and not ft:match("gitrebase") then + vim.api.nvim_win_set_cursor(0, mark) + end + end + end, +}) + +-- 注释延续行为 +vim.api.nvim_create_autocmd("FileType", { + group = group, + callback = function() + vim.opt_local.formatoptions:remove("o") -- o/O 换行不延续注释 + vim.opt_local.formatoptions:append("r") -- 回车延续注释 + end, +}) + +-- 延迟设置 Treesitter 折叠(避免启动时加载 treesitter 模块) +vim.api.nvim_create_autocmd("BufEnter", { + group = group, + once = true, + callback = function() + vim.o.foldmethod = "expr" + vim.o.foldexpr = "v:lua.vim.treesitter.foldexpr()" + end, +}) diff --git a/lua/keymaps.lua b/lua/keymaps.lua new file mode 100644 index 0000000..253ea17 --- /dev/null +++ b/lua/keymaps.lua @@ -0,0 +1,154 @@ +vim.g.mapleader = " " + +local map = vim.keymap.set + +map("x", "p", [["_dP]], { desc = "Paste over selection without losing yanked text" }) +-- map({ "n", "v" }, "d", [["_d]], { desc = "Delete without yanking" }) + +map("n", "", ":nohl", { desc = "Clear search highlighting", silent = true }) + +map("v", "<", "", ">gv", { desc = "Indent and keep selection" }) + +map("n", "J", "mzJ`z", { desc = "Join lines without moving cursor" }) + +map("n", "ss", [[:%s/\<\>//gI]], + { desc = "Replace word cursor is on globally" }) +-- search +map("v", "ss", ":s/\\%V", { desc = "Search and replace in visual selection" }) + +-- general +map("n", "$", "g_") +map("v", "$", "g_") +map("v", ">", ">gv") +map("v", "<", "u", function() + vim.cmd.packadd("nvim.undotree") + require("undotree").open() +end, { desc = "Toggle Builtin Undotree" }) + +map("t", "", "", { desc = "Escape termainl" }) +map("n", "tt", ":term", { desc = "Open new terminal" }) + +-- tabs +map("n", "tc", ":tabclose", { desc = "Close current tab" }) +map("n", "tn", ":tabnew", { desc = "New tab" }) +map("n", "]", ":tabnext", { desc = "Next tab" }) +map("n", "[", ":tabprevious", { desc = "Previous tab" }) + + +-- yank path +map("n", "yp", function() + local path = vim.fn.expand("%") + vim.fn.setreg("+", path) + vim.notify("Copied relative path: " .. path, vim.log.levels.INFO) +end, { desc = "Yank relative file path" }) + +map("n", "yP", function() + local path = vim.fn.expand("%:p") + vim.fn.setreg("+", path) + vim.notify("Copied absolute path: " .. path, vim.log.levels.INFO) +end, { desc = "Yank absolute file path" }) + +-- buffer +map("n", "x", function() + local buf = vim.api.nvim_get_current_buf() + if vim.bo[buf].modified then + vim.ui.select({ "Yes", "No" }, { + prompt = "Buffer has unsaved changes. Close without saving?", + format_item = function(item) + return item + end, + }, function(choice) + if choice == "Yes" then + vim.api.nvim_buf_delete(buf, { force = true }) + end + end) + else + vim.cmd.bdelete() + end +end, { desc = "Close current buffer" }) +map("n", "bn", "enew", { desc = "Buffer new" }) + +map("n", "bo", function() + local current = vim.api.nvim_get_current_buf() + local skipped_ft = { "NvimTree", "oil", "aerial" } + for _, buf in ipairs(vim.api.nvim_list_bufs()) do + if buf ~= current and vim.api.nvim_buf_is_loaded(buf) then + local ft = vim.bo[buf].filetype + if not vim.tbl_contains(skipped_ft, ft) then + vim.api.nvim_buf_delete(buf, { force = true }) + end + end + end +end, { desc = "Close other buffers" }) + +-- mini.pick buffer picker (telescope buffers equivalent) +map("n", "", function() + local MiniPick = require("mini.pick") + + local delete_buf = function() + local matches = MiniPick.get_picker_matches() + local item = matches and matches.current + if not item or not item.bufnr then return end + local bufnr = item.bufnr + + -- 先删除 buffer(pcall 保护,避免删除当前窗口 buffer 时出错) + pcall(vim.api.nvim_buf_delete, bufnr, { force = true }) + + -- 刷新列表:只保留仍然有效的 buffer + if MiniPick.is_picker_active() then + local items = vim.tbl_filter(function(i) + return i.bufnr and vim.api.nvim_buf_is_valid(i.bufnr) + end, MiniPick.get_picker_items() or {}) + MiniPick.set_picker_items(items) + end + end + + -- 收集所有 buffer,过滤特殊类型和未列出的,按最近使用排序 + local bufs = vim.tbl_filter(function(b) + return vim.bo[b.bufnr].buftype == "" and b.listed == 1 + end, vim.fn.getbufinfo()) + table.sort(bufs, function(a, b) + return a.lastused > b.lastused + end) + + local items = {} + for _, info in ipairs(bufs) do + local name = info.name ~= "" and vim.fn.fnamemodify(info.name, ":.") or "[No Name]" + table.insert(items, { + text = name, + bufnr = info.bufnr, + }) + end + + MiniPick.start({ + source = { + name = "Buffers", + items = items, + show = function(buf_id, items_arr, query) + MiniPick.default_show(buf_id, items_arr, query, { show_icons = true }) + end, + }, + mappings = { + delete_buffer = { char = "", func = delete_buf }, + }, + }) +end, { desc = "Buffers" }) + +-- mini.pick filetype picker +map("n", "ft", function() + local MiniPick = require("mini.pick") + local filetypes = vim.fn.getcompletion("", "filetype") + MiniPick.start({ + source = { + name = "Filetypes", + items = filetypes, + choose = function(item) + vim.bo.filetype = item + end, + }, + }) +end, { desc = "Change filetype" }) diff --git a/lua/lsp.lua b/lua/lsp.lua new file mode 100644 index 0000000..02c3b82 --- /dev/null +++ b/lua/lsp.lua @@ -0,0 +1,52 @@ +require("mason").setup() + +vim.keymap.set('n', 'gd', vim.lsp.buf.definition, { desc = "Go to definition" }) +vim.keymap.set('n', 'gh', vim.lsp.buf.hover, { desc = "Hover" }) +vim.keymap.set("n", "fm", vim.lsp.buf.format, { desc = "Format Local buffer" }) +vim.keymap.set("n", "df", vim.diagnostic.open_float, { desc = "Show line diagnostics" }) +vim.keymap.set("n", "ca", vim.lsp.buf.code_action, { desc = "Code action" }) + +local diagnostic_goto = function(next, severity) + return function() + vim.diagnostic.jump({ + count = (next and 1 or -1) * vim.v.count1, + severity = severity and vim.diagnostic.severity[severity] or nil, + float = true, + }) + end +end + +vim.keymap.set("n", "]d", diagnostic_goto(true), { desc = "Next Diagnostic" }) +vim.keymap.set("n", "[d", diagnostic_goto(false), { desc = "Prev Diagnostic" }) +vim.keymap.set("n", "]e", diagnostic_goto(true, "ERROR"), { desc = "Next Error" }) +vim.keymap.set("n", "[e", diagnostic_goto(false, "ERROR"), { desc = "Prev Error" }) +vim.keymap.set("n", "]w", diagnostic_goto(true, "WARN"), { desc = "Next Warning" }) +vim.keymap.set("n", "[w", diagnostic_goto(false, "WARN"), { desc = "Prev Warning" }) + +vim.diagnostic.config({ virtual_text = true }) + +local capabilities = vim.lsp.protocol.make_client_capabilities() +capabilities = vim.tbl_deep_extend("force", capabilities, require("mini.completion").get_lsp_capabilities()) + +vim.lsp.config("*", { capabilities = capabilities }) + +vim.lsp.config("lua_ls", { + settings = { + Lua = { + diagnostics = { globals = { "vim" } }, + }, + }, +}) + +vim.lsp.enable({ + "html", + "cssls", + "gopls", + "vtsls", + "rust_analyzer", + "lua_ls", + "taplo", + "svelte", + "dartls", + "kotlin_lsp" +}) diff --git a/lua/options.lua b/lua/options.lua new file mode 100644 index 0000000..83c5c33 --- /dev/null +++ b/lua/options.lua @@ -0,0 +1,46 @@ +vim.g.netrw_banner = 0 + +vim.opt.nu = true +vim.opt.relativenumber = true +vim.opt.cursorline = true +vim.opt.cursorlineopt = "both" +vim.opt.autoread = true + +vim.opt.tabstop = 4 +vim.opt.softtabstop = 4 +vim.opt.shiftwidth = 4 +vim.opt.expandtab = true + +vim.opt.smartindent = true +vim.opt.inccommand = "split" + +vim.opt.splitbelow = true +vim.opt.splitright = true + +vim.opt.ignorecase = true +vim.opt.smartcase = true +vim.opt.laststatus = 3 + +vim.opt.swapfile = false +vim.opt.backup = false +vim.opt.undodir = vim.fn.stdpath("data") .. "/undodir" +vim.opt.undofile = true + +vim.opt.completeopt = "menuone,noselect,fuzzy,nosort" +vim.opt.shortmess:append("c") +vim.opt.clipboard:append("unnamedplus") +vim.opt.isfname:append("@-@") +vim.opt.guicursor = "" +vim.opt.scrolloff = 8 + +vim.opt.colorcolumn = "0" +vim.opt.signcolumn = "yes" + +vim.api.nvim_create_autocmd("TextYankPost", { + desc = "Highlight when yanking (copying) text", + callback = function() + vim.hl.on_yank() + end, +}) + +vim.opt.foldlevel = 99 -- 打开文件时默认展开所有折叠 diff --git a/lua/pack.lua b/lua/pack.lua new file mode 100644 index 0000000..3acf0e8 --- /dev/null +++ b/lua/pack.lua @@ -0,0 +1,91 @@ +vim.pack.add({ + "https://github.com/nvim-mini/mini.nvim", + "https://github.com/rafamadriz/friendly-snippets", + { src = "https://github.com/nvim-treesitter/nvim-treesitter", branch = "main" }, + "https://github.com/neovim/nvim-lspconfig", + "https://github.com/mason-org/mason.nvim", + "https://github.com/tpope/vim-fugitive", +}) + +-- mini files ---- +local MiniFiles = require("mini.files") +MiniFiles.setup({ + mappings = { + go_in = "", + go_in_plus = "L", + go_out = "_", + go_out_plus = "H", + }, +}) + +vim.keymap.set("n", "-", "lua MiniFiles.open()", { desc = "Toggle mini file explorer" }) +vim.keymap.set("n", "-", function() + MiniFiles.open(vim.api.nvim_buf_get_name(0), false) + MiniFiles.reveal_cwd() +end, { desc = "Toggle into currently opened file" }) + +---- mini notify ---- +require("mini.notify").setup({ + -- only show messages + content = { + format = function(notif) + return notif.msg + end, + }, +}) + +--- mini cmdline completion --- +require("mini.cmdline").setup({ + autocorrect = { enable = false } +}) + +--- mini surround --- +require("mini.surround").setup() +-- Default Keymaps +-- | `sa` | Add surrounding or Direct with 'saiw' | +-- | `sd` | Delete surrounding | +-- | `sr` | Replace surrounding | +-- | `sf` | Find surrounding (right) | +-- | `sF` | Find surrounding (left) | +-- | `sh` | Highlight surrounding | +-- | `sn` | Update n_lines | +-- | `l` / `n` | as suffix for prev/next | + +--- mini picker --- +local MiniPick = require("mini.pick") +local MiniExtra = require("mini.extra") +MiniPick.setup() +MiniExtra.setup() + +-- keymaps +vim.keymap.set("n", "ff", function() MiniPick.builtin.files() end, { desc = "Mini File Picker" }) +vim.keymap.set("n", "fw", function() MiniPick.builtin.grep({ pattern = vim.fn.expand("") }) end, { desc = "Grep word/Search word" }) +vim.keymap.set("n", "vh", function() MiniPick.builtin.help() end, { desc = "Mini Help" }) + +vim.keymap.set("n", "ds", function() MiniExtra.pickers.diagnostic() end, { desc = "Mini Picker Diagnostics" }) +vim.keymap.set("n", "sk", function() MiniExtra.pickers.keymaps() end, { desc = 'Search keymaps' }) + +--- mini completions --- +require("mini.completion").setup({ + lsp_completion = { + auto_setup = true, + } +}) + +--- mini snippets --- +local MiniSnippets = require("mini.snippets") +MiniSnippets.setup({ + snippets = { + MiniSnippets.gen_loader.from_lang(), -- loads friendly-snippets + }, +}) +MiniSnippets.start_lsp_server({ match = false }) + +--- mini diff and fugitive --- +local MiniDiff = require("mini.diff") +MiniDiff.setup({ + source = MiniDiff.gen_source.git({ index = false }), +}) + +vim.keymap.set("n", "gg", "tabnew | Git | only", { desc = "Fugitive Full Page New Tab" }) +vim.keymap.set("n", "gd", "Gvdiffsplit", { desc = "Git diff split", }) diff --git a/lua/treesitter.lua b/lua/treesitter.lua new file mode 100644 index 0000000..b307872 --- /dev/null +++ b/lua/treesitter.lua @@ -0,0 +1,49 @@ +local treesitter = require("nvim-treesitter") + +local ensure_installed = { + "lua", + "vim", + "vimdoc", + -- Web + "javascript", + "typescript", + "tsx", + "jsdoc", + "json", + "html", + "css", + "yaml", + -- Backend + "c", + "rust", + "toml", + "go", + "gomod", + "gosum", + "gowork", + -- Infra + "dockerfile", + "make", +} + +treesitter.install(ensure_installed) + +vim.api.nvim_create_autocmd("FileType", { + pattern = "*", + callback = function(args) + local buf = args.buf + local ft = vim.bo[buf].filetype + + local lang = vim.treesitter.language.get_lang(ft) + if not lang then + return + end + + local ok_add = pcall(vim.treesitter.language.add, lang) + if not ok_add then + return + end + + pcall(vim.treesitter.start, buf, lang) + end, +}) diff --git a/lua/usercmds.lua b/lua/usercmds.lua new file mode 100644 index 0000000..40d1eee --- /dev/null +++ b/lua/usercmds.lua @@ -0,0 +1,21 @@ +vim.api.nvim_create_user_command("PackAdd", function(opts) + vim.pack.add(opts.fargs) +end, { nargs = "+", desc = "Add plugins (:PackAdd user/repo1 user/repo2)" }) + +-- Pack Delete and Update cmds are built-in on Nightly 0.13 +vim.api.nvim_create_user_command("PackDel", function(opts) + vim.pack.del(opts.fargs) +end, { nargs = "+", desc = "Delete plugins (:PackDel plugin1 plugin2)" }) + +vim.api.nvim_create_user_command("PackUpdate", function(opts) + -- checks if any argument is passed + if opts.args:match("%S") then + -- update specific plugins + local plugins = vim.split(opts.args, "%s+", { trimempty = true }) + -- update only specified plugins + vim.pack.update(plugins) + else + -- update all + vim.pack.update() + end +end, { nargs = "*", desc = "Update all plugins or specific ones" }) diff --git a/nvim-pack-lock.json b/nvim-pack-lock.json new file mode 100644 index 0000000..02725f1 --- /dev/null +++ b/nvim-pack-lock.json @@ -0,0 +1,28 @@ +{ + "plugins": { + "friendly-snippets": { + "rev": "6cd7280adead7f586db6fccbd15d2cac7e2188b9", + "src": "https://github.com/rafamadriz/friendly-snippets" + }, + "mason.nvim": { + "rev": "bb639d4bf385a4d89f478b83af4d770be05ab7eb", + "src": "https://github.com/mason-org/mason.nvim" + }, + "mini.nvim": { + "rev": "44657837c7338e52727facc85c1d95bec1f6bd7c", + "src": "https://github.com/nvim-mini/mini.nvim" + }, + "nvim-lspconfig": { + "rev": "6f76a3eeadc2ee235d74cd7d5319e95a261084af", + "src": "https://github.com/neovim/nvim-lspconfig" + }, + "nvim-treesitter": { + "rev": "4916d6592ede8c07973490d9322f187e07dfefac", + "src": "https://github.com/nvim-treesitter/nvim-treesitter" + }, + "vim-fugitive": { + "rev": "3b753cf8c6a4dcde6edee8827d464ba9b8c4a6f0", + "src": "https://github.com/tpope/vim-fugitive" + } + } +}