From 4366e3d696cbe5fc1dc2da6df115bd41792f3f13 Mon Sep 17 00:00:00 2001 From: xfy Date: Thu, 28 May 2026 15:36:55 +0800 Subject: [PATCH] Add comprehensive Chinese comments and uniform 4-space indentation across all config files - Document every Lua module with detailed Chinese comments explaining purpose, design decisions, and key implementation details - Standardize indentation from tabs to 4 spaces for consistency - Add .claude/ to .gitignore Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitignore | 1 + init.lua | 114 +++++++---- lua/autocmds.lua | 98 +++++++--- lua/git.lua | 291 +++++++++++++++++---------- lua/keymaps.lua | 167 ++++++++++++---- lua/lazy.lua | 69 +++++++ lua/lsp.lua | 277 ++++++++++++++++---------- lua/options.lua | 158 ++++++++++++--- lua/pack.lua | 479 +++++++++++++++++++++++++++++---------------- lua/pick.lua | 172 ++++++++++------ lua/treesitter.lua | 58 ++++-- lua/usercmds.lua | 67 ++++++- 12 files changed, 1375 insertions(+), 576 deletions(-) diff --git a/.gitignore b/.gitignore index 950a1c4..0ad715e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .omc/ others/ +.claude/ diff --git a/init.lua b/init.lua index 1b875d3..5c7164f 100644 --- a/init.lua +++ b/init.lua @@ -1,42 +1,88 @@ +-- ============================================================================= +-- Neovim 配置文件入口 (init.lua) +-- ============================================================================= +-- 本文件是 Neovim 启动时第一个加载的配置文件。 +-- 设计目标:最小化启动开销,所有重模块均通过自定义懒加载框架延迟加载。 +-- +-- 文件加载顺序: +-- 1. init.lua → 禁用内置插件、设置 colorscheme、加载核心模块 +-- 2. options.lua → 全局选项 (number, indent, clipboard 等) +-- 3. keymaps.lua → 键位映射 (leader = space) +-- 4. autocmds.lua → 自动命令 (恢复光标位置、折叠、yank 高亮等) +-- 5. usercmds.lua → 自定义命令 (:PackAdd, :PackDel, :PackUpdate) +-- 6. pack.lua → 插件声明与懒加载配置 +-- ============================================================================= + +-- 记录启动时间点,用于在 starter 页脚展示启动耗时。 +-- vim.uv (Neovim 0.10+) 是 libuv 的绑定,提供高性能计时器; +-- vim.loop 是旧版兼容名,在 0.10 之前使用。 _G.nvim_start_time = (vim.uv or vim.loop).hrtime() --- 禁用不需要的内置插件,减少启动时 source 的 plugin 文件 -vim.g.loaded_2html_plugin = 1 -vim.g.loaded_getscript = 1 -vim.g.loaded_getscriptPlugin = 1 -vim.g.loaded_gzip = 1 -vim.g.loaded_logipat = 1 -vim.g.loaded_matchit = 1 -vim.g.loaded_matchparen = 1 -vim.g.loaded_netrw = 1 -vim.g.loaded_netrwFileHandlers = 1 -vim.g.loaded_netrwPlugin = 1 -vim.g.loaded_netrwSettings = 1 -vim.g.loaded_rrhelper = 1 -vim.g.loaded_spellfile_plugin = 1 -vim.g.loaded_tar = 1 -vim.g.loaded_tarPlugin = 1 -vim.g.loaded_tutor_mode_plugin = 1 -vim.g.loaded_vimball = 1 -vim.g.loaded_vimballPlugin = 1 -vim.g.loaded_zip = 1 -vim.g.loaded_zipPlugin = 1 -vim.g.loaded_rplugin = 1 -vim.g.loaded_tohtml = 1 -vim.g.loaded_syntax = 1 -vim.g.loaded_synmenu = 1 -vim.g.loaded_optwin = 1 -vim.g.loaded_compiler = 1 -vim.g.loaded_bugreport = 1 -vim.g.loaded_ftplugin = 1 +-- ============================================================================= +-- 禁用不需要的内置插件 +-- ============================================================================= +-- Neovim 默认会加载大量内置插件(netrw、tar、zip 等), +-- 这些插件会在 startup 阶段 source 对应的 plugin/*.vim 文件,增加启动时间。 +-- 通过设置 loaded_* 全局变量为 1(真值),可以跳过这些插件的加载。 +-- +-- 注意:loaded_syntax 设为 1 是为了避免旧版 Vim 兼容的 syntax 加载路径; +-- 实际语法高亮由 treesitter 接管,不受影响。 +vim.g.loaded_2html_plugin = 1 -- :TOhtml 命令(将 buffer 导出为 HTML) +vim.g.loaded_getscript = 1 -- GetScript 插件(自动下载脚本) +vim.g.loaded_getscriptPlugin = 1 -- GetScript 插件主体 +vim.g.loaded_gzip = 1 -- Gzip 压缩文件读写支持 +vim.g.loaded_logipat = 1 -- LogiPat 逻辑模式匹配 +vim.g.loaded_matchit = 1 -- % 匹配扩展(由 treesitter 或 mini.pairs 替代) +vim.g.loaded_matchparen = 1 -- 括号匹配高亮(由 treesitter 替代) +vim.g.loaded_netrw = 1 -- Netrw 文件浏览器(由 mini.files 替代) +vim.g.loaded_netrwFileHandlers = 1 -- Netrw 文件处理器 +vim.g.loaded_netrwPlugin = 1 -- Netrw 插件主体 +vim.g.loaded_netrwSettings = 1 -- Netrw 设置 +vim.g.loaded_rrhelper = 1 -- RRHelper(R 语言相关) +vim.g.loaded_spellfile_plugin = 1 -- 拼写文件自动下载 +vim.g.loaded_tar = 1 -- Tar 归档支持 +vim.g.loaded_tarPlugin = 1 -- Tar 插件主体 +vim.g.loaded_tutor_mode_plugin = 1 -- Vim Tutor 教程模式 +vim.g.loaded_vimball = 1 -- Vimball 归档格式 +vim.g.loaded_vimballPlugin = 1 -- Vimball 插件主体 +vim.g.loaded_zip = 1 -- Zip 归档支持 +vim.g.loaded_zipPlugin = 1 -- Zip 插件主体 +vim.g.loaded_rplugin = 1 -- 远程插件框架 +vim.g.loaded_tohtml = 1 -- :TOhtml 的另一入口 +vim.g.loaded_syntax = 1 -- 旧版 syntax 自动加载 +vim.g.loaded_synmenu = 1 -- Syntax 菜单 +vim.g.loaded_optwin = 1 -- 选项窗口 (:options) +vim.g.loaded_compiler = 1 -- 编译器插件自动加载 +vim.g.loaded_bugreport = 1 -- Bug 报告生成 +vim.g.loaded_ftplugin = 1 -- 文件类型插件(手动管理) + +-- ============================================================================= +-- 启用 Neovim 0.12+ 内置 UI 增强 +-- ============================================================================= +-- vim._core.ui2 是 Neovim 0.12 实验性的 UI 增强模块, +-- 启用后提供更现代的默认界面行为(如更好的消息处理)。 require("vim._core.ui2").enable({}) -require("options") -require("keymaps") -require("autocmds") -require("usercmds") -require("pack") +-- ============================================================================= +-- 加载核心配置模块 +-- ============================================================================= +-- 这些模块按顺序加载,后续模块可以依赖前面模块的设置。 +-- 例如 pack.lua 中使用的键位映射依赖于 keymaps.lua 中设置的 leader 键。 +require("options") -- 全局 vim 选项设置 +require("keymaps") -- 键位映射定义 +require("autocmds") -- 自动命令(autocmd) +require("usercmds") -- 用户自定义命令 +require("pack") -- 插件管理与懒加载配置 + +-- ============================================================================= +-- Colorscheme 设置 +-- ============================================================================= +-- moonflyTransparent:使 moonfly 主题的背景透明, +-- 即使当前使用 catppuccin-mocha,此变量仍保留以备切换主题。 vim.g.moonflyTransparent = true + +-- 应用 colorscheme。catppuccin-mocha 的定义在 colors/catppuccin-mocha.lua 中。 +-- 该文件是一个精简版,只包含 Mocha 变体的调色板和高亮组定义。 vim.cmd("colorscheme catppuccin-mocha") diff --git a/lua/autocmds.lua b/lua/autocmds.lua index 856cfc1..baa8308 100644 --- a/lua/autocmds.lua +++ b/lua/autocmds.lua @@ -1,36 +1,84 @@ +-- ============================================================================= +-- 自动命令配置 (lua/autocmds.lua) +-- ============================================================================= +-- 自动命令(autocmd)是事件驱动的回调机制, +-- 在特定编辑器事件(如打开文件、切换 buffer、进入插入模式等)发生时自动执行。 +-- +-- 所有 autocmd 归属于 "UserAutocmds" 组, +-- 使用 { clear = true } 确保每次重载配置时先清除旧的 autocmd, +-- 避免重复注册导致命令被多次触发。 +-- ============================================================================= + +-- 创建用户 autocmd 组,clear = true 表示创建前清空组内所有现有命令 local group = vim.api.nvim_create_augroup("UserAutocmds", { clear = true }) --- 恢复上次编辑位置 +-- --------------------------------------------------------------------------- +-- 1. 恢复上次编辑位置 +-- --------------------------------------------------------------------------- +-- 打开文件后,将光标恢复到上次关闭时的位置。 +-- 原理:Vim 在关闭文件时会保存光标位置到 `"` 标记(双引号标记), +-- 下次打开时可通过此标记恢复。 +-- +-- 跳过的情况: +-- - commit / gitcommit:Git 提交消息文件,总是从头开始编辑 +-- - xxd:十六进制编辑模式 +-- - gitrebase:Git rebase 交互式编辑 + 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 + group = group, + callback = function() + -- 获取 `"` 标记的位置(行, 列) + local mark = vim.api.nvim_buf_get_mark(0, '"') + local row = mark[1] -- 行号(1-based) + 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, + -- 仅当标记有效(行号 > 1 且在文件范围内)时恢复光标 + 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, }) --- 注释延续行为 +-- --------------------------------------------------------------------------- +-- 2. 注释延续行为 +-- --------------------------------------------------------------------------- +-- 控制按回车或 o/O 时是否自动延续注释符号(如 //、#、--)。 +-- +-- formatoptions 说明: +-- o - 使用 o/O 换行时延续注释(被移除,避免在 normal 模式下意外延续) +-- r - 按回车时延续注释(保留,insert 模式下回车延续注释符) +-- +-- 注意:此设置对自动格式化(conform.nvim)的行为也有影响。 + 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, + group = group, + callback = function() + -- 移除 'o':按 o/O 时不自动插入注释符号 + vim.opt_local.formatoptions:remove("o") + -- 追加 'r':按回车时自动插入注释符号 + vim.opt_local.formatoptions:append("r") + end, }) --- 延迟设置 Treesitter 折叠(避免启动时加载 treesitter 模块) +-- --------------------------------------------------------------------------- +-- 3. 延迟设置 Treesitter 折叠 +-- --------------------------------------------------------------------------- +-- 使用 treesitter 的 foldexpr 进行语法感知的代码折叠。 +-- 延迟到 BufEnter 时设置(而非启动时),避免在 startup 阶段加载 treesitter 模块。 +-- +-- once = true 表示此 autocmd 只触发一次,首次 BufEnter 后自动销毁。 +-- foldmethod = "expr" 表示使用表达式(foldexpr)计算折叠范围。 +-- foldexpr = "v:lua.vim.treesitter.foldexpr()" 是 Neovim 0.10+ 的内置函数, +-- 基于 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, + group = group, + once = true, + callback = function() + vim.o.foldmethod = "expr" + vim.o.foldexpr = "v:lua.vim.treesitter.foldexpr()" + end, }) diff --git a/lua/git.lua b/lua/git.lua index b5b1f90..c5eeddf 100644 --- a/lua/git.lua +++ b/lua/git.lua @@ -1,111 +1,202 @@ +-- ============================================================================= +-- Git 工具封装 (lua/git.lua) +-- ============================================================================= +-- 本模块提供基于 mini.diff 和原生 git 命令的 Git 相关功能。 +-- +-- 功能列表: +-- 1. Hunk Preview(ghp)— 预览当前光标处 git diff hunk +-- 2. Blame Line(ghb)— 查看当前行的 git blame 信息 +-- 3. Blame Buffer(gB)— 整文件 blame(via fugitive) +-- 4. File History(gD)— 查看当前文件的 git log +-- +-- 依赖: +-- - mini.diff(已在 pack.lua 中通过 BufReadPost 懒加载) +-- - vim-fugitive(已在 pack.lua 中通过按键触发懒加载) +-- ============================================================================= + +-- --------------------------------------------------------------------------- +-- 辅助函数:获取光标所在行的 hunk +-- --------------------------------------------------------------------------- +-- 遍历当前 buffer 的所有 hunks,找到包含光标所在行的那个。 +-- +-- 返回值: +-- hunk - hunk 对象 { type, buf_start, buf_count, ref_start, ref_count } +-- ref_text - 参考文本(HEAD 版本的内容) +-- nil - 光标不在任何 hunk 上 + local function get_cursor_hunk() - local buf = vim.api.nvim_get_current_buf() - local data = require("mini.diff").get_buf_data(buf) - if not data or not data.hunks or #data.hunks == 0 then - return nil - end - local cursor_line = vim.api.nvim_win_get_cursor(0)[1] - for _, h in ipairs(data.hunks) do - local start_line = math.max(h.buf_start, 1) - local end_line = h.buf_start + h.buf_count - 1 - if h.buf_count == 0 then - end_line = start_line - end - if cursor_line >= start_line and cursor_line <= end_line then - return h, data.ref_text - end - end - return nil + local buf = vim.api.nvim_get_current_buf() + -- 从 mini.diff 获取当前 buffer 的 diff 数据 + local data = require("mini.diff").get_buf_data(buf) + if not data or not data.hunks or #data.hunks == 0 then + return nil + end + + local cursor_line = vim.api.nvim_win_get_cursor(0)[1] + + for _, h in ipairs(data.hunks) do + -- hunk 在 buffer 中的起始行(1-based) + local start_line = math.max(h.buf_start, 1) + -- hunk 在 buffer 中的结束行 + local end_line = h.buf_start + h.buf_count - 1 + -- 空删除(0 行)时特殊处理 + if h.buf_count == 0 then + end_line = start_line + end + + -- 检查光标是否在此 hunk 范围内 + if cursor_line >= start_line and cursor_line <= end_line then + return h, data.ref_text + end + end + + return nil end +-- --------------------------------------------------------------------------- +-- Hunk Preview — 浮动窗口预览当前 hunk +-- --------------------------------------------------------------------------- +-- 创建一个居中的浮动窗口,显示光标所在 hunk 的详细 diff 信息。 +-- 支持三种 hunk 类型: +-- add — 显示新增的行 +-- delete — 显示被删除的行(从参考文本中恢复) +-- change — 并排显示修改前后的内容 + local function preview_hunk() - local hunk, ref_text = get_cursor_hunk() - if not hunk then - vim.notify("Cursor not on a hunk", vim.log.levels.WARN) - return - end - local lines = {} - table.insert(lines, "Hunk type: " .. hunk.type) - table.insert(lines, string.format("Buffer lines: %d-%d", hunk.buf_start, hunk.buf_start + hunk.buf_count - 1)) - table.insert(lines, string.format("Reference lines: %d-%d", hunk.ref_start, hunk.ref_start + hunk.ref_count - 1)) - table.insert(lines, "---") - local buf = vim.api.nvim_get_current_buf() - local ref_lines = vim.split(ref_text or "", "\n") - if hunk.type == "delete" then - table.insert(lines, "Deleted lines (from reference):") - for i = hunk.ref_start, hunk.ref_start + hunk.ref_count - 1 do - table.insert(lines, "- " .. (ref_lines[i] or "")) - end - elseif hunk.type == "add" then - table.insert(lines, "Added lines:") - for i = hunk.buf_start, hunk.buf_start + hunk.buf_count - 1 do - table.insert(lines, "+ " .. (vim.api.nvim_buf_get_lines(buf, i - 1, i, false)[1] or "")) - end - else - table.insert(lines, "Before (reference):") - for i = hunk.ref_start, hunk.ref_start + hunk.ref_count - 1 do - table.insert(lines, "- " .. (ref_lines[i] or "")) - end - table.insert(lines, "After (buffer):") - for i = hunk.buf_start, hunk.buf_start + hunk.buf_count - 1 do - table.insert(lines, "+ " .. (vim.api.nvim_buf_get_lines(buf, i - 1, i, false)[1] or "")) - end - end - local width = math.min(80, vim.o.columns - 4) - local height = math.min(#lines + 2, vim.o.lines - 4) - local preview_buf = vim.api.nvim_create_buf(false, true) - vim.api.nvim_buf_set_lines(preview_buf, 0, -1, false, lines) - vim.api.nvim_set_option_value("modifiable", false, { buf = preview_buf }) - vim.api.nvim_set_option_value("filetype", "diff", { buf = preview_buf }) - local win = vim.api.nvim_open_win(preview_buf, true, { - relative = "editor", - row = math.floor((vim.o.lines - height) / 2), - col = math.floor((vim.o.columns - width) / 2), - width = width, - height = height, - style = "minimal", - border = "rounded", - title = " Hunk Preview ", - title_pos = "center", - }) - vim.keymap.set("n", "q", function() - vim.api.nvim_win_close(win, true) - end, { buffer = preview_buf }) - vim.keymap.set("n", "", function() - vim.api.nvim_win_close(win, true) - end, { buffer = preview_buf }) + local hunk, ref_text = get_cursor_hunk() + if not hunk then + vim.notify("光标不在变更区域", vim.log.levels.WARN) + return + end + + -- 构建预览内容 + local lines = {} + table.insert(lines, "Hunk 类型: " .. hunk.type) + table.insert(lines, string.format("Buffer 行: %d-%d", hunk.buf_start, hunk.buf_start + hunk.buf_count - 1)) + table.insert(lines, string.format("参考行: %d-%d", hunk.ref_start, hunk.ref_start + hunk.ref_count - 1)) + table.insert(lines, "---") + + local buf = vim.api.nvim_get_current_buf() + local ref_lines = vim.split(ref_text or "", "\n") + + if hunk.type == "delete" then + -- 删除型 hunk:显示被删除的内容(来自参考文本) + table.insert(lines, "被删除的行(来自参考版本):") + for i = hunk.ref_start, hunk.ref_start + hunk.ref_count - 1 do + table.insert(lines, "- " .. (ref_lines[i] or "")) + end + elseif hunk.type == "add" then + -- 新增型 hunk:显示 buffer 中新增的内容 + table.insert(lines, "新增的行:") + for i = hunk.buf_start, hunk.buf_start + hunk.buf_count - 1 do + table.insert(lines, "+ " .. (vim.api.nvim_buf_get_lines(buf, i - 1, i, false)[1] or "")) + end + else + -- 修改型 hunk:显示修改前后的对比 + table.insert(lines, "修改前(参考版本):") + for i = hunk.ref_start, hunk.ref_start + hunk.ref_count - 1 do + table.insert(lines, "- " .. (ref_lines[i] or "")) + end + table.insert(lines, "修改后(当前版本):") + for i = hunk.buf_start, hunk.buf_start + hunk.buf_count - 1 do + table.insert(lines, "+ " .. (vim.api.nvim_buf_get_lines(buf, i - 1, i, false)[1] or "")) + end + end + + -- 计算浮动窗口尺寸 + local width = math.min(80, vim.o.columns - 4) + local height = math.min(#lines + 2, vim.o.lines - 4) + + -- 创建预览 buffer + local preview_buf = vim.api.nvim_create_buf(false, true) -- 无文件、可编辑 + vim.api.nvim_buf_set_lines(preview_buf, 0, -1, false, lines) + vim.api.nvim_set_option_value("modifiable", false, { buf = preview_buf }) + vim.api.nvim_set_option_value("filetype", "diff", { buf = preview_buf }) -- diff 语法高亮 + + -- 打开浮动窗口 + local win = vim.api.nvim_open_win(preview_buf, true, { + relative = "editor", + row = math.floor((vim.o.lines - height) / 2), + col = math.floor((vim.o.columns - width) / 2), + width = width, + height = height, + style = "minimal", + border = "rounded", + title = " Hunk Preview ", + title_pos = "center", + }) + + -- q / Esc 关闭窗口 + vim.keymap.set("n", "q", function() + vim.api.nvim_win_close(win, true) + end, { buffer = preview_buf }) + vim.keymap.set("n", "", function() + vim.api.nvim_win_close(win, true) + end, { buffer = preview_buf }) end +-- --------------------------------------------------------------------------- +-- Blame Line — 查看当前行的 Git blame 信息 +-- --------------------------------------------------------------------------- +-- 执行 git blame --porcelain 获取当前行的详细提交信息, +-- 以通知消息的形式展示提交哈希、作者、时间和提交摘要。 +-- +-- --porcelain 格式是机器可读的 blame 输出,包含以下字段: +-- [] +-- author +-- author-mail +-- author-time +-- author-tz +-- summary +-- ... + local function blame_line() - local file = vim.api.nvim_buf_get_name(0) - if file == "" then - vim.notify("No file name", vim.log.levels.WARN) - return - end - local line = vim.api.nvim_win_get_cursor(0)[1] - local cmd = { "git", "blame", "-L", line .. "," .. line, "--porcelain", file } - local output = vim.fn.system(cmd) - if vim.v.shell_error ~= 0 then - vim.notify("git blame failed", vim.log.levels.ERROR) - return - end - local hash = output:match("^(%x+)%s") or "?" - local author = output:match("author ([^\n]+)") or "?" - local email = output:match("author%-mail ([^\n]+)") or "?" - local time = output:match("author%-time (%d+)") - local summary = output:match("summary ([^\n]+)") or "?" - local time_str = time and os.date("%Y-%m-%d %H:%M", tonumber(time)) or "?" - vim.notify( - string.format("%s | %s %s | %s\n%s", hash:sub(1, 8), author, email, time_str, summary), - vim.log.levels.INFO - ) + local file = vim.api.nvim_buf_get_name(0) + if file == "" then + vim.notify("当前 buffer 无文件名", vim.log.levels.WARN) + return + end + + local line = vim.api.nvim_win_get_cursor(0)[1] + -- -L 限制 blame 范围为单行,提升性能 + local cmd = { "git", "blame", "-L", line .. "," .. line, "--porcelain", file } + local output = vim.fn.system(cmd) + + if vim.v.shell_error ~= 0 then + vim.notify("git blame 执行失败", vim.log.levels.ERROR) + return + end + + -- 解析 --porcelain 输出 + local hash = output:match("^(%x+)%s") or "?" + local author = output:match("author ([^\n]+)") or "?" + local email = output:match("author%-mail ([^\n]+)") or "?" + local time = output:match("author%-time (%d+)") + local summary = output:match("summary ([^\n]+)") or "?" + local time_str = time and os.date("%Y-%m-%d %H:%M", tonumber(time)) or "?" + + vim.notify( + string.format("%s | %s %s | %s\n%s", hash:sub(1, 8), author, email, time_str, summary), + vim.log.levels.INFO + ) end -vim.keymap.set("n", "ghp", preview_hunk, { desc = "Preview hunk" }) -vim.keymap.set("n", "ghb", blame_line, { desc = "Blame line" }) +-- --------------------------------------------------------------------------- +-- 键位映射 +-- --------------------------------------------------------------------------- + +-- ghp — 预览当前 hunk +vim.keymap.set("n", "ghp", preview_hunk, { desc = "预览 hunk" }) + +-- ghb — 查看当前行 blame +vim.keymap.set("n", "ghb", blame_line, { desc = "Blame 当前行" }) + +-- gB — 整文件 blame(使用 fugitive 的 :Git blame) vim.keymap.set("n", "gB", function() - vim.cmd("Git blame") -end, { desc = "Blame buffer" }) + vim.cmd("Git blame") +end, { desc = "Blame 整个文件" }) + +-- gD — 查看当前文件的 git 历史 vim.keymap.set("n", "gD", function() - vim.cmd("Git log -p -- %") -end, { desc = "Git file history" }) + vim.cmd("Git log -p -- %") +end, { desc = "查看文件 Git 历史" }) diff --git a/lua/keymaps.lua b/lua/keymaps.lua index de51839..00b7867 100644 --- a/lua/keymaps.lua +++ b/lua/keymaps.lua @@ -1,84 +1,177 @@ +-- ============================================================================= +-- 键位映射配置 (lua/keymaps.lua) +-- ============================================================================= +-- Leader 键设置为 Space,所有以 开头的映射均使用空格触发。 +-- +-- 映射模式说明: +-- n - normal 模式(默认) +-- v - visual / select 模式 +-- i - insert 模式 +-- t - terminal 模式 +-- x - visual 模式(不含 select) +-- +-- 按键分组: +-- f* → 查找/文件相关(由 pick.lua 补充) +-- g* → Git 相关(由 git.lua 补充) +-- b* → Buffer 管理 +-- y* → 复制/粘贴 +-- t* → Terminal/Tab +-- → 窗口导航与保存 +-- ============================================================================= + +-- 设置 Leader 键为空格。必须在任何 映射之前设置。 vim.g.mapleader = " " +-- 本地别名,简化映射代码 local map = vim.keymap.set -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" }) +-- --------------------------------------------------------------------------- +-- 基础编辑映射 +-- --------------------------------------------------------------------------- +-- 按 Esc 清除搜索高亮(/ ? 搜索后的匹配高亮) +-- silent = true 避免在命令行显示 :nohl 的反馈 +map("n", "", ":nohl", { desc = "清除搜索高亮", silent = true }) + +-- Visual 模式下缩进后保持选区,便于连续多次缩进 +map("v", "<", "", ">gv", { desc = "增加缩进并保持选区" }) + +-- J(合并行)时不移动光标。 +-- 原理:mz 设置标记 z,J 合并行,`z 跳回标记位置。 +map("n", "J", "mzJ`z", { desc = "合并行且不移动光标" }) + +-- --------------------------------------------------------------------------- +-- 搜索与替换 +-- --------------------------------------------------------------------------- + +-- 快速替换当前光标下的单词(全局替换)。 +-- 在命令行插入光标下的单词。 +-- 映射展开后形如 :%s/oldword/oldword/gI,光标停在末尾,可修改替换内容。 map( "n", "ss", - [[:%s/\<\>//gI]], - { desc = "Replace word cursor is on globally" } + [[:%s/<>/<>/gI]], + { desc = "全局替换光标下的单词" } ) -map("v", "ss", ":s/\\%V", { desc = "Search and replace in visual selection" }) --- general +-- Visual 模式下在选区范围内搜索替换 +-- \%V 是 Vim 正则中的可视区域限定符,确保替换只在选区内生效 +map("v", "ss", ":s/\\%V", { desc = "在可视选区内搜索替换" }) + +-- --------------------------------------------------------------------------- +-- 行尾操作符重映射 +-- --------------------------------------------------------------------------- +-- 将 $ 映射为 g_(行尾最后一个非空白字符), +-- 这比 $(真正的行尾,通常包含尾随空格)更符合直觉。 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" }) +end, { desc = "打开内置撤销树" }) -map("n", "", "w", { desc = "Save file" }) -map("n", "", "%y+", { desc = "Copy whole file" }) +-- --------------------------------------------------------------------------- +-- 文件操作 +-- --------------------------------------------------------------------------- -map("t", "", "", { desc = "Escape termainl" }) -map("n", "tt", ":term", { desc = "Open new terminal" }) +-- 保存当前文件(normal 模式) +map("n", "", "w", { desc = "保存文件" }) --- window navigation -map("n", "", "h", { desc = "Switch to left window" }) -map("n", "", "j", { desc = "Switch to down window" }) -map("n", "", "k", { desc = "Switch to up window" }) -map("n", "", "l", { desc = "Switch to right window" }) +-- 复制整行内容到系统剪贴板 +map("n", "", "%y+", { desc = "复制整个文件内容" }) --- 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" }) +-- --------------------------------------------------------------------------- +-- Terminal 模式 +-- --------------------------------------------------------------------------- --- yank path +-- Terminal 模式下按 返回 Normal 模式 +-- 是 Vim 内置的终端转义序列 +map("t", "", "", { desc = "从终端模式返回普通模式" }) + +-- 打开新的终端窗口 +map("n", "tt", ":term", { desc = "打开新终端" }) + +-- --------------------------------------------------------------------------- +-- 窗口导航 +-- --------------------------------------------------------------------------- +-- 使用 在窗口间快速跳转,替代 h/j/k/l 的繁琐操作。 + +map("n", "", "h", { desc = "切换到左侧窗口" }) +map("n", "", "j", { desc = "切换到下方窗口" }) +map("n", "", "k", { desc = "切换到上方窗口" }) +map("n", "", "l", { desc = "切换到右侧窗口" }) + +-- --------------------------------------------------------------------------- +-- Tab 管理 +-- --------------------------------------------------------------------------- + +map("n", "tc", ":tabclose", { desc = "关闭当前标签页" }) +map("n", "tn", ":tabnew", { desc = "新建标签页" }) +map("n", "]", ":tabnext", { desc = "下一个标签页" }) +map("n", "[", ":tabprevious", { desc = "上一个标签页" }) + +-- --------------------------------------------------------------------------- +-- 复制文件路径 +-- --------------------------------------------------------------------------- + +-- 复制当前文件的相对路径到系统剪贴板 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" }) + vim.notify("已复制相对路径: " .. path, vim.log.levels.INFO) +end, { desc = "复制相对文件路径" }) + +-- 复制当前文件的绝对路径到系统剪贴板 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" }) + vim.notify("已复制绝对路径: " .. path, vim.log.levels.INFO) +end, { desc = "复制绝对文件路径" }) --- buffers +-- --------------------------------------------------------------------------- +-- Buffer 管理 +-- --------------------------------------------------------------------------- + +-- 关闭当前 Buffer,有未保存更改时提示确认。 +-- 使用 vim.ui.select 提供交互式选择对话框。 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?", + vim.ui.select({ "是", "否" }, { + prompt = "Buffer 有未保存的更改,不保存就关闭吗?", format_item = function(item) return item end, }, function(choice) - if choice == "Yes" then + if choice == "是" 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" }) +end, { desc = "关闭当前 Buffer" }) + +-- 新建空 Buffer +map("n", "bn", "enew", { desc = "新建 Buffer" }) + +-- 关闭除当前 Buffer 外的所有 Buffer。 +-- 跳过指定文件类型的 Buffer(如文件管理器),避免误关闭侧边栏。 map("n", "bo", function() local current = vim.api.nvim_get_current_buf() - local skipped_ft = { "NvimTree", "oil", "aerial" } + 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 @@ -87,4 +180,4 @@ map("n", "bo", function() end end end -end, { desc = "Close other buffers" }) +end, { desc = "关闭其他 Buffer" }) diff --git a/lua/lazy.lua b/lua/lazy.lua index 016f831..878469a 100644 --- a/lua/lazy.lua +++ b/lua/lazy.lua @@ -1,7 +1,38 @@ +-- ============================================================================= +-- 自定义懒加载框架 (lua/lazy.lua) +-- ============================================================================= +-- 本模块与 lazy.nvim 插件管理器完全无关, +-- 是一个约 35 行的轻量级懒加载原语封装,用于延迟加载重型插件模块。 +-- +-- 核心设计: +-- - 通过 _loaded 表跟踪已加载模块,避免重复初始化 +-- - on_event:通过 autocmd 在特定事件(如 InsertEnter、BufReadPost)触发加载 +-- - on_keys:通过键位映射触发加载,首次按键时初始化插件 +-- - load:直接加载,用于手动触发或内部调用 +-- ============================================================================= + local M = {} +-- 已加载模块的标记表。 +-- 键为模块标识名(如 "treesitter", "lsp", "completion"), +-- 值为 true 表示该模块已完成初始化。 +-- 此表用于防止重复加载和重复执行 setup()。 M._loaded = {} +-- --------------------------------------------------------------------------- +-- 直接加载模块 +-- --------------------------------------------------------------------------- +-- 参数: +-- name - 模块标识名(自定义字符串,用于 _loaded 去重) +-- fn - 可选的初始化函数,在首次加载时执行 +-- +-- 行为: +-- 1. 检查 _loaded[name] 是否为真,是则直接返回(幂等) +-- 2. 标记为已加载 +-- 3. 如果有 fn,执行 fn() 完成插件 setup +-- +-- 使用场景: +-- 在按键回调中手动触发加载,或在 on_event 回调中延迟初始化。 M.load = function(name, fn) if M._loaded[name] then return @@ -12,6 +43,24 @@ M.load = function(name, fn) end end +-- --------------------------------------------------------------------------- +-- 事件触发懒加载 +-- --------------------------------------------------------------------------- +-- 参数: +-- name - 模块标识名 +-- event - Neovim autocmd 事件名(如 "InsertEnter", "BufReadPost", "VimEnter") +-- pattern - autocmd 匹配模式,默认为 "*" +-- fn - 初始化函数 +-- +-- 原理: +-- 创建一个一次性的 autocmd(once = true), +-- 当指定事件首次触发时,调用 M.load(name, fn) 完成初始化。 +-- 由于 once = true,autocmd 在触发后自动销毁,不会重复执行。 +-- +-- 典型用法: +-- lazy.on_event("completion", "InsertEnter", "*", function() +-- require("mini.completion").setup({ ... }) +-- end) M.on_event = function(name, event, pattern, fn) vim.api.nvim_create_autocmd(event, { pattern = pattern or "*", @@ -22,6 +71,26 @@ M.on_event = function(name, event, pattern, fn) }) end +-- --------------------------------------------------------------------------- +-- 按键触发懒加载 +-- --------------------------------------------------------------------------- +-- 参数: +-- name - 模块标识名 +-- keys - 键位序列(如 "ff", "gg") +-- mode - 映射模式,默认为 "n"(normal) +-- fn - 初始化函数,在首次按键时执行 +-- action - 可选的额外动作函数,在 fn 之后执行 +-- opts - 键位映射选项(desc, buffer 等) +-- +-- 原理: +-- 创建一个键位映射,首次按下时: +-- 1. 调用 M.load(name, fn) 完成插件初始化 +-- 2. 如果提供了 action,执行 action() +-- 3. 后续按键直接执行 action(因为 _loaded 已标记) +-- +-- 注意: +-- 此设计使得首次按键稍慢(需要 setup),后续按键与直接映射无异。 +-- 适合重型插件如 mini.pick、vim-fugitive。 M.on_keys = function(name, keys, mode, fn, action, opts) mode = mode or "n" opts = opts or {} diff --git a/lua/lsp.lua b/lua/lsp.lua index 89c7797..3118c35 100644 --- a/lua/lsp.lua +++ b/lua/lsp.lua @@ -1,126 +1,207 @@ +-- ============================================================================= +-- LSP、格式化与诊断配置 (lua/lsp.lua) +-- ============================================================================= +-- 本文件配置 Neovim 的 LSP 客户端、代码格式化(conform.nvim)和诊断导航。 +-- +-- 加载方式: +-- 由 pack.lua 通过 lazy.on_event("lsp", "VimEnter", "*", ...) 延迟加载, +-- 在 VimEnter 事件触发后初始化,避免阻塞 startup。 +-- +-- 依赖加载顺序: +-- 1. conform.nvim(BufWritePre 时按需 packadd 加载) +-- 2. nvim-lspconfig(直接 packadd) +-- 3. mason.nvim(直接 setup) +-- ============================================================================= + local lazy = require("lazy") --- conform.nvim - BufWritePre 懒加载 -local function setup_conform() - vim.cmd.packadd("conform.nvim") - local function biome_or_prettier() - if vim.fs.find({ "biome.json", "biome.jsonc" }, { upward = true, stop = vim.uv.os_homedir() })[1] then - return { "biome-check", "biome", stop_after_first = true } - end - return { "prettier" } - end +-- --------------------------------------------------------------------------- +-- conform.nvim — 代码格式化(BufWritePre 懒加载) +-- --------------------------------------------------------------------------- +-- conform.nvim 是一个轻量级格式化器包装器,支持保存时自动格式化。 +-- 由于格式化器通常不是立即需要的,通过 lazy.on_event 在 BufWritePre 时 +-- 才进行 packadd 和 setup。 - require("conform").setup({ - formatters_by_ft = { - lua = { "stylua" }, - go = { "gofumpt", "goimports" }, - javascript = biome_or_prettier, - typescript = biome_or_prettier, - javascriptreact = biome_or_prettier, - typescriptreact = biome_or_prettier, - json = biome_or_prettier, - css = { "prettier" }, - html = { "prettier" }, - markdown = { "prettier" }, - toml = { "taplo" }, - }, - format_on_save = function(bufnr) - if vim.b[bufnr].autoformat == false or vim.g.autoformat == false then - return nil - end - return { timeout_ms = 500, lsp_fallback = true } - end, - }) +local function setup_conform() + -- 通过 packadd 加载 conform.nvim(因为 vim.pack.add 时设置了 load = false) + vim.cmd.packadd("conform.nvim") + + -- 动态判断使用 Biome 还是 Prettier。 + -- 从当前文件向上搜索 biome.json / biome.jsonc,如果找到则使用 Biome, + -- 否则回退到 Prettier。stop 参数限制搜索到用户主目录,避免向上搜索到根目录。 + local function biome_or_prettier() + if vim.fs.find({ "biome.json", "biome.jsonc" }, { upward = true, stop = vim.uv.os_homedir() })[1] then + return { "biome-check", "biome", stop_after_first = true } + end + return { "prettier" } + end + + require("conform").setup({ + -- 按文件类型配置格式化器 + formatters_by_ft = { + lua = { "stylua" }, -- Lua 格式化 + go = { "gofumpt", "goimports" }, -- Go:先 gofumpt 格式化,再 goimports 整理导入 + javascript = biome_or_prettier, -- JS:Biome 或 Prettier + typescript = biome_or_prettier, -- TS:Biome 或 Prettier + javascriptreact = biome_or_prettier, -- JSX:Biome 或 Prettier + typescriptreact = biome_or_prettier, -- TSX:Biome 或 Prettier + json = biome_or_prettier, -- JSON:Biome 或 Prettier + css = { "prettier" }, -- CSS:Prettier + html = { "prettier" }, -- HTML:Prettier + markdown = { "prettier" }, -- Markdown:Prettier + toml = { "taplo" }, -- TOML:taplo + }, + + -- 保存时自动格式化回调 + -- 可通过 vim.b[bufnr].autoformat = false 关闭当前 buffer 的自动格式化 + -- 或通过 vim.g.autoformat = false 全局关闭 + format_on_save = function(bufnr) + if vim.b[bufnr].autoformat == false or vim.g.autoformat == false then + return nil -- 返回 nil 表示不格式化 + end + return { timeout_ms = 500, lsp_fallback = true } + end, + }) end +-- BufWritePre 时触发 conform 加载。 +-- 注意:conform 的 setup 内部会注册自己的 BufWritePre autocmd, +-- 但新创建的 autocmd 不会在同一次事件中触发。 +-- 因此这里手动调用 format 补偿第一次保存(确保首次保存也能格式化)。 lazy.on_event("conform", "BufWritePre", "*", function() - setup_conform() - -- conform 的 setup 注册了 BufWritePre,但新 autocmd 不会在同一次事件中触发 - -- 所以这里手动调用 format 补偿第一次保存 - local bufnr = vim.api.nvim_get_current_buf() - if vim.b[bufnr].autoformat ~= false and vim.g.autoformat ~= false then - require("conform").format({ bufnr = bufnr, timeout_ms = 500, lsp_fallback = true }) - end + setup_conform() + local bufnr = vim.api.nvim_get_current_buf() + if vim.b[bufnr].autoformat ~= false and vim.g.autoformat ~= false then + require("conform").format({ bufnr = bufnr, timeout_ms = 500, lsp_fallback = true }) + end end) --- mason + LSP 配置 +-- --------------------------------------------------------------------------- +-- Mason + LSP 配置 +-- --------------------------------------------------------------------------- +-- Mason 是 LSP 服务器、DAP 适配器和格式化工具的安装管理器。 +-- nvim-lspconfig 提供常见 LSP 服务器的预设配置。 + vim.cmd.packadd("nvim-lspconfig") require("mason").setup() +-- 构建 LSP 客户端 capabilities(能力声明),告知服务器客户端支持的功能。 +-- 先获取 Neovim 默认 capabilities,再与 mini.completion 的 LSP 补全能力合并。 local capabilities = vim.lsp.protocol.make_client_capabilities() capabilities = vim.tbl_deep_extend("force", capabilities, require("mini.completion").get_lsp_capabilities()) +-- 为所有 LSP 服务器设置默认 capabilities vim.lsp.config("*", { capabilities = capabilities }) +-- Lua 语言服务器特殊配置:将 "vim" 声明为全局变量,避免 "Undefined global" 诊断 vim.lsp.config("lua_ls", { - settings = { - Lua = { - diagnostics = { globals = { "vim" } }, - }, - }, + settings = { + Lua = { + diagnostics = { globals = { "vim" } }, + }, + }, }) --- Installed --- ◍ biome --- ◍ css-lsp --- ◍ gofumpt --- ◍ goimports --- ◍ golangci-lint --- ◍ gopls --- ◍ html-lsp --- ◍ kotlin-lsp --- ◍ lua-language-server --- ◍ prettier --- ◍ rust-analyzer --- ◍ stylua --- ◍ svelte-language-server --- ◍ vtsls +-- --------------------------------------------------------------------------- +-- 已安装的 Mason 工具清单(通过 :Mason 查看) +-- --------------------------------------------------------------------------- +-- 以下是通过 Mason 安装的 LSP 服务器和工具: +-- ◍ biome - JS/TS/JSON 格式化和 lint +-- ◍ css-lsp - CSS 语言服务器 +-- ◍ gofumpt - Go 格式化(stricter than gofmt) +-- ◍ goimports - Go 导入整理 +-- ◍ golangci-lint - Go linter +-- ◍ gopls - Go 语言服务器 +-- ◍ html-lsp - HTML 语言服务器 +-- ◍ kotlin-lsp - Kotlin 语言服务器 +-- ◍ lua-language-server- Lua 语言服务器 +-- ◍ prettier - 通用代码格式化器 +-- ◍ rust-analyzer - Rust 语言服务器 +-- ◍ stylua - Lua 格式化器 +-- ◍ svelte-language-server - Svelte 语言服务器 +-- ◍ taplo - TOML 工具 +-- ◍ vtsls - TypeScript 语言服务器(VS Code 的 TS 服务端移植) + +-- 启用指定的 LSP 服务器 vim.lsp.enable({ - "html", - "cssls", - "gopls", - "vtsls", - "rust_analyzer", - "lua_ls", - "taplo", - "svelte", - "dartls", - "kotlin_lsp", + "html", -- HTML 语言支持 + "cssls", -- CSS/SCSS/Less 语言支持 + "gopls", -- Go 语言支持 + "vtsls", -- TypeScript/JavaScript 支持(推荐替代 tsserver) + "rust_analyzer", -- Rust 语言支持 + "lua_ls", -- Lua 语言支持 + "taplo", -- TOML 支持 + "svelte", -- Svelte 框架支持 + "dartls", -- Dart/Flutter 支持 + "kotlin_lsp", -- Kotlin 支持 }) --- LSP keymaps(用 function 包装延迟 vim.lsp/vim.diagnostic 模块加载) -vim.keymap.set("n", "gd", function() - vim.lsp.buf.definition() -end, { desc = "Go to definition" }) -vim.keymap.set("n", "gh", function() - vim.lsp.buf.hover() -end, { desc = "Hover" }) -vim.keymap.set("n", "fm", function() - lazy.load("conform", setup_conform) - require("conform").format({ lsp_fallback = true }) -end, { desc = "Format buffer" }) -vim.keymap.set("n", "df", function() - vim.diagnostic.open_float() -end, { desc = "Show line diagnostics" }) -vim.keymap.set("n", "ca", function() - vim.lsp.buf.code_action() -end, { desc = "Code action" }) +-- --------------------------------------------------------------------------- +-- LSP 键位映射 +-- --------------------------------------------------------------------------- +-- 使用函数包装器包裹 LSP 调用,延迟加载 vim.lsp / vim.diagnostic 模块, +-- 避免在启动时预加载这些重型模块。 +-- gd - Go to Definition:跳转到定义位置 +vim.keymap.set("n", "gd", function() + vim.lsp.buf.definition() +end, { desc = "跳转到定义" }) + +-- gh - Hover:显示光标下符号的文档悬浮窗 +vim.keymap.set("n", "gh", function() + vim.lsp.buf.hover() +end, { desc = "悬停查看文档" }) + +-- fm - 手动格式化当前 buffer +vim.keymap.set("n", "fm", function() + lazy.load("conform", setup_conform) + require("conform").format({ lsp_fallback = true }) +end, { desc = "格式化当前 Buffer" }) + +-- df - 显示当前行的诊断浮动窗口 +vim.keymap.set("n", "df", function() + vim.diagnostic.open_float() +end, { desc = "显示行诊断" }) + +-- ca - Code Action:显示可用的代码操作(如自动修复、重构) +vim.keymap.set("n", "ca", function() + vim.lsp.buf.code_action() +end, { desc = "代码操作" }) + +-- --------------------------------------------------------------------------- +-- 诊断导航 +-- --------------------------------------------------------------------------- +-- 辅助函数:创建诊断跳转的闭包。 +-- 参数: +-- next - true 表示向后跳,false 表示向前跳 +-- severity - 可选的严重性过滤("ERROR", "WARN", "INFO", "HINT") +-- +-- vim.v.count1 是计数前缀的默认值(无数字时为 1), +-- 支持 3]d 跳转到第 3 个诊断。 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 + 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" }) +-- ]d / [d - 下一个 / 上一个诊断(所有级别) +vim.keymap.set("n", "]d", diagnostic_goto(true), { desc = "下一个诊断" }) +vim.keymap.set("n", "[d", diagnostic_goto(false), { desc = "上一个诊断" }) +-- ]e / [e - 下一个 / 上一个错误 +vim.keymap.set("n", "]e", diagnostic_goto(true, "ERROR"), { desc = "下一个错误" }) +vim.keymap.set("n", "[e", diagnostic_goto(false, "ERROR"), { desc = "上一个错误" }) + +-- ]w / [w - 下一个 / 上一个警告 +vim.keymap.set("n", "]w", diagnostic_goto(true, "WARN"), { desc = "下一个警告" }) +vim.keymap.set("n", "[w", diagnostic_goto(false, "WARN"), { desc = "上一个警告" }) + +-- --------------------------------------------------------------------------- +-- 诊断显示配置 +-- --------------------------------------------------------------------------- +-- 在代码行右侧显示诊断文本(virtual text)。 vim.diagnostic.config({ virtual_text = true }) diff --git a/lua/options.lua b/lua/options.lua index c8a91e9..97a09ed 100644 --- a/lua/options.lua +++ b/lua/options.lua @@ -1,53 +1,151 @@ +-- ============================================================================= +-- 全局选项配置 (lua/options.lua) +-- ============================================================================= +-- 本文件集中设置所有影响编辑器行为的全局选项。 +-- 选项分为以下几类: +-- 1. 界面显示(行号、光标、颜色) +-- 2. 缩进与格式化(tab、空格、智能缩进) +-- 3. 搜索行为(大小写敏感) +-- 4. 窗口与分割 +-- 5. 文件持久化(swap、backup、undo) +-- 6. 补全与消息 +-- 7. 剪贴板(延迟初始化,避免启动阻塞) +-- 8. 折叠与 diff 高亮 +-- ============================================================================= + +-- 禁用 netrw 横幅(netrw 本身已通过 loaded_netrw 禁用,此选项作为保险) 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 +-- --------------------------------------------------------------------------- +-- 1. 界面显示 +-- --------------------------------------------------------------------------- -vim.opt.tabstop = 4 -vim.opt.softtabstop = 4 -vim.opt.shiftwidth = 4 -vim.opt.expandtab = true +vim.opt.nu = true -- 显示绝对行号 +vim.opt.relativenumber = true -- 显示相对行号(便于配合数字 + j/k 跳转) +vim.opt.cursorline = true -- 高亮当前行 +vim.opt.cursorlineopt = "both" -- 高亮当前行的行号和文本行(number + line) +vim.opt.autoread = true -- 当文件被外部修改时自动重新读取 -vim.opt.smartindent = true -vim.opt.inccommand = "split" +-- --------------------------------------------------------------------------- +-- 2. 缩进与格式化 +-- --------------------------------------------------------------------------- +-- 使用 4 空格缩进,适用于大多数语言。 +-- 特定语言的缩进设置(如 JS/TS 的 2 空格)应在 ftplugin 中覆盖。 -vim.opt.splitbelow = true -vim.opt.splitright = true +vim.opt.tabstop = 4 -- Tab 键显示的宽度(字符数) +vim.opt.softtabstop = 4 -- 插入模式下 Tab/BS 的行为宽度 +vim.opt.shiftwidth = 4 -- 自动缩进和 >/< 操作的宽度 +vim.opt.expandtab = true -- 将 Tab 键转换为空格 -vim.opt.ignorecase = true -vim.opt.smartcase = true -vim.opt.laststatus = 3 +vim.opt.smartindent = true -- 基于语法的智能缩进(C-style 语言效果最佳) +vim.opt.inccommand = "split" -- 实时预览替换效果(:s 命令在分屏中显示预览) -vim.opt.swapfile = false -vim.opt.backup = false -vim.opt.undodir = vim.fn.stdpath("data") .. "/undodir" -vim.opt.undofile = true +-- --------------------------------------------------------------------------- +-- 3. 窗口与分割 +-- --------------------------------------------------------------------------- +vim.opt.splitbelow = true -- 水平分割时新窗口在下方 +vim.opt.splitright = true -- 垂直分割时新窗口在右侧 + +-- --------------------------------------------------------------------------- +-- 4. 搜索行为 +-- --------------------------------------------------------------------------- + +vim.opt.ignorecase = true -- 搜索时忽略大小写 +vim.opt.smartcase = true -- 如果搜索包含大写字母,则区分大小写 + -- 与 ignorecase 配合:全小写时忽略大小写, + -- 包含大写时精确匹配 + +-- --------------------------------------------------------------------------- +-- 5. 状态栏 +-- --------------------------------------------------------------------------- + +vim.opt.laststatus = 3 -- 全局状态栏(所有窗口共享一个状态栏) + -- 值为 2 时每个窗口有独立状态栏 + +-- --------------------------------------------------------------------------- +-- 6. 文件持久化 +-- --------------------------------------------------------------------------- +-- 禁用 swap 和 backup 文件,使用 undofile 实现跨会话的撤销历史。 + +vim.opt.swapfile = false -- 禁用交换文件(.swp) +vim.opt.backup = false -- 禁用备份文件(~ 后缀) +vim.opt.undodir = vim.fn.stdpath("data") .. "/undodir" -- 撤销历史存放目录 +vim.opt.undofile = true -- 启用持久化撤销(关闭文件后仍能撤销) + +-- --------------------------------------------------------------------------- +-- 7. 补全与消息 +-- --------------------------------------------------------------------------- + +-- 补全选项: +-- menuone - 即使只有一个匹配项也显示菜单 +-- noselect - 不自动选择第一项(需手动选择) +-- fuzzy - 启用模糊匹配(Neovim 0.11+) +-- nosort - 保持原始顺序,不按字母排序 vim.opt.completeopt = "menuone,noselect,fuzzy,nosort" + +-- shortmess: 缩短各种消息提示 +-- c - 补全相关消息的缩短(如 "match 1 of 5" → "1/5") vim.opt.shortmess:append("c") --- 延迟初始化 clipboard,避免启动时 provider 检测阻塞(SSH 环境尤其明显) + +-- --------------------------------------------------------------------------- +-- 8. 剪贴板(延迟初始化) +-- --------------------------------------------------------------------------- +-- 剪贴板集成(unnamedplus)会在启动时检测外部剪贴板提供者(如 xclip、wl-copy)。 +-- 在 SSH 远程环境中,此检测可能阻塞数秒。 +-- 通过 vim.schedule() 将剪贴板设置推迟到启动事件循环之后, +-- 避免阻塞 startup 的关键路径。 vim.schedule(function() - vim.opt.clipboard:append("unnamedplus") + vim.opt.clipboard:append("unnamedplus") end) + +-- --------------------------------------------------------------------------- +-- 9. 其他杂项 +-- --------------------------------------------------------------------------- + +-- 将 @ 和 - 视为文件名的一部分(用于 gf/gx 等命令) vim.opt.isfname:append("@-@") + +-- 保持光标与屏幕边缘的最小距离(行数),确保上下文可见 vim.opt.scrolloff = 8 +-- 禁用 colorcolumn(右侧参考线) vim.opt.colorcolumn = "0" + +-- 始终显示符号列(用于诊断、git diff 等标记),避免文本左右跳动 vim.opt.signcolumn = "yes" + +-- 启用真彩色支持(24-bit RGB),colorscheme 需要此选项才能正确渲染 vim.opt.termguicolors = true -vim.api.nvim_set_hl(0, "MiniDiffSignAdd", { link = "DiffAdd" }) -vim.api.nvim_set_hl(0, "MiniDiffSignChange", { link = "DiffChange" }) -vim.api.nvim_set_hl(0, "MiniDiffSignDelete", { link = "DiffDelete" }) +-- --------------------------------------------------------------------------- +-- 10. 高亮组链接 +-- --------------------------------------------------------------------------- +-- 将 mini.diff 的符号高亮组链接到标准 diff 高亮组, +-- 使 git diff 的添加/修改/删除标记使用 colorscheme 定义的配色。 + +vim.api.nvim_set_hl(0, "MiniDiffSignAdd", { link = "DiffAdd" }) -- 添加的行 +vim.api.nvim_set_hl(0, "MiniDiffSignChange", { link = "DiffChange" }) -- 修改的行 +vim.api.nvim_set_hl(0, "MiniDiffSignDelete", { link = "DiffDelete" }) -- 删除的行 + +-- --------------------------------------------------------------------------- +-- 11. Yank 高亮 +-- --------------------------------------------------------------------------- +-- 复制文本时短暂高亮被复制的区域,提供视觉反馈。 +-- vim.hl.on_yank() 是 Neovim 0.11+ 的内置函数, +-- 旧版使用 vim.highlight.on_yank()。 vim.api.nvim_create_autocmd("TextYankPost", { - desc = "Highlight when yanking (copying) text", - callback = function() - vim.hl.on_yank() - end, + desc = "复制文本时高亮被复制的区域", + callback = function() + vim.hl.on_yank() + end, }) -vim.opt.foldlevel = 99 -- 打开文件时默认展开所有折叠 +-- --------------------------------------------------------------------------- +-- 12. 折叠设置 +-- --------------------------------------------------------------------------- +-- foldlevel = 99 表示默认展开所有折叠。 +-- 实际的 foldmethod 和 foldexpr 在 autocmds.lua 中通过 BufEnter 延迟设置, +-- 避免启动时加载 treesitter 模块。 +vim.opt.foldlevel = 99 diff --git a/lua/pack.lua b/lua/pack.lua index 35183bc..351fc2d 100644 --- a/lua/pack.lua +++ b/lua/pack.lua @@ -1,231 +1,376 @@ +-- ============================================================================= +-- 插件管理与懒加载配置 (lua/pack.lua) +-- ============================================================================= +-- 本文件使用 Neovim 0.12+ 内置的 vim.pack 管理插件, +-- 并结合自定义懒加载框架 (lua/lazy.lua) 延迟初始化重型插件。 +-- +-- 插件列表: +-- mini.nvim - 单体插件集(starter、pick、extra、files、icons、 +-- notify、cmdline、completion、snippets、diff、surround) +-- friendly-snippets - 社区代码片段集合 +-- nvim-treesitter - 语法树解析与高亮 +-- nvim-lspconfig - LSP 客户端配置 +-- mason.nvim - LSP/DAP/格式化工具安装管理器 +-- conform.nvim - 代码格式化(保存时自动格式化) +-- vim-fugitive - Git 集成 +-- grug-far.nvim - 搜索与替换 +-- +-- 懒加载策略: +-- InsertEnter → completion, snippets +-- BufReadPost → diff, surround +-- VimEnter → treesitter, lsp +-- 按键触发 → pick, fugitive, files, grugfar +-- BufWritePre → conform +-- ============================================================================= + local lazy = require("lazy") local pick = require("pick") require("git") +-- 缓存启动耗时。pack.lua 在 init.lua 末尾加载,此时核心配置已全部就绪, +-- 计算出的时间就是真正的启动耗时。避免 starter 页脚在后续渲染时数值持续增长。 +local startup_ms = ((vim.uv or vim.loop).hrtime() - _G.nvim_start_time) / 1e6 + +-- --------------------------------------------------------------------------- +-- 插件安装声明 +-- --------------------------------------------------------------------------- +-- vim.pack.add(urls, { load = false }) 将插件下载到 pack 目录, +-- 但不自动加载(load = false)。后续通过 packadd 或 require 按需加载。 +-- 所有插件的状态锁定在 nvim-pack-lock.json 中。 + 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/stevearc/conform.nvim", - "https://github.com/tpope/vim-fugitive", - "https://github.com/MagicDuck/grug-far.nvim", + "https://github.com/nvim-mini/mini.nvim", -- 核心 UI/功能插件集 + "https://github.com/rafamadriz/friendly-snippets", -- 代码片段库 + { src = "https://github.com/nvim-treesitter/nvim-treesitter", branch = "main" }, -- 语法树 + "https://github.com/neovim/nvim-lspconfig", -- LSP 配置 + "https://github.com/mason-org/mason.nvim", -- 工具安装器 + "https://github.com/stevearc/conform.nvim", -- 格式化 + "https://github.com/tpope/vim-fugitive", -- Git 集成 + "https://github.com/MagicDuck/grug-far.nvim", -- 搜索替换 }, { load = false }) --- mini.starter 启动页 +-- --------------------------------------------------------------------------- +-- mini.starter — 启动页 +-- --------------------------------------------------------------------------- +-- 每次打开 Neovim(无文件参数时)显示的欢迎页面, +-- 展示 Neovim ASCII Logo 和启动耗时统计。 + local starter = require("mini.starter") +-- 统计已安装插件数量(用于页脚显示 "Loaded X/Y plugins") +-- 遍历 stdpath("data")/site/pack/core/opt/ 目录下的所有子目录 local function get_total_plugins() - local pack_dir = vim.fn.stdpath("data") .. "/site/pack/core/opt" - local paths = vim.fn.glob(pack_dir .. "/*", false, true) - local count = 0 - for _, path in ipairs(paths) do - if vim.fn.isdirectory(path) == 1 then - count = count + 1 - end - end - return count + local pack_dir = vim.fn.stdpath("data") .. "/site/pack/core/opt" + local paths = vim.fn.glob(pack_dir .. "/*", false, true) + local count = 0 + for _, path in ipairs(paths) do + if vim.fn.isdirectory(path) == 1 then + count = count + 1 + end + end + return count end +-- Neovim ASCII Art Logo(使用 Unicode 方块字符绘制) local header_lines = { - "", - "███╗ ██╗███████╗ ██████╗ ██╗ ██╗██╗███╗ ███╗", - "████╗ ██║██╔════╝██╔═══██╗██║ ██║██║████╗ ████║", - "██╔██╗ ██║█████╗ ██║ ██║██║ ██║██║██╔████╔██║", - "██║╚██╗██║██╔══╝ ██║ ██║╚██╗ ██╔╝██║██║╚██╔╝██║", - "██║ ╚████║███████╗╚██████╔╝ ╚████╔╝ ██║██║ ╚═╝ ██║", - "╚═╝ ╚═══╝╚══════╝ ╚═════╝ ╚═══╝ ╚═╝╚═╝ ╚═╝", - "", + "", + "███╗ ██╗███████╗ ██████╗ ██╗ ██╗██╗███╗ ███╗", + "████╗ ██║██╔════╝██╔═══██╗██║ ██║██║████╗ ████║", + "██╔██╗ ██║█████╗ ██║ ██║██║ ██║██║██╔████╔██║", + "██║╚██╗██║██╔══╝ ██║ ██║╚██╗ ██╔╝██║██║╚██╔╝██║", + "██║ ╚████║███████╗╚██████╔╝ ╚████╔╝ ██║██║ ╚═╝ ██║", + "╚═╝ ╚═══╝╚══════╝ ╚═════╝ ╚═══╝ ╚═╝╚═╝ ╚═╝", + "", } +-- 计算 Logo 的最大显示宽度,用于页脚居中计算 local max_header_width = 0 for _, line in ipairs(header_lines) do - max_header_width = math.max(max_header_width, vim.fn.strdisplaywidth(line)) + max_header_width = math.max(max_header_width, vim.fn.strdisplaywidth(line)) end starter.setup({ - autoopen = true, - evaluate_single = false, - items = { { name = " ", action = "", section = "" } }, - header = table.concat(header_lines, "\n"), - footer = function() - local uv = vim.uv or vim.loop - local ms = (uv:hrtime() - _G.nvim_start_time) / 1e6 - local loaded = vim.tbl_count(require("lazy")._loaded) - local total = get_total_plugins() - local text = string.format(" Loaded %d/%d plugins in %.0f ms", loaded, total, ms) - local text_width = vim.fn.strdisplaywidth(text) - local pad = math.floor((max_header_width - text_width) / 2) - return string.rep(" ", pad) .. text - end, - content_hooks = { - starter.gen_hook.aligning("center", "center"), - }, + autoopen = true, -- 无参数启动时自动打开 + evaluate_single = false, -- 只有一个选项时不自动执行 + items = { { name = " ", action = "", section = "" } }, -- 空选项(仅展示页眉页脚) + header = table.concat(header_lines, "\n"), -- Logo 文本 + + -- 页脚函数:显示启动耗时和插件加载统计 + footer = function() + -- 已加载的懒加载模块数(动态统计) + local loaded = vim.tbl_count(require("lazy")._loaded) + local total = get_total_plugins() + -- startup_ms 在 pack.lua 加载时一次性计算,避免后续渲染时数值变化 + local text = string.format(" Loaded %d/%d plugins in %.0f ms", loaded, total, startup_ms) + local text_width = vim.fn.strdisplaywidth(text) + -- 计算左填充空格数以实现居中 + local pad = math.floor((max_header_width - text_width) / 2) + return string.rep(" ", pad) .. text + end, + + -- 内容钩子:水平和垂直居中 + content_hooks = { + starter.gen_hook.aligning("center", "center"), + }, }) --- mini.files - 按键触发 -lazy.on_keys("files", "-", "n", function() - require("mini.files").setup({ - mappings = { - go_in = "", - go_in_plus = "L", - go_out = "_", - go_out_plus = "H", - }, - }) -end, function() - require("mini.files").open() -end, { desc = "Toggle mini file explorer" }) +-- --------------------------------------------------------------------------- +-- mini.files — 文件浏览器(按键触发懒加载) +-- --------------------------------------------------------------------------- +-- 提供悬浮文件浏览器,支持目录导航和文件操作。 -lazy.on_keys("files", "-", "n", function() - require("mini.files").setup({ - mappings = { - go_in = "", - go_in_plus = "L", - go_out = "_", - go_out_plus = "H", - }, - }) -end, function() - local MiniFiles = require("mini.files") - MiniFiles.open(vim.api.nvim_buf_get_name(0), false) - MiniFiles.reveal_cwd() -end, { desc = "Toggle into currently opened file" }) - --- mini.icons - 首次需要时加载(VimEnter 后延迟) -vim.api.nvim_create_autocmd("VimEnter", { - once = true, - callback = function() - require("mini.icons").setup() - lazy._loaded["icons"] = true - end, -}) - --- mini.notify - 首次 vim.notify 调用时加载 -vim.notify = function(msg, level, opts) - require("mini.notify").setup({ - content = { - format = function(notif) - return notif.msg - end, - }, - }) - vim.notify = require("mini.notify").make_notify() - return vim.notify(msg, level, opts) +-- mini.files setup 配置(复用) +local function setup_mini_files() + require("mini.files").setup({ + mappings = { + go_in = "", -- 回车进入目录或打开文件 + go_in_plus = "L", -- L 进入并同步光标 + go_out = "_", -- _ 返回上级目录 + go_out_plus = "H", -- H 返回上级并同步光标 + }, + }) end --- mini.cmdline - 首次按 : 时加载 +-- - :在当前工作目录打开文件浏览器 +lazy.on_keys("files", "-", "n", setup_mini_files, function() + require("mini.files").open() +end, { desc = "打开文件浏览器(工作目录)" }) + +-- - :在当前文件所在目录打开文件浏览器 +lazy.on_keys("files", "-", "n", setup_mini_files, function() + local MiniFiles = require("mini.files") + -- 打开当前文件所在目录,并定位到当前文件 + MiniFiles.open(vim.api.nvim_buf_get_name(0), false) + MiniFiles.reveal_cwd() +end, { desc = "打开文件浏览器(当前文件目录)" }) + +-- 在启动页(mini.starter)中也绑定 - / -, +-- 因为 starter buffer 可能拦截全局映射 +vim.api.nvim_create_autocmd("User", { + pattern = "MiniStarterOpened", + callback = function(args) + vim.keymap.set("n", "-", function() + require("lazy").load("files", setup_mini_files) + require("mini.files").open() + end, { buffer = args.buf, desc = "打开文件浏览器(工作目录)" }) + + vim.keymap.set("n", "-", function() + require("lazy").load("files", setup_mini_files) + local MiniFiles = require("mini.files") + -- starter 无当前文件,打开工作目录 + MiniFiles.open(vim.fn.getcwd()) + end, { buffer = args.buf, desc = "打开文件浏览器(工作目录)" }) + end, +}) + +-- --------------------------------------------------------------------------- +-- mini.icons — 图标支持(VimEnter 后延迟加载) +-- --------------------------------------------------------------------------- +-- 提供文件类型图标,供 mini.pick、mini.files 等使用。 +-- 在 VimEnter 后 setup,确保 UI 已初始化。 + +vim.api.nvim_create_autocmd("VimEnter", { + once = true, + callback = function() + require("mini.icons").setup() + lazy._loaded["icons"] = true + end, +}) + +-- --------------------------------------------------------------------------- +-- mini.notify — 通知消息 +-- --------------------------------------------------------------------------- +-- 替换默认的 vim.notify,提供美观的浮动通知窗口。 +-- 直接初始化(轻量,不阻塞启动)。 + +require("mini.notify").setup({ + content = { + -- 简化格式:只显示消息内容,不附加时间戳等元信息 + format = function(notif) + return notif.msg + end, + }, +}) +-- 将 vim.notify 重定向到 mini.notify +vim.notify = require("mini.notify").make_notify() + +-- --------------------------------------------------------------------------- +-- mini.cmdline — 增强命令行 +-- --------------------------------------------------------------------------- +-- 提供美观的命令行界面,首次按 : 时加载。 +-- 使用 expr 映射:返回 ":" 让 Vim 继续处理命令行输入。 + vim.keymap.set("n", ":", function() - vim.keymap.del("n", ":") - require("mini.cmdline").setup({ - autocorrect = { enable = false }, - }) - return ":" + -- 首次按下 : 时删除此映射,避免后续重复触发 setup + vim.keymap.del("n", ":") + require("mini.cmdline").setup({ + autocorrect = { enable = false }, -- 禁用自动纠正 + }) + return ":" end, { expr = true, noremap = true }) +-- 诊断位置列表(Quickfix 的本地化版本) vim.keymap.set("n", "ds", function() - vim.diagnostic.setloclist() -end, { desc = "LSP diagnostic loclist" }) + vim.diagnostic.setloclist() +end, { desc = "诊断位置列表" }) --- finders +-- --------------------------------------------------------------------------- +-- mini.pick / mini.extra — 文件查找器(按键触发懒加载) +-- --------------------------------------------------------------------------- +-- 所有 pick 相关映射共用 pick.load_pick() 初始化,确保 mini.pick 和 mini.extra +-- 只在首次使用时 setup 一次。 + +-- ff :文件查找 lazy.on_keys("pick", "ff", "n", pick.load_pick, function() - require("mini.pick").builtin.files() -end, { desc = "Mini File Picker" }) + require("mini.pick").builtin.files() +end, { desc = "文件查找" }) + +-- fw :实时 grep(在项目中搜索文本) lazy.on_keys("pick", "fw", "n", pick.load_pick, function() - require("mini.pick").builtin.grep_live() -end, { desc = "Live grep in project" }) + require("mini.pick").builtin.grep_live() +end, { desc = "项目内实时搜索" }) + +-- sk :键位映射搜索 lazy.on_keys("pick", "sk", "n", pick.load_pick, function() - require("mini.extra").pickers.keymaps() -end, { desc = "Search keymaps" }) + require("mini.extra").pickers.keymaps() +end, { desc = "搜索键位映射" }) + +-- fa :查找所有文件(含隐藏和忽略的文件) lazy.on_keys("pick", "fa", "n", pick.load_pick, function() - require("mini.pick").builtin.cli({ command = { "rg", "--files", "--hidden", "--no-ignore", "--color=never" } }) -end, { desc = "Find all files (including hidden/ignored)" }) + require("mini.pick").builtin.cli({ + command = { "rg", "--files", "--hidden", "--no-ignore", "--color=never" } + }) +end, { desc = "查找所有文件(含隐藏/忽略)" }) + +-- fh :帮助标签搜索 lazy.on_keys("pick", "fh", "n", pick.load_pick, function() - require("mini.pick").builtin.help() -end, { desc = "Search help tags" }) + require("mini.pick").builtin.help() +end, { desc = "搜索帮助标签" }) + +-- fo :最近打开的文件 lazy.on_keys("pick", "fo", "n", pick.load_pick, function() - require("mini.extra").pickers.oldfiles() -end, { desc = "Search oldfiles" }) + require("mini.extra").pickers.oldfiles() +end, { desc = "最近打开的文件" }) + +-- fz :当前缓冲区行内搜索 lazy.on_keys("pick", "fz", "n", pick.load_pick, function() - require("mini.extra").pickers.buf_lines({ scope = "current" }) -end, { desc = "Search in current buffer" }) + require("mini.extra").pickers.buf_lines({ scope = "current" }) +end, { desc = "当前缓冲区行内搜索" }) + +-- --------------------------------------------------------------------------- +-- vim-fugitive — Git 集成(按键触发懒加载) +-- --------------------------------------------------------------------------- +-- tpope 的 Git 包装插件,提供 :Git 等命令。 --- vim-fugitive - 按键触发 local function load_fugitive() - vim.cmd.packadd("vim-fugitive") + vim.cmd.packadd("vim-fugitive") end + +-- gg :在新标签页中打开 Fugitive 全屏 lazy.on_keys("fugitive", "gg", "n", load_fugitive, function() - vim.cmd("tabnew | Git | only") -end, { desc = "Fugitive Full Page New Tab" }) + vim.cmd("tabnew | Git | only") +end, { desc = "Fugitive 全屏新标签" }) + +-- gd :Git diff 垂直分割 lazy.on_keys("fugitive", "gd", "n", load_fugitive, function() - vim.cmd("Gvdiffsplit") -end, { desc = "Git diff split" }) + vim.cmd("Gvdiffsplit") +end, { desc = "Git diff 分割" }) + +-- --------------------------------------------------------------------------- +-- mini.completion — 自动补全(InsertEnter 懒加载) +-- --------------------------------------------------------------------------- +-- 轻量级补全引擎,支持 LSP 补全、buffer 单词、路径补全。 --- mini.completion - InsertEnter lazy.on_event("completion", "InsertEnter", "*", function() - require("mini.completion").setup({ - lsp_completion = { - auto_setup = true, - }, - }) + require("mini.completion").setup({ + lsp_completion = { + auto_setup = true, -- 自动配置 LSP 补全源 + }, + }) end) --- mini.snippets - InsertEnter +-- --------------------------------------------------------------------------- +-- mini.snippets — 代码片段(InsertEnter 懒加载) +-- --------------------------------------------------------------------------- +-- 代码片段引擎,支持 LSP 片段扩展。 + lazy.on_event("snippets", "InsertEnter", "*", function() - local MiniSnippets = require("mini.snippets") - MiniSnippets.setup({ - snippets = { - MiniSnippets.gen_loader.from_lang(), - }, - }) - MiniSnippets.start_lsp_server({ match = false }) + local MiniSnippets = require("mini.snippets") + MiniSnippets.setup({ + snippets = { + -- 从 friendly-snippets 加载对应语言的片段 + MiniSnippets.gen_loader.from_lang(), + }, + }) + -- 启动 LSP 片段服务器(match = false 表示不匹配时不自动触发) + MiniSnippets.start_lsp_server({ match = false }) end) --- mini.diff - BufReadPost +-- --------------------------------------------------------------------------- +-- mini.diff — Git diff 标记(BufReadPost 懒加载) +-- --------------------------------------------------------------------------- +-- 在 signcolumn 中显示当前 buffer 相对于 git HEAD 的变更标记。 + lazy.on_event("diff", "BufReadPost", "*", function() - require("mini.diff").setup({ - source = require("mini.diff").gen_source.git({ index = false }), - view = { - style = "sign", - signs = { add = "│", change = "│", delete = "│" }, - }, - mappings = { - apply = "gs", - textobject = "", - }, - }) + require("mini.diff").setup({ + -- 使用 git 作为 diff 源(index = false 表示对比工作区 vs HEAD,不含暂存区) + source = require("mini.diff").gen_source.git({ index = false }), + view = { + style = "sign", -- 以符号形式显示(在 signcolumn 中) + signs = { add = "│", change = "│", delete = "│" }, -- 统一的竖线符号 + }, + mappings = { + apply = "gs", -- gs 应用当前 hunk + textobject = "", -- 禁用文本对象映射 + }, + }) end) --- mini.surround - BufReadPost +-- --------------------------------------------------------------------------- +-- mini.surround — 环绕文本操作(BufReadPost 懒加载) +-- --------------------------------------------------------------------------- +-- 快速添加、删除、修改包围符号(如括号、引号、HTML 标签等)。 + lazy.on_event("surround", "BufReadPost", "*", function() - require("mini.surround").setup() + require("mini.surround").setup() end) --- 延迟加载 heavy 模块 +-- --------------------------------------------------------------------------- +-- 重型模块延迟加载(VimEnter 事件) +-- --------------------------------------------------------------------------- +-- treesitter 和 lsp 是启动时最耗时的模块, +-- 延迟到 VimEnter 事件触发后加载,让编辑器界面先渲染出来。 + lazy.on_event("treesitter", "VimEnter", "*", function() - require("treesitter").setup() -end) -lazy.on_event("lsp", "VimEnter", "*", function() - require("lsp") + require("treesitter").setup() end) --- grug-far - sr 触发 +lazy.on_event("lsp", "VimEnter", "*", function() + require("lsp") +end) + +-- --------------------------------------------------------------------------- +-- grug-far.nvim — 搜索与替换(按键触发懒加载) +-- --------------------------------------------------------------------------- +-- 提供类似 VS Code 的查找替换界面,支持正则和文件过滤。 + local function load_grug_far() - vim.cmd.packadd("grug-far.nvim") - require("grug-far").setup({ headerMaxWidth = 80 }) + vim.cmd.packadd("grug-far.nvim") + require("grug-far").setup({ headerMaxWidth = 80 }) end local function open_grug_far() - local grug = require("grug-far") - local ext = vim.bo.buftype == "" and vim.fn.expand("%:e") - grug.open({ - transient = true, - prefills = { - filesFilter = ext and ext ~= "" and "*." .. ext or nil, - }, - }) + local grug = require("grug-far") + -- 根据当前文件扩展名预填充文件过滤器 + local ext = vim.bo.buftype == "" and vim.fn.expand("%:e") + grug.open({ + transient = true, -- 关闭后自动销毁 buffer + prefills = { + filesFilter = ext and ext ~= "" and "*." .. ext or nil, + }, + }) end -lazy.on_keys("grugfar", "sr", "n", load_grug_far, open_grug_far, { desc = "Search and Replace" }) -lazy.on_keys("grugfar", "sr", "x", load_grug_far, open_grug_far, { desc = "Search and Replace" }) +-- Normal 模式和 Visual 模式均绑定 sr +lazy.on_keys("grugfar", "sr", "n", load_grug_far, open_grug_far, { desc = "搜索并替换" }) +lazy.on_keys("grugfar", "sr", "x", load_grug_far, open_grug_far, { desc = "搜索并替换" }) diff --git a/lua/pick.lua b/lua/pick.lua index a816c19..9b8a5f9 100644 --- a/lua/pick.lua +++ b/lua/pick.lua @@ -1,83 +1,125 @@ +-- ============================================================================= +-- mini.pick 封装 (lua/pick.lua) +-- ============================================================================= +-- 本模块封装 mini.pick 和 mini.extra 的初始化逻辑, +-- 提供统一的懒加载入口和自定义 picker。 +-- +-- 主要功能: +-- 1. load_pick() — 懒加载 mini.pick 和 mini.extra +-- 2. Buffer picker()— 自定义 buffer 列表,支持 删除 +-- 3. Filetype picker(ft)— 快速切换文件类型 +-- ============================================================================= + local lazy = require("lazy") local M = {} +-- --------------------------------------------------------------------------- +-- 懒加载入口 +-- --------------------------------------------------------------------------- +-- 通过 lazy.load 确保 mini.pick 和 mini.extra 只初始化一次。 +-- 此函数被 pack.lua 中所有 pick 相关的 on_keys 映射共用。 + M.load_pick = function() - lazy.load("pick", function() - require("mini.pick").setup() - end) - lazy.load("extra", function() - require("mini.extra").setup() - end) + lazy.load("pick", function() + require("mini.pick").setup() + end) + lazy.load("extra", function() + require("mini.extra").setup() + end) end --- Buffer picker () — supports to delete buffer +-- --------------------------------------------------------------------------- +-- Buffer Picker() +-- --------------------------------------------------------------------------- +-- 显示当前打开的 buffer 列表,按最近使用时间排序。 +-- 特殊功能: +-- - :删除当前选中的 buffer(带图标过滤) +-- - 支持 mini.icons 图标显示 + vim.keymap.set("n", "", function() - M.load_pick() - local MiniPick = require("mini.pick") + M.load_pick() + 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 的回调函数 + 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 - pcall(vim.api.nvim_buf_delete, bufnr, { force = true }) + -- 安全删除 buffer(force = true 跳过未保存确认) + pcall(vim.api.nvim_buf_delete, bufnr, { force = true }) - 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 + -- 如果 picker 仍活跃,刷新列表以移除已删除的 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 - 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) + -- 获取所有普通 buffer(排除特殊 buffer 如终端、quickfix) + local bufs = vim.tbl_filter(function(b) + return vim.bo[b.bufnr].buftype == "" and b.listed == 1 + end, vim.fn.getbufinfo()) - 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 + -- 按最后使用时间降序排列(最近使用的在前) + table.sort(bufs, function(a, b) + return a.lastused > b.lastused + 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" }) + -- 构建 picker items + 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 + + -- 启动 picker + 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 = "Buffer 列表" }) + +-- --------------------------------------------------------------------------- +-- Filetype Picker(ft) +-- --------------------------------------------------------------------------- +-- 显示所有可用的文件类型列表,选择后设置当前 buffer 的 filetype。 +-- 常用于打开无扩展名文件或纠正错误的文件类型检测。 --- Filetype picker (ft) vim.keymap.set("n", "ft", function() - M.load_pick() - 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" }) + M.load_pick() + local MiniPick = require("mini.pick") + -- 获取所有可用的文件类型名称 + local filetypes = vim.fn.getcompletion("", "filetype") + MiniPick.start({ + source = { + name = "Filetypes", + items = filetypes, + choose = function(item) + -- 选择后将当前 buffer 的 filetype 设为选中值 + vim.bo.filetype = item + end, + }, + }) +end, { desc = "切换文件类型" }) return M diff --git a/lua/treesitter.lua b/lua/treesitter.lua index 88c4734..93237ca 100644 --- a/lua/treesitter.lua +++ b/lua/treesitter.lua @@ -1,56 +1,90 @@ +-- ============================================================================= +-- Treesitter 配置 (lua/treesitter.lua) +-- ============================================================================= +-- 本模块配置 Neovim 的 Treesitter 集成,提供语法树驱动的语法高亮、 +-- 代码折叠和文本对象支持。 +-- +-- 加载方式: +-- 由 pack.lua 通过 lazy.on_event("treesitter", "VimEnter", "*", ...) 延迟加载。 +-- +-- 设计要点: +-- 1. Parser 安装延迟 100ms 执行,避免阻塞 startup +-- 2. 高亮按 buffer 动态附加(FileType autocmd),只安装需要的 parser +-- ============================================================================= + local M = {} +-- --------------------------------------------------------------------------- +-- 预安装的 parser 列表 +-- --------------------------------------------------------------------------- +-- 这些 parser 会在首次启动后延迟安装。 +-- 如果某个 parser 未在此列表中,打开对应文件类型时仍可通过 nvim-treesitter +-- 的 :TSInstall 手动安装。 + local ensure_installed = { "lua", "vim", "vimdoc", - -- Web + -- Web 前端 "javascript", "typescript", - "tsx", - "jsdoc", + "tsx", -- TypeScript JSX + "jsdoc", -- JSDoc 注释 "json", "html", "css", "yaml", - -- Backend + -- 后端 "c", "rust", "toml", "go", - "gomod", - "gosum", - "gowork", - -- Infra + "gomod", -- Go Modules + "gosum", -- Go Sum + "gowork", -- Go Workspaces + -- 基础设施 "dockerfile", "make", } +-- --------------------------------------------------------------------------- +-- 模块初始化 +-- --------------------------------------------------------------------------- M.setup = function() + -- 加载 nvim-treesitter(通过 packadd 激活 opt 插件) vim.cmd.packadd("nvim-treesitter") local treesitter = require("nvim-treesitter") - -- 延迟安装 parser,避免阻塞 + -- 延迟安装 parser:在 startup 完成 100ms 后后台安装, + -- 避免在安装过程中阻塞编辑器。 vim.defer_fn(function() treesitter.install(ensure_installed) end, 100) + -- 按文件类型动态启用 treesitter 高亮。 + -- 当文件的 filetype 被设置时(BufRead、:setfiletype 等), + -- 尝试查找并加载对应的 parser,成功后启用高亮。 vim.api.nvim_create_autocmd("FileType", { - pattern = "*", + pattern = "*", -- 匹配所有文件类型 callback = function(args) local buf = args.buf local ft = vim.bo[buf].filetype + -- 将文件类型映射到 treesitter 语言名 + -- 例如 "javascriptreact" → "javascript" local lang = vim.treesitter.language.get_lang(ft) if not lang then - return + return -- 无对应语言,跳过 end + -- 尝试注册语言(如果 parser 已安装) + -- pcall 用于安全调用:parser 未安装时不会报错 local ok_add = pcall(vim.treesitter.language.add, lang) if not ok_add then - return + return -- parser 未安装,跳过 end + -- 启用 treesitter 高亮 pcall(vim.treesitter.start, buf, lang) end, }) diff --git a/lua/usercmds.lua b/lua/usercmds.lua index 40d1eee..017f8ed 100644 --- a/lua/usercmds.lua +++ b/lua/usercmds.lua @@ -1,21 +1,72 @@ +-- ============================================================================= +-- 用户自定义命令 (lua/usercmds.lua) +-- ============================================================================= +-- 定义 Neovim 命令行可用的自定义命令,封装 vim.pack API 的日常操作。 +-- +-- Neovim 0.12+ 内置 vim.pack 插件管理,提供: +-- vim.pack.add(urls, opts) - 添加/安装插件 +-- vim.pack.del(names) - 删除插件 +-- vim.pack.update(names?) - 更新插件 +-- +-- 以下命令是对这些 API 的友好封装,支持命令行参数解析。 +-- ============================================================================= + +-- --------------------------------------------------------------------------- +-- :PackAdd — 添加插件 +-- --------------------------------------------------------------------------- +-- 用法::PackAdd user/repo1 user/repo2 ... +-- 示例::PackAdd stevearc/conform.nvim tpope/vim-fugitive +-- +-- 原理: +-- 将命令行参数(opts.fargs,已按空格分割的字符串数组) +-- 直接传递给 vim.pack.add()。 +-- vim.pack 会从 GitHub 下载插件到 stdpath("data")/site/pack/core/opt/。 +-- +-- nargs = "+" 表示至少需要 1 个参数。 + vim.api.nvim_create_user_command("PackAdd", function(opts) vim.pack.add(opts.fargs) -end, { nargs = "+", desc = "Add plugins (:PackAdd user/repo1 user/repo2)" }) +end, { nargs = "+", desc = "添加插件 (:PackAdd user/repo1 user/repo2)" }) + +-- --------------------------------------------------------------------------- +-- :PackDel — 删除插件 +-- --------------------------------------------------------------------------- +-- 用法::PackDel plugin1 plugin2 ... +-- 示例::PackDel conform.nvim vim-fugitive +-- +-- 注意: +-- 参数是插件目录名(即 repo 名),不是完整 URL。 +-- Neovim 0.13 Nightly 已内置此命令,这里为 0.12 提供兼容。 +-- +-- nargs = "+" 表示至少需要 1 个参数。 --- 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)" }) +end, { nargs = "+", desc = "删除插件 (:PackDel plugin1 plugin2)" }) + +-- --------------------------------------------------------------------------- +-- :PackUpdate — 更新插件 +-- --------------------------------------------------------------------------- +-- 用法: +-- :PackUpdate → 更新所有插件 +-- :PackUpdate name1 name2 ... → 更新指定插件 +-- +-- 原理: +-- 检查 opts.args 是否包含非空白字符(%S 匹配任意非空白)。 +-- 如果有参数,按空白分割为数组后逐个更新; +-- 如果无参数,调用无参版本更新所有插件。 +-- +-- nargs = "*" 表示接受 0 个或多个参数。 vim.api.nvim_create_user_command("PackUpdate", function(opts) - -- checks if any argument is passed + -- 检查是否有传入任何参数(包含非空白字符) if opts.args:match("%S") then - -- update specific plugins + -- 按空白字符分割参数,trimempty 去除空字符串 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" }) +end, { nargs = "*", desc = "更新所有插件或指定插件" })