面试题手册

梳理高频技术问题,帮助你按主题复习和查漏补缺。

服务端阅读 05月27日 14:58

Vim 的寄存器到底有几种,各自用在什么场景?

为什么你的 Vim 粘贴总是不对你一定遇到过这种情况:复制了一行代码,删掉另一行,再粘贴时发现粘贴的是刚删掉的内容,而不是你复制的那行。这不是 bug,这是 Vim 寄存器机制在起作用——大多数操作都默认写入同一个无名寄存器,后进来的把前面的覆盖了。Vim 并不是只有一个剪贴板,它有十几种寄存器,每种都有明确的用途。搞清楚它们,复制粘贴不再踩坑,还能用寄存器做宏录制、表达式计算、跨程序复制等高级操作。无名寄存器("")——默认的垃圾桶每次执行 yank、delete、change 等操作,内容都会自动写入无名寄存器 ""。普通模式下按 p 粘贴,用的就是它。问题在于,dd 删除一行和 yy 复制一行都会覆盖 ""。所以你复制之后做了一次删除,粘贴出来的就是删除的内容。这不代表原来的内容丢了——它还在数字寄存器 "0 里。所以下次遇到"粘贴不对",先试 "0p,大概率就是你想要的内容。命名寄存器("a–"z)——手动管理的 26 个抽屉命名寄存器是最常用的一类,用法简单:操作前加 "寄存器名。"ayy " 将当前行复制到寄存器 a"ap " 粘贴寄存器 a 的内容"bdw " 删除一个单词并存入寄存器 b这样你可以在 a 里存一段代码,b 里存另一段,随时按 "ap 和 "bp 取出来,互不干扰。大写字母是追加,不是覆盖如果寄存器 a 里已经有内容,"ayy 会覆盖它。但用大写 "Ayy 则是追加:"ayy " 覆盖写入寄存器 a"Ayy " 追加到寄存器 a 末尾这在收集分散内容时很有用——比如把文件中多个位置的函数签名逐行追加到同一个寄存器,最后一次性粘贴。数字寄存器("0–"9)——自动记录的历史栈数字寄存器不需要手动指定,Vim 自动维护:"0:最近一次 yank 的内容,不会被 delete 覆盖"1:最近一次 delete 或 change 的内容"2:倒数第二次 delete 的内容…以此类推到 "9注意 "0 是 yank 专用,只有 y 操作才会更新它。dd 和 x 只会更新 "1 到 "9,不会碰 "0。实际场景:你 yy 复制了一行,然后 dd 删了几行,想粘贴最初复制的那行——"0p 就是答案。只读寄存器——Vim 自动填入的元信息四个只读寄存器,你只能读取,不能手动写入:| 寄存器 | 内容 | 典型用法 ||--------|------|----------|| "% | 当前文件名 | 插入文件名:插入模式下 Ctrl+r % || ". | 最后插入的文本 | 重复上次输入:插入模式下 Ctrl+r . || ": | 最后执行的 Ex 命令 | 再次执行上条命令:@: || "/ | 最后的搜索模式 | 替换时复用::%s//替换内容/g |其中 "/ 在替换命令里特别实用——:%s//new/g 等价于 :%s/上次搜索的词/new/g,省去重新输入搜索内容。黑洞寄存器("_)——删除但不留痕迹"_dd 删除一行,但不会存入任何寄存器,无名寄存器和数字寄存器都不会被更新。什么时候用?当你删掉的内容不需要粘贴,又不想污染寄存器历史的时候。比如清理大量注释行,用 "_dd 逐行删除,你的 "0 仍然保存着之前 yank 的内容,不受影响。表达式寄存器("=)——在插入模式做计算在插入模式下按 Ctrl+r =,Vim 会在命令行提示你输入一个表达式,计算结果直接插入光标处。" 插入模式下:Ctrl+r =3600*24↵ " 插入 86400Ctrl=r =strftime('%Y-%m-%d')↵ " 插入当前日期也可以在命令里引用变量或函数返回值,适合需要动态插入内容的场景。系统剪贴板("+ 和 "*)——和外部程序互通Vim 默认不与系统剪贴板交互,需要通过 "+ 或 "* 寄存器:"+yy:复制当前行到系统剪贴板"+p:从系统剪贴板粘贴"+ 和 "* 在 Windows 和 macOS 上行为一致,都指向系统剪贴板。在 Linux 上有区别:"+ 是 CLIPBOARD(Ctrl+C/V),"* 是 PRIMARY(鼠标选中即复制,中键粘贴)。如果你希望每次 yank/paste 自动使用系统剪贴板,可以设置:set clipboard=unnamedplus这样普通 yy 和 p 就直接操作系统剪贴板了。:reg——查看所有寄存器内容忘了某个寄存器里存了什么?用 :reg 或 :registers 查看全部,也可以指定只看某几个::reg " 查看所有非空寄存器:reg a b 0 " 只看寄存器 a、b、0:reg / : " 查看搜索模式和上次 Ex 命令输出格式是寄存器名 + 内容,内容中的换行用 ^J 表示。调试宏或确认寄存器状态时经常用到。宏与寄存器——本质上是同一套机制Vim 的宏录制就是把按键序列存进命名寄存器。qa 开始录制到寄存器 a,再按 q 停止,@a 回放。这意味着:录制的宏可以用 :reg a 查看,内容就是一串按键字符你可以把宏内容粘贴出来编辑,改好再 yank 回去——修改宏不需要重新录制用大写追加可以往宏里追加指令:qA 追加录制到 a 宏末尾编辑宏的流程::reg a " 先看看宏 a 里的内容"ap " 把宏内容粘贴到缓冲区" 编辑这一行按键序列"ayy " 重新 yank 回寄存器 a这种可编辑性是 Vim 宏区别于简单"录制回放"的关键——出错了不用重来,改一行就行。把寄存器用起来寄存器不是 Vim 里"知道就好"的冷知识,它直接影响日常编辑效率。几个建议:复制重要内容时指定命名寄存器("ayy),避免被后续删除覆盖需要干净删除时用黑洞寄存器("_dd),保持寄存器历史干净复制粘贴跨程序时显式用 "+y / "+p,不要依赖自动剪贴板设置录制宏后用 :reg 检查内容,复杂宏直接编辑比重新录制更高效Vim 的寄存器体系看起来种类多,但核心逻辑就是一条:每次操作前加 "寄存器名,就是指定目标寄存器;不加,就是无名寄存器。记住这个规律,其他的都是在此基础上的分类和特例。
服务端阅读 05月27日 14:52

Vim 的标签导航怎么用?从 ctags 到 LSP 的完整跳转方案

为什么需要标签导航阅读源码时,你会在函数调用处和定义处之间反复切换。如果没有标签系统,只能靠 grep 或 /:function_name 搜索,效率很低。Vim 的标签导航机制让你在光标处一键跳转到定义,再一键返回,是代码阅读的核心工作流。生成 tags 文件:ctags标签导航的前提是有 tags 文件。ctags 扫描源码,把函数、类、变量的定义位置记录到一个索引文件中。安装 Universal Ctags(Exuberant Ctags 的活跃 fork):# macOSbrew install universal-ctags# Ubuntu/Debiansudo apt install universal-ctags在项目根目录生成 tags 文件:ctags -R .-R 表示递归扫描子目录,生成的 tags 文件存放在当前目录。如果你的项目有 node_modules 或 build 目录,建议排除:ctags -R --exclude=node_modules --exclude=build .C/C++ 项目需要更详细的标签信息,可以加参数:ctags -R --c++-kinds=+p --fields=+iaS --extra=+q .--c++-kinds=+p:记录函数声明和外部声明--fields=+iaS:记录继承关系、访问权限、函数签名--extra=+q:为同名函数生成额外的区分行基本跳转:Ctrl-] 和 Ctrl-T这是最常用的操作,务必记住:Ctrl-]:跳转到光标下标识符的定义Ctrl-T:沿标签栈返回上一个位置使用流程:把光标移到函数名上,按 Ctrl-] 跳过去,看完按 Ctrl-T 回来。可以连续跳转多次,每次 Ctrl-T 回退一层。在终端中,Ctrl-] 可能被 shell 的 telnet 快捷键拦截。解决办法是用 :tag 命令,或者重新映射终端快捷键。用 :tag 和 :tselect 精确跳转当光标不在目标标识符上时,可以直接用命令跳转::tag main跳转到 main 的定义。支持 Tab 补全,输入 :tag m<Tab> 会列出所有 m 开头的标签。如果一个标签有多个定义(比如不同文件中同名函数),:tag 只跳到第一个。这时用 :tselect 列出所有匹配::tselect parseVim 会显示一个选择列表,输入编号即可跳转。:tjump 是更智能的版本:只有一个匹配时直接跳转,多个匹配时弹出选择列表。相当于 :tselect 和 :tag 的合体。在匹配项之间浏览::tnext — 下一个匹配:tprev — 上一个匹配:tfirst — 第一个匹配:tlast — 最后一个匹配g] :预览式选择跳转g ] 把光标下标识符的所有匹配列出来让你选择,和 :tjump 效果类似,但不需要输入命令。日常使用中,g ] 比 Ctrl-] 更稳妥——遇到多个定义时不会跳错位置。另外几个预览相关的命令:Ctrl-W } — 在预览窗口中打开定义,不离开当前位置:ptag func_name — 在预览窗口打开指定标签:pclose — 关闭预览窗口标签栈::tags 查看跳转历史每次 Ctrl-] 跳转都会压入标签栈。查看栈内容::tags输出类似: # TO tag FROM line in file/text 1 1 parse 12 main.c> 2 2 process 45 parser.c 3 1 validate 78 parser.c> 标记当前所在位置。Ctrl-T 每次弹出一层,也可以用数字前缀一次回退多层:3 Ctrl-T 回退 3 层。注意标签栈和跳转列表(jumplist)不同。Ctrl-O / Ctrl-I 操作的是跳转列表,范围更广;Ctrl-T 操作的是标签栈,只追踪标签跳转。两者配合使用效果最好。配置 tags 选项Vim 通过 tags 选项定位 tags 文件。默认值是 ./tags,tags,即在当前文件目录和工作目录查找。常见配置:" 向上级目录查找 tags 文件,直到找到为止set tags=./tags;,tags;" 或者指定固定路径set tags+=/path/to/project/tags./tags; 中的分号表示向上递归查找——Vim 会从当前文件所在目录开始,逐级向上找 tags 文件,直到根目录。这解决了在子目录中打开文件时找不到项目根目录 tags 文件的问题。如果项目有多个 tags 文件,用 += 追加:set tags+=/path/to/external-lib/tags多项目的 tags 管理当你同时在多个项目间切换时,每个项目应该有自己的 tags 文件。几个实践建议:把 tags 文件加到 .gitignore。tags 文件是本地生成的,不应该提交到仓库。用 set autochdir。Vim 自动把工作目录切换到当前文件所在目录,配合 ./tags; 的递归查找,基本可以覆盖大部分场景:set autochdirset tags=./tags;,tags;大项目按模块拆分 tags。在子目录分别生成 tags 文件,Vim 会自动合并所有匹配的标签。使用 $PROJECT_HOME 环境变量。在 vimrc 中动态设置 tags 路径:if $PROJECT_HOME != '' set tags+=$PROJECT_HOME/tagsendifcscope:标签之外的代码交叉引用ctags 只能跳转到定义,无法查找"谁调用了这个函数"。cscope 补充了这个能力。生成 cscope 数据库:# 在项目根目录find . -name "*.c" -o -name "*.h" > cscope.filescscope -b-b 表示只构建数据库,不进入交互界面。生成的 cscope.out 文件就是数据库。在 Vim 中连接数据库::cs add cscope.out验证连接::cs showcscope 的查询类型:| 命令 | 缩写 | 含义 ||------|------|------|| :cs find s symbol | 0 | 查找符号的所有引用 || :cs find g symbol | 1 | 查找全局定义 || :cs find d func | 2 | 查找该函数调用的函数 || :cs find c func | 3 | 查找调用该函数的函数 || :cs find t text | 4 | 查找文本字符串 || :cs find e pattern | 6 | egrep 模式搜索 || :cs find f file | 7 | 查找文件 || :cs find i include | 8 | 查找 include 该文件的文件 |最常用的是 :cs find c(谁调用了这个函数)和 :cs find d(这个函数调用了谁),这是 ctags 做不到的。:cstag 命令同时搜索 cscope 数据库和 tags 文件,建议在 vimrc 中设置:set csto=1set cst这样 Ctrl-] 会优先查 cscope,再查 tags。gutentags:自动生成 tags 文件手动跑 ctags -R 很容易忘。vim-gutentags 插件在后台自动管理 tags 文件的生成和更新。安装(以 vim-plug 为例):Plug 'ludovicchabant/vim-gutentags'基本配置:" 指定 tags 文件存放目录(避免污染项目根目录)let g:gutentags_cache_dir = expand('~/.cache/vim/ctags/')" 确保缓存目录存在if !isdirectory(g:gutentags_cache_dir) silent! mkdir -p g:gutentags_cache_direndif" 项目根目录标记let g:gutentags_project_root = ['.git', '.hg', '.root']gutentags 的行为:打开项目中的文件时,自动在后台生成 tags保存文件时,增量更新 tags(不是全量重建)通过 .git 等标记识别项目根目录不依赖 Python 或 Ruby,纯 Vim 脚本 + ctags状态栏显示生成进度:set statusline+=%{gutentags#statusline()}生成 tags 时状态栏会显示 TAGS,完成后自动消失。LSP 方案:coc.nvim 和 nvim-lspconfigLSP(Language Server Protocol)提供了比 ctags 更精确的代码导航。LSP 的跳转基于语义分析,能区分同名函数的重载,能跳转到依赖库中的定义,还能查找所有引用。coc.nvim(Vim 8+ / Neovim):Plug 'neoclide/coc.nvim', {'branch': 'release'}安装语言服务器后,用 gd 跳转到定义,gr 查找引用,K 查看文档。coc.nvim 还会把 LSP 结果注册为 tags,所以 Ctrl-] 也能用。nvim-lspconfig(Neovim 0.5+):local lspconfig = require('lspconfig')lspconfig.pyright.setup{} -- Pythonlspconfig.ts_ls.setup{} -- TypeScriptlspconfig.clangd.setup{} -- C/C++快捷键映射:vim.keymap.set('n', 'gd', vim.lsp.buf.definition)vim.keymap.set('n', 'gr', vim.lsp.buf.references)vim.keymap.set('n', 'K', vim.lsp.buf.hover)LSP 和 ctags 不是互斥的。实际工作中很多人同时使用:LSP 覆盖有完善语言服务器的语言,ctags 作为兜底方案覆盖配置文件、Makefile、shell 脚本等 LSP 不方便覆盖的文件类型。如果你不想为每个语言配 LSP 服务器,可以试试 ctags-lsp——一个基于 ctags 实现的轻量 LSP 服务器,开箱即用,精度介于裸 ctags 和完整 LSP 之间。一份实用的 vimrc 配置参考" tags 文件查找策略set tags=./tags;,tags;" cscope 优先于 tagsset csto=1set cst" gutentags 配置let g:gutentags_cache_dir = expand('~/.cache/vim/ctags/')let g:gutentags_project_root = ['.git', '.root']" 快捷键映射nnoremap <C-]> g] " 多匹配时选择,而非直接跳第一个nnoremap <C-T> <C-T> " 回退保持默认nnoremap <leader>cs :cs find s <C-R>=expand('<cword>')<CR><CR>nnoremap <leader>cg :cs find g <C-R>=expand('<cword>')<CR><CR>nnoremap <leader>cc :cs find c <C-R>=expand('<cword>')<CR><CR>Vim 的标签导航从 ctags 起步,cscope 补足交叉引用,gutentags 解决自动化,LSP 带来语义精度。根据项目语言和规模选择合适的组合,比追单一方案更实际。
服务端阅读 05月27日 14:52

VimScript 脚本编程怎么写?从变量作用域到插件结构全讲清楚

为什么要学 VimScriptVim 的真正威力不在于快捷键多,而在于你可以用脚本把编辑器改造成自己想要的样子。自动格式化、批量重命名、项目专属配置——这些全靠 VimScript 驱动。即使你现在用 Neovim + Lua,读懂已有插件的 VimScript 源码依然是刚需。变量与作用域VimScript 的变量用 let 声明,用 unlet 删除。关键在于作用域前缀——每个前缀决定了变量的可见范围和生命周期:let g:global_var = 1 " 全局变量,任何地方都能访问let s:script_var = 2 " 脚本局部变量,只在当前 .vim 文件内可见let l:local_var = 3 " 函数局部变量,只在当前函数内可见(函数内默认)let b:buffer_var = 4 " 缓冲区局部变量,绑定到当前文件缓冲区let w:window_var = 5 " 窗口局部变量,绑定到当前窗口let t:tab_var = 6 " 标签页局部变量,绑定到当前标签页let a:arg_var = 7 " 函数参数,只在函数体内可用几个容易踩的坑:函数体内不加前缀的变量默认是 l:,不是全局的。脚本级的 s: 变量在文件多次 source 时不会重置,生命周期跟 Vim 进程一致。v: 是 Vim 内置变量(如 v:count、v:errmsg),只读居多,不要覆盖。还可以用 &option 访问选项值(如 &tabstop),@r 访问寄存器,$ENV 访问环境变量。echo &tabstop " 读取选项let &tabstop = 4 " 设置选项echo @a " 读取寄存器 a 的内容echo $HOME " 读取环境变量字符串操作VimScript 的字符串有两种引号,行为不同:let s1 = "hello world" " 双引号:支持 \ 等转义let s2 = 'hello world' " 单引号:原样输出, 就是两个字符常用字符串函数:echo strlen("hello") " => 5echo strpart("hello", 1, 3) " => ell(从索引1取3个字符)echo substitute("hello", "l", "L", "g") " => heLLoecho tolower("Hello") " => helloecho toupper("hello") " => HELLOecho stridx("hello", "ll") " => 2(查找子串位置)echo split("a,b,c", ",") " => ['a', 'b', 'c']echo join(['a', 'b'], "-") " => a-b拼接字符串推荐用 . 运算符:let msg = "file: " . expand("%") . " line: " . line(".")列表与字典列表(List)就是数组,字典(Dict)就是哈希表:" 列表let fruits = ["apple", "banana", "cherry"]echo fruits[0] " => appleecho fruits[-1] " => cherry(负索引从末尾取)call add(fruits, "date") " 追加元素echo len(fruits) " => 4" 列表推导let squares = map(range(5), 'v:val * v:val')echo squares " => [0, 1, 4, 9, 16]" 字典let user = {"name": "vim", "version": 9}echo user.name " => vimecho user["version"] " => 9let user.lang = "VimScript" " 添加键call remove(user, "lang") " 删除键echo keys(user) " => ['name', 'version']echo values(user) " => ['vim', 9]控制流if / else / endifif &filetype ==# "python" echo "Python file"elseif &filetype ==# "javascript" echo "JS file"else echo "Other file"endif注意 ==# 是大小写敏感比较,==? 是忽略大小写。裸写 == 受用户 ignorecase 设置影响,不推荐。for 循环for item in ["a", "b", "c"] echo itemendforfor i in range(1, 10) echo iendforwhile 循环let i = 0while i < 5 echo i let i += 1endwhileVimScript 没有 break/continue 的等价物(Vim9script 有了),传统做法是用条件变量控制循环。函数定义function! s:Greet(name) echo "Hello, " . a:name return a:nameendfunction关键规则:函数名如果不加 s: 前缀,必须以大写字母开头。function! 加 ! 表示如果函数已存在则覆盖,插件开发必加。函数参数用 a: 前缀访问,如 a:name、a:1(可变参数)。函数默认返回 0,除非显式 return。abort 关键字让函数在出错时立即中止,而不是继续执行。function! s:Min(num1, num2) abort return a:num1 < a:num2 ? a:num1 : a:num2endfunction可变参数用 ...:function! s:Varargs(name, ...) echo a:name echo a:0 " 可变参数个数 echo a:1 " 第一个可变参数 echo a:000 " 可变参数列表endfunctionautocmd 编程autocmd 是 Vim 事件驱动的核心——在特定事件发生时自动执行命令:augroup AutoFormat autocmd! autocmd BufWritePre *.py call s:AutoFormatPython() autocmd BufWritePre *.js call s:AutoFormatJS()augroup END要点:必须用 augroup 包裹,否则每次 source 文件都会追加重复的 autocmd。autocmd! 先清空同组旧命令。常用事件:BufWritePre(保存前)、BufRead(打开文件)、FileType(设置文件类型)、InsertEnter(进入插入模式)。autocmd 体尽量短,复杂逻辑抽成函数调用。function! s:AutoFormatPython() " 复杂格式化逻辑 silent! %!autopep8 -endfunction自定义命令:command 与 、用 :command 定义用户命令,比函数调用方便得多:command! -nargs=1 -complete=file MyEdit :edit <args><f-args> 将命令参数拆分为函数参数列表,是最常用的参数传递方式:command! -nargs=* Grep call s:Grep(<f-args>)function! s:Grep(...) abort let pattern = join(a:000, ' ') silent! grep! pattern cwindowendfunction<range> 让命令支持行范围:command! -range Align call s:Align(<line1>, <line2>)function! s:Align(line1, line2) abort execute a:line1 . ',' . a:line2 . '!column -t'endfunction常用 command 参数:| 参数 | 含义 ||------|------|| -nargs=0 | 无参数(默认) || -nargs=1 | 恰好一个参数 || -nargs=* | 零或多个参数 || -nargs=? | 零或一个参数 || -nargs=+ | 至少一个参数 || -range | 允许行范围 || -bang | 允许 ! 修饰 || -complete=file | 文件名补全 || -bar | 允许 | 管道连接 |插件结构:plugin 与 autoload一个规范的 Vim 插件目录结构:my-plugin/├── plugin/│ └── my-plugin.vim " 启动时加载,定义命令和映射├── autoload/│ └── my-plugin.vim " 按需加载,放函数实现├── doc/│ └── my-plugin.txt " 帮助文档└── after/ └── ftplugin/ └── python.vim " 文件类型专用配置plugin/ 在 Vim 启动时执行,只放命令定义和映射,不放重逻辑。autoload/ 是懒加载机制。文件 autoload/my-plugin.vim 里的函数必须命名为 my-plugin#FuncName,只有第一次调用时才会加载:" plugin/my-plugin.vim(启动时执行)command! MyCommand call my-plugin#Run()" autoload/my-plugin.vim(首次调用 MyCommand 时才加载)function! my-plugin#Run() echo "Hello from autoload"endfunction这种分离让插件启动时零开销,用到才加载。VimScript vs Lua(Neovim)如果你用 Neovim,可能已经在纠结该学哪个。核心区别:性能:Lua(LuaJIT)在循环和密集计算上比 VimScript 快约 56 倍。Neovim 用 Lua 加载 30 个插件只需 94ms,VimScript 配置动辄数百毫秒。语言设计:VimScript 有 30 年历史包袱——隐式类型转换令人困惑、作用域规则不一致、缺少标准数据结构。Lua 是正经的编程语言,有模块、闭包、协程和成熟的生态。生态趋势:2022 年之后,主流 Neovim 插件(telescope、lazy.nvim、nvim-cmp、nvim-treesitter)全部用 Lua 编写,不再接受 VimScript 贡献。兼容性:Neovim 仍然支持 VimScript,init.vim 和 init.lua 可以共存。用 vim.cmd() 在 Lua 中执行 VimScript,用 luaeval() 在 VimScript 中调用 Lua。选择建议:用 Vim → 只能写 VimScript(或 Vim9script)。用 Neovim + 新插件 → 学 Lua,用 init.lua。需要维护老插件 → 必须读懂 VimScript。从哪里开始实践打开 Vim,输入 :help usr_41 阅读 Vim 官方脚本教程。把你的 .vimrc 里重复的配置抽成函数,给常用操作绑定 command!,再用 augroup 挂上自动命令——这就是你第一个"插件"了。不需要一步到位写 autoload 结构,先让东西跑起来,再重构不迟。
服务端阅读 05月27日 14:52

Vim 的可视模式怎么用?字符、行、块选择与列编辑实战

为什么你总在 Vim 里手忙脚乱地选中文字很多人用 Vim 编辑文本时,还在靠 v 一个字符一个字符地挪,遇到多行操作就切回鼠标。问题不在你,在于你没把可视模式的三个子模式用熟。Vim 的可视模式本质上是一种"先选中,再操作"的工作流——你告诉 Vim "我要处理这段文字",然后下一条命令只作用于选区。理解了这一点,后面的操作都顺理成章。三种可视模式,三种选中粒度Vim 提供了三种可视模式,对应三种选择粒度:字符可视模式(v):逐字符选择,适合精确选中一行内的一小段文字。行可视模式(V):逐行选择,整行为单位,适合批量操作连续多行。块可视模式(Ctrl-v):矩形块选择,适合列编辑——这是可视模式里最强大也最容易被忽略的子模式。按 v、V、Ctrl-v 进入对应模式后,再用移动命令(hjkl、w、}、gg、G 等)扩展选区。选区确定后,按任意操作键(d、y、c、> 等)执行。三种模式之间可以互相切换:在字符可视模式下按 V 切到行模式,按 Ctrl-v 切到块模式,无需先按 Esc 退出。选区端点与 o 键进入可视模式后,选区有两个端点:起点和光标所在位置。按 o 可以让光标跳到另一个端点,这样你就能往反方向调整选区。这在选中了一大片区域后发现"起点选错了"时非常管用,不用退出重来。gv:重新选中上一次的选区执行完操作后,选区就消失了。如果你想对同一个区域再做一次操作,按 gv 可以重新选中上一次的可视选区。这在连续对同一块文本执行多条命令时很实用,比如先 V 选中几行用 > 缩进,再 gv 重新选中用 :s/foo/bar/g 做替换。块操作:列编辑的核心块可视模式是 Vim 区别于其他编辑器的杀手功能。进入块选择后,你可以对矩形区域做以下操作:| 操作 | 按键 | 说明 ||------|------|------|| 批量插入(左侧) | I | 在块左侧输入文本,按 Esc 后所有行同时生效 || 批量追加(右侧) | A | 在块右侧输入文本,按 Esc 后所有行同时生效 || 批量替换 | c | 删除选中内容并进入插入模式,输入后按 Esc 全部行生效 || 批量删除 | d 或 x | 直接删除选中块 || 单字符替换 | r | 将选中区域内每个字符替换为你输入的那个字符 |实战:批量给多行加注释假设你有以下代码,想给三行加 // 注释:int a = 1;int b = 2;int c = 3;操作步骤:把光标移到第一行行首,按 Ctrl-v 进入块可视模式。按 jj(或 2j)向下选中三行的第一个字符。按 I(大写)在块左侧插入,输入 //,然后按 Esc。三行会同时变成 // int a = 1;、// int b = 2;、// int c = 3;。实战:批量修改对齐的值假设你有一组配置项,想把 = true 改成 = false:debug = trueverbose = truelog = true操作步骤:光标移到第一行的 t 上,按 Ctrl-v 进入块选择。按 2j 向下选中三行,再按 e 向右选中 true 整个单词。按 c,输入 false,按 Esc。三行同时变成 = false。可视模式与 . 命令配合Vim 的 . 命令会重复上一次修改操作。在可视模式下执行的操作同样可以被 . 重复。比如你用 V 选中一段代码做了 > 缩进,之后把光标移到另一段代码按 .,就能重复同样的缩进操作。这在批量格式化代码时效率很高。可视模式与宏配合宏(q 录制)和可视模式可以组合使用。常见场景:录制一个宏,其中包含可视模式选中某段文本并执行操作,然后用 @a 在其他位置重复执行。你也可以先在可视模式下选中多行,然后对选区执行 :'<,'>normal @a,让宏在每一行上运行。可视模式下的搜索与替换在可视模式下按 : 会自动填充 :'<,'>,表示命令范围限定在当前选区。你可以直接跟 s 命令做替换::'<,'>s/old/new/g这比手动计算行号再写 :10,20s/old/new/g 方便得多。你也可以用 :! 对选区执行外部命令,比如 :'<,'>!sort 对选中行排序。另一个实用技巧:在可视模式下按 g 再按 /,可以用选中的文字作为搜索模式(某些 Vim 版本和 Neovim 支持),快速跳转到下一个匹配位置。Select 模式:Vim 里的"普通选中"Vim 还有一个 Select 模式(gh 进入字符选择、gH 进入行选择、gCtrl-h 进入块选择),行为更接近普通编辑器:选中后直接输入文字会替换选区,不用先按 c 或 d。这个模式适合从其他编辑器刚转到 Vim 的用户做过渡,但长期来看,可视模式才是 Vim 的正道——因为可视模式下你可以先选中再决定做什么操作,更灵活。vim-gv 插件:可视化浏览选区历史gv 插件(非内置 gv 命令)提供了一个弹窗,列出你本次会话中的所有可视选区历史,你可以选择任意一条重新选中。安装后按两次 gv(第一次触发内置 gv,第二次触发插件)即可打开历史列表。对于需要频繁在不同选区之间切换的复杂编辑任务,这个插件能省不少事。把可视模式变成肌肉记忆Vim 可视模式的三个子模式覆盖了文本选择的所有场景:v 做精确字符选择,V 做整行操作,Ctrl-v 做列编辑。配合 o 切换端点、gv 重选、块操作的 I/A/c/d,以及与 . 命令和宏的组合,你几乎不需要离开键盘就能完成任何批量编辑。从今天开始,遇到需要选中多行或多列的场景,强迫自己用可视模式而不是鼠标——一周后你会发现编辑速度上了一个台阶。
服务端阅读 05月27日 14:50

Vim 搜索和替换有哪些必须掌握的高级技巧?

Vim 的搜索能力远不止输入关键词然后按回车。正则元字符模式、搜索高亮策略、替换确认机制、跨文件搜索——这些才是真正拉开效率差距的地方。用 / 和 ? 精准定位/ 向下搜索,? 向上搜索,这是最基本的区分。按 n 跳到下一个匹配,N 跳到上一个(在 ? 搜索时方向反转)。搜索当前光标下的单词有两个快捷操作:* 向下搜索整个单词,# 向上搜索。Vim 会自动给关键词加上 \< 和 \> 边界,不会匹配到包含该单词的更长字符串。如果只想搜索光标下单词的部分匹配(不要求单词边界),用 g* 和 g#。四种正则模式:\v \V \m \MVim 的正则语法有四种模式,这是很多人忽略的关键机制:\m(magic):默认模式。.、*、^、$ 等有特殊含义,但 +、?、(、)、{、}、| 需要反斜杠转义才生效。写分组是 \( 和 \),或逻辑是 \|。\v(very magic):所有元字符都直接生效,不需要反斜杠。写分组直接用 (),或逻辑直接用 |,量词直接用 + 和 ?。这是最接近 Perl 正则的模式,写复杂表达式时强烈推荐。\M(nomagic):只有 ^ 和 $ 有特殊含义,其余字符全部当作字面量。\V(very nomagic):只有反斜杠本身有特殊含义,搜索的就是字面文本。当你需要搜索包含大量特殊字符的字符串时,用这个模式最省心。实际用法是在搜索模式开头加上模式修饰符:/\v\d+\.\d+ 匹配浮点数,/\Vfoo.bar 搜索字面的 "foo.bar"。搜索高亮:hlsearch 和 incsearch两个选项控制搜索时的视觉反馈:set hlsearch " 高亮所有匹配项set incsearch " 输入时实时跳转到第一个匹配hlsearch 打开后,最后一次搜索的所有匹配都会高亮显示。缺点是高亮会一直留在屏幕上,用 :nohlsearch(简写 :noh)临时关闭。很多人在 vimrc 中映射一个快捷键:nnoremap <Esc><Esc> :nohlsearch<CR>连按两次 Esc 清除高亮。incsearch 让你在输入搜索模式的过程中就能看到当前匹配位置,不用等按回车。这对复杂正则特别有用,输入到一半就能判断模式是否正确。替换命令 :s 和 :%s基本语法::s/old/new/ " 当前行替换第一个匹配:s/old/new/g " 当前行替换所有匹配:%s/old/new/g " 全文替换:5,20s/old/new/g " 第5行到第20行替换范围除了行号,还支持这些写法:.,$s — 从当前行到文件末尾1,.s — 从第一行到当前行'<,'>s — 可视模式下选中区域(按 : 自动填充)确认替换:c 标志替换最怕改错地方。加 c 标志让每次替换前都确认::%s/old/new/gcVim 会逐个高亮匹配,提示你选择:y — 替换当前n — 跳过当前a — 替换剩余全部q — 退出替换l — 替换当前后退出这个交互流程让大规模替换变得安全可控。正则捕获与反向引用用 \( \) 分组(magic 模式)或 () 分组(very magic 模式),替换时用 \1、\2 引用捕获内容:" 将 "lastName, firstName" 改为 "firstName lastName":%s/\v(\w+),\s*(\w+)/\2 \1/g这里 \1 是第一个括号匹配的内容(姓),\2 是第二个(名)。另一个实用场景——给函数调用加引号::%s/\vfunc\(([^)]+)\)/func("\1")/g大小写控制:\c 和 \C在搜索模式中直接加修饰符比改设置更灵活:/pattern\c — 忽略大小写搜索/pattern\C — 强制区分大小写也可以配合 ignorecase 和 smartcase 选项:set ignorecase " 默认忽略大小写set smartcase " 搜索模式中包含大写字母时自动区分大小写smartcase 的逻辑是:如果你特意输入了大写字母,说明你要精确匹配,否则就忽略大小写。这个组合比单独用 ignorecase 更智能。搜索历史的复用Vim 保存搜索历史,在搜索模式下按上下方向键可以回溯之前的搜索模式。更高效的做法是先输入部分内容再按上下键,Vim 只显示以该内容开头的历史条目。命令行窗口是另一个利器:按 q/ 打开搜索历史窗口,q: 打开命令历史窗口。在这个窗口中可以用 Vim 的全部编辑能力修改历史命令,然后按回车执行。跨文件搜索::vimgrep当搜索范围需要超出当前文件::vimgrep /pattern/ **/*.py这会递归搜索当前目录下所有 .py 文件。搜索结果进入 quickfix 列表。常用操作::copen — 打开 quickfix 窗口:cnext / :cprev — 在匹配项之间跳转:cfirst / :clast — 跳到第一个/最后一个匹配:cclose — 关闭 quickfix 窗口也可以指定文件范围::vimgrep /TODO/ src/**/*.js:vimgrep /FIXME/ *.cquickfix 列表不只服务于 vimgrep,:grep、编译错误等都会用到同一套导航命令,值得记住。从搜索到替换的实战流程一个高效的工作流是先搜索验证,再替换执行:用 /pattern 搜索,n 逐个检查匹配是否符合预期确认无误后直接执行 :%s//replacement/g——注意搜索模式留空,Vim 自动使用最后一次搜索的模式不确定时加 c 标志逐步确认这个流程把搜索和替换打通,避免了在替换命令中重新输入复杂正则的麻烦。Vim 的搜索体系从单文件关键词到跨文件正则,覆盖了文本定位的完整链路。掌握这些技巧后,日常编辑中的查找替换操作会从反复试错变成一次到位。
服务端阅读 05月27日 14:50

Vim 的折叠功能怎么用?

打开一个上千行的配置文件或源码时,满屏文本让人无从下手。Vim 的折叠功能可以把逻辑块收成一行,让代码结构一目了然——但很多人只停留在 zc/zo 的程度,不知道 Vim 其实提供了六种折叠方式,各有适用场景。六种折叠方式Vim 通过 foldmethod 选项决定折叠规则,一共有六种:manual — 手动折叠。用 zf 配合移动命令圈选范围来创建折叠,最灵活但退出后丢失(除非持久化)。适合临时阅读不熟悉的文件。indent — 按缩进折叠。缩进越深,折叠层级越高,Vim 用 shiftwidth 的值把缩进空格数折算成折叠级别。Python、YAML 这类缩进敏感的语言用这个最省心:set foldmethod=indentexpr — 表达式折叠。通过 foldexpr 指定一个 Vim 表达式,对每一行求值返回折叠级别。灵活性最强,写法也最复杂。一个常见用法——按空行分段落折叠:set foldmethod=exprset foldexpr=getline(v:lnum)=~'^\s*$'?'<1':1返回值的含义:正整数表示折叠级别,>N 表示 N 级折叠从此行开始,<N 表示 N 级折叠到此行结束,= 继承上一行级别,a1/s1 分别在上一行基础上加/减一级。为了性能,建议把逻辑封装成函数:set foldexpr=MyFoldLevel()function MyFoldLevel() let line = getline(v:lnum) if line =~# '^\s*$' return '<1' else return 1 endifendfunctionsyntax — 语法折叠。依赖语法高亮文件中定义的 fold 区域,不需要额外配置,前提是当前文件类型的语法文件支持折叠。大部分主流语言开箱即用:set foldmethod=syntaxdiff — 差异折叠。只在 diff 模式下生效,自动把未修改的连续行折叠起来,只展示差异部分。用 vimdiff 比较文件时自动启用,无需手动设置。marker — 标记折叠。通过文本中的标记符号定义折叠边界,默认是 {{{ 和 }}}。标记会写入文件内容,所以退出后依然存在,且支持撤销/重做:set foldmethod=marker" 代码中写:" 函数开始 {{{function! Example() " ...endfunction" }}}不同文件类型可以用对应的注释格式:Python 用 # {{{,HTML 用 <!-- {{{ -->,C 用 /* {{{ */。折叠操作的快捷键掌握创建、删除、打开、关闭四个维度就够了:创建折叠:zf + 移动命令:创建折叠。zf3j 把当前行及下方三行折起来,zf% 折叠配对的括号块,zfa} 折叠当前大括号内的内容。zf + 可视选择:在 Visual 模式下选中后按 zf。删除折叠:zd:删除光标处的一个折叠(只删折叠结构,不删内容)。zD:递归删除光标处所有嵌套折叠。zE:删除当前窗口所有折叠。打开/关闭折叠:zo:打开当前折叠。zc:关闭当前折叠。za:切换开关(最常用)。zO / zC:递归打开/关闭所有嵌套层。zR:打开所有折叠(全局)。zM:关闭所有折叠(全局)。在折叠行上按回车或双击也可以打开折叠,但快捷键更高效。嵌套折叠折叠可以层层嵌套。一个函数内部有 if 块,if 块内部有循环,每层都可以独立折叠。嵌套深度由 foldnestmax 控制,默认没有上限:set foldnestmax=3超过最大嵌套层数的折叠会被合并到允许的最深层级。对于结构复杂的代码,适当限制嵌套层级能避免过度折叠导致结构不清晰。折叠相关的显示选项foldcolumn — 在窗口左侧显示折叠指示列。设置为 0 隐藏,最大 12,建议设为 2 或 3:set foldcolumn=2折叠列里用 - 表示折叠打开的行,+ 表示折叠关闭的位置,| 表示折叠层级的延续。有了折叠列,鼠标点击也可以操作折叠。foldlevel — 控制初始折叠深度。设为 0 打开文件时全部折叠,设为 99 等于全部展开。设一个中间值,打开文件就能看到结构骨架:set foldlevel=2配合 foldlevelstart 可以单独控制打开文件时的初始折叠级别而不影响后续操作。foldminlines — 折叠最少显示行数。如果一个折叠内容不足指定行数,就不允许折叠它。避免把两三行的小块也折起来:set foldminlines=5折叠持久化manual 模式的折叠退出 Vim 就没了。要持久化,在 vimrc 中加:augroup FoldPersist autocmd! autocmd BufWinLeave * mkview autocmd BufWinEnter * silent loadviewaugroup ENDmkview 保存当前窗口的折叠状态、光标位置等信息,loadview 恢复。视图文件默认存在 ~/.vim/view/ 目录下。marker 模式天然持久,因为标记写在文件内容里,但会污染文件,协作项目慎用。indent、expr、syntax 这三种是按规则实时计算的,不需要额外持久化——重新打开文件,折叠会自动重建。大文件折叠的性能折叠是有代价的。foldmethod=expr 和 foldmethod=syntax 需要对每一行求值或语法解析,文件上万行时可能出现明显的卡顿,尤其是滚动和插入时频繁重算。几个应对方法:大文件优先用 indent,计算量最小。把 foldexpr 的逻辑封装成函数,Vim 对编译过的函数调用比直接求值快。用 foldnestmax 限制嵌套层数,减少计算深度。只读查看时开启折叠,编辑时临时切回 manual:set foldmethod=manual。Vim 8+ 可以用 foldmethod=expr 配合 async 插件异步计算,但原生的折叠本身是同步的。如果你经常处理大文件,建议在 vimrc 里按文件类型设置不同的折叠策略,而不是一刀切。推荐的 vimrc 折叠配置把上面这些选项组合起来,一个实用折中的配置:" 默认使用缩进折叠set foldmethod=indentset foldlevel=2set foldcolumn=2set foldminlines=3set foldnestmax=6" 按文件类型覆盖autocmd FileType vim setlocal foldmethod=markerautocmd FileType python setlocal foldmethod=indentautocmd FileType json,yaml setlocal foldmethod=syntax" 持久化 manual 折叠augroup FoldPersist autocmd! autocmd BufWinLeave * mkview autocmd BufWinEnter * silent loadviewaugroup END折叠不是花哨的功能——它解决的是真实问题:在有限屏幕里看清代码结构。花十分钟配好折叠策略,之后每次打开文件都能直接看到骨架而不是一片文字墙。
服务端阅读 05月27日 14:49

Vim 的拼写检查怎么开启和校正?

写代码的时候拼错变量名、写文档的时候拼错单词,这种事谁都遇到过。Vim 从 7.0 开始就内置了拼写检查,不需要额外装插件,配几行就能用。但很多人要么不知道这个功能,要么只知道 :set spell 就停了。下面把常用操作和容易踩的坑都过一遍。开启拼写检查并选择语言核心就两个选项:set spellset spelllang=en_usspell 打开拼写检查,spelllang 指定检查哪种语言。Vim 默认只自带英语拼写文件,第一次切换到其他语言时会自动从 vim.org 下载对应的 .spl 文件,放到 ~/.vim/spell/ 目录下。如果只想对当前缓冲区生效而不影响其他文件,用 setlocal 代替 set:setlocal spell spelllang=en_us关闭拼写检查则是 :set nospell。在拼写错误之间跳转开启之后,Vim 会用不同颜色标记出问题单词:SpellBad(红色)— 不认识的词SpellCap(蓝色)— 应该大写但没大写SpellRare(黄色)— 罕见拼写SpellLocal(绿色)— 不符合当前区域的拼写跳转命令:| 命令 | 作用 ||------|------|| ]s | 跳到下一个拼写错误 || [s | 跳到上一个拼写错误 || ]S | 跳到下一个坏词(只跳 SpellBad,忽略大小写等问题) || [S | 跳到上一个坏词 |日常用 ]s 和 [s 就够了,]S 和 [S 在你只关心"这个词不认识"的时候更精准。用 z= 修正拼写光标放在拼写错误的单词上,按 z=,Vim 会弹出一个建议列表,编号从 1 开始。输入对应数字回车即可替换。如果建议列表太长,可以加计数前缀跳过前面的选项,比如 3z= 直接选第三个建议。在可视模式下先选中一段文本再按 z=,可以对选中部分做批量替换建议。用 zg 添加到词库、用 zw 标记为错误zg 把光标下的词标记为"好词",写入 spellfile,以后不再报错。这在遇到专有名词、项目术语、人名时特别有用。zw 则相反,把光标下的词标记为"坏词"——即使系统词典认为它合法,也会被标红。如果手滑加错了,撤销命令是 zug(撤销 zg)和 zuw(撤销 zw)。注意区分大小写:zG 和 zW 是会话级的,只在当前 Vim 进程内生效,退出后丢失。zg 和 zw 则写入 spellfile,持久保存。自定义词盘:spellfile 配置Vim 的 spellfile 是一个纯文本文件,每行一个单词。默认路径类似 ~/.vim/spell/en.utf-8.add,命名规则是 {语言}.{编码}.add。手动指定 spellfile:set spellfile=~/.vim/spell/en.utf-8.add这个文件可以直接编辑——想批量加词,直接往里面写就行,Vim 会在下次载入时自动编译成 .spl 格式。也可以把它加入版本控制,团队共享一份术语表。想加载多个词盘,用逗号分隔:set spellfile=~/.vim/spell/en.utf-8.add,~/.vim/spell/project.utf-8.addzg 会把词加到第一个 spellfile 里。另外,set complete+=kspell 可以让 Vim 在插入模式补全时也参考拼写词典,输入时按 Ctrl-N / Ctrl-P 即可触发。多语言拼写:中英文混合场景spelllang 支持逗号分隔的多语言列表:set spelllang=en_us,zh_cn这样中英文会同时检查。但有一个现实问题:Vim 对中文的拼写检查能力远不如英文,中文的 .spl 文件并不像英文那样有完整的词库覆盖。实际使用中,zh 的拼写检查价值有限,更多时候还是靠英文检查来抓 typo。一个更务实的做法是只开英文检查,中文文本不会被误报(Vim 对不在词典语言范围内的文本默认不检查):set spelllang=en_us如果你的文档以英文为主、夹杂少量中文,这个配置就够了。编程场景:只在注释和字符串中检查写代码时,如果全局开 spell,变量名和方法名会大面积标红,很干扰。Vim 的语法系统提供了 @Spell 和 @NoSpell 两个集群(cluster),可以让拼写检查只作用于特定语法区域。对大多数 filetype 来说,Vim 自带的语法文件已经把注释和字符串归入了 @Spell,其他区域归入 @NoSpell。所以正常情况下,在代码文件里开 set spell,只有注释和字符串会被检查,变量名不会报错。如果你的某个 filetype 没有做好这个区分,可以在语法文件里手动调整:syntax match myComment "\/\/.*" contains=@Spellsyntax match myIdent "\<\w\+\>" contains=@NoSpell这样就能精确控制哪些区域参与拼写检查。spellfile-plugin:自动下载词盘Vim 自带一个 spellfile-plugin,当 spelllang 设置了一个本地没有对应 .spl 文件的语言时,这个插件会自动从 vim.org 下载。它默认是启用的。如果因为网络问题下载失败,可以手动从 ftp://ftp.vim.org/pub/vim/runtime/spell/ 下载对应的 .spl 文件,放到 ~/.vim/spell/ 目录下。禁用自动下载:let g:loaded_spellfile_plugin = 1与 coc.nvim 和 nvim-lsp 的配合Vim 内置的 spell 是基于词典的拼写检查,不依赖 LSP。但如果你还用了 coc.nvim 或 nvim-lspconfig,两者可以并行不冲突。coc.nvim 有 coc-spell-checker 扩展,底层基于 cspell,可以检查代码中的标识符和注释拼写。它和 Vim 原生 spell 各管各的,不会互相干扰,但也会出现同一段文本两边都报错的情况。如果觉得冗余,可以关掉其中一个。nvim-lsp 方面,有几个专门的拼写检查 Language Server:cspell-lsp — 基于 cspell,支持自定义词典,对代码场景优化好typos-lsp — 轻量快速,专注抓源码中的 typoltex-ls — 基于 LanguageTool,除了拼写还检查语法,适合写文档和 Markdownharper-ls — 隐私友好的语法检查器,离线运行配置示例(nvim-lspconfig + cspell):vim.lsp.enable("cspell_ls")vim.lsp.config("cspell_ls", { cmd = { "cspell-lsp", "--stdio" }, filetypes = { "markdown", "gitcommit", "text" }, root_markers = { ".git" },})实际使用建议:日常写 Markdown 和 commit message 用 Vim 内置 spell 就够了,轻量且零依赖。如果项目需要更严格的拼写检查(比如开源项目要求 CI 里也跑 cspell),再上 LSP 方案。常用配置汇总一段比较实用的 .vimrc 配置:" 拼写检查set spellset spelllang=en_usset spellfile=~/.vim/spell/en.utf-8.addset complete+=kspell" 只在特定文件类型开启autocmd FileType markdown,gitcommit setlocal spellautocmd FileType python,javascript setlocal nospell" 快捷键nnoremap <F5> :set spell!<CR>nnoremap <leader>s ]snnoremap <leader>S [sVim 的拼写检查不算复杂,但覆盖面比很多人想象的广——从简单的英文纠错到多语言混合、代码注释定向检查、团队共享词盘,都能做。花五分钟配好,之后每次写文档和 commit message 都能少犯几个低级拼写错误。
服务端阅读 05月27日 14:48

Vim 退出后如何恢复上次的工作状态?

每次关闭 Vim 再重新打开,窗口布局没了,文件列表清空,折叠消失了——这种"从零开始"的体验让人抓狂。Vim 内置的会话(session)和视图(view)功能,专门解决这个问题。用 :mksession 保存完整工作环境:mksession 把当前 Vim 的窗口布局、标签页、缓冲区列表、折叠状态、当前目录等信息序列化成一个 Vim 脚本文件:" 保存到当前目录的 Session.vim:mksession" 指定路径:mksession ~/sessions/project-a.vim" 文件已存在时强制覆盖:mksession! ~/sessions/project-a.vim会话文件本质上是一段 Vim 脚本,可以直接打开查看。里面记录了 tabnew、split、edit 等命令,Vim 逐行执行就能还原你的编辑环境。用 :source 或 vim -S 恢复会话恢复会话有两种方式:" 在 Vim 内部加载:source Session.vim# 启动时直接加载vim -S Session.vim# 等价写法vim -S ~/sessions/project-a.vimvim -S 是最常用的方式,可以把加载会话的命令写进项目目录的 Makefile 或 shell alias 里,一条命令就能回到上次的工作状态。sessionoptions 控制保存范围不是所有东西都需要保存到会话里。sessionoptions(缩写 ssop)决定了 :mksession 写入哪些内容:" 查看当前设置:set sessionoptions?" 典型配置:set sessionoptions=buffers,curdir,folds,help,tabpages,winsize,terminal常用选项说明:buffers — 缓冲区列表(包括隐藏缓冲区)tabpages — 所有标签页,去掉则只保存当前标签页winsize — 窗口大小比例folds — 手动折叠信息curdir — 当前工作目录terminal — 终端窗口实际使用中经常需要微调。比如你不希望会话保存终端窗口,因为重新打开时原来的 shell 进程已经不存在了::set sessionoptions-=terminal不想保存空白窗口::set sessionoptions-=blank:mkview 和 :loadview 保存单个窗口的状态会话保存的是全局状态,但有时你只想保存某个窗口的折叠、滚动位置和本地选项。这时用视图:" 保存当前窗口的视图:mkview" 加载当前窗口的视图:loadview视图和会话的区别在于作用域——视图只管当前窗口,会话管整个 Vim 实例。视图默认保存在 viewdir 目录下,文件名由缓冲区路径编码而来:" 查看 viewdir 位置:set viewdir?" 自定义 viewdir:set viewdir=~/.vim/views一个常见用法是在 vimrc 里自动保存和恢复视图:autocmd BufWinLeave *.py mkviewautocmd BufWinEnter *.py loadview这样每次切换 Python 文件时,之前的折叠和滚动位置都能自动恢复。会话与标签页、窗口、缓冲区的关系理解这三者的关系有助于用好会话:缓冲区(buffer):文件在内存中的实例,会话保存的是缓冲区列表,不是文件内容窗口(window):缓冲区的视口,一个缓冲区可以出现在多个窗口中,会话保存窗口的布局和尺寸标签页(tabpage):窗口的容器,每个标签页有自己的窗口布局,会话保存所有标签页(如果 sessionoptions 包含 tabpages)关键点:会话恢复时,Vim 会先加载缓冲区列表,然后按照保存的布局重建窗口和标签页。如果文件已经被移动或删除,对应的窗口会变成空缓冲区。项目级会话管理给不同项目维护独立的会话文件是最实用的做法。在项目根目录保存一个 Session.vim,启动时用 vim -S 加载:# 在项目目录下cd ~/projects/my-appvim -S更规范的做法是把会话文件放在统一目录,按项目名区分:mkdir -p ~/.vim/sessionsvim -S ~/.vim/sessions/my-app.vim也可以在 vimrc 里根据当前目录自动选择会话:let g:session_dir = expand('~/.vim/sessions/')let g:session_file = g:session_dir . substitute(getcwd(), '/', '_', 'g') . '.vim'自动保存和恢复会话手动执行 :mksession 容易忘记。用自动命令在退出 Vim 时自动保存:" 退出时自动保存会话autocmd VimLeave * if v:this_session != '' | execute 'mksession!' v:this_session | endifv:this_session 变量保存着当前会话文件的路径。如果还没加载过会话,这个变量为空字符串,此时不执行保存,避免在随意打开文件时产生多余的 Session.vim。配合启动时自动加载:" 启动时加载项目会话(无参数时)autocmd VimEnter * if argc() == 0 && filereadable(g:session_file) | execute 'source' g:session_file | endifargc() == 0 确保只有直接输入 vim 不带文件参数时才加载会话,避免和 vim file.txt 这种用法冲突。tpope/vim-obsession 插件手动管理会话的自动命令虽然能工作,但需要处理各种边界情况(比如打开多个 Vim 实例、会话文件冲突)。tpope 的 vim-obsession 插件把这些细节都封装好了:" 安装后,在项目目录执行:Obsess ~/.vim/sessions/my-app.vim" 停止自动保存:Obsess!启动 Obsession 后,它会持续跟踪当前会话的变化,在合适时机自动更新会话文件。退出 Vim 时自动保存,不需要额外配置。和手动方案相比,vim-obsession 的优势在于:不会在未启动 Obsession 的情况下覆盖已有会话正确处理多实例场景和 vim-fugitive 等插件配合良好如果你经常在多个项目之间切换,vim-obsession 基本上是必装的。viminfo 和会话的分工会话保存的是"你在看什么"——窗口、标签页、缓冲区布局。viminfo 保存的是"你做过什么"——命令历史、搜索历史、寄存器内容、标记位置。" 保存 viminfo:wviminfo ~/.vim/viminfo" 加载 viminfo:rviminfo ~/.vim/viminfo完整的恢复需要两者配合:会话还原布局,viminfo 还原操作历史。Vim 默认在退出时自动写入 viminfo,所以通常只需要手动管理会话部分即可。Vim 的会话机制把"恢复工作环境"这件事从手动操作变成了可自动化的流程。从最基础的 :mksession / :source 开始,配合 sessionoptions 调整保存范围,再用自动命令或 vim-obsession 实现无人值守的保存恢复,这个渐进式的路径覆盖了从偶尔用到每天依赖的全部场景。
服务端阅读 05月27日 14:46

Vim 的键映射怎么配置才不会踩坑?

Vim 的强大很大程度上来自键映射——把任意按键组合重新定义为自定义操作。但映射命令种类繁多,map 和 noremap 的区别、各模式前缀的含义、Leader 键的用法,稍不注意就会写出互相冲突甚至递归死循环的配置。这篇文章把这些问题逐个说清楚。map / nmap / vmap / imap / omap / cmap 分别管哪个模式Vim 有多种编辑模式,映射命令的前缀决定了它生效的范围:map:普通模式 + 可视模式 + 操作符等待模式都生效,范围最广nmap:仅普通模式(Normal)vmap:仅可视模式(Visual),包括行可视和块可视imap:仅插入模式(Insert)omap:仅操作符等待模式(Operator-pending),比如按 d 之后等待移动目标的那段时间cmap:仅命令行模式(Command-line),即按 : 之后输入命令时实际配置中,nmap 和 imap 使用频率最高。omap 用得少但很有用——比如你可以把 dw 中 w 的行为重新定义,而不影响普通模式下直接按 w 的跳转。为什么推荐 noremap 而不是 mapmap 系列命令是递归映射。当你写了这样的配置:nmap j gjnmap gj 5j按 j 时 Vim 会先展开为 gj,再把 gj 展开为 5j,最终变成按5次 j,而 j 又被映射为 gj——无限递归,Vim 直接报错。noremap 系列命令(nnoremap、inoremap、vnoremap、onoremap、cnoremap)是非递归映射,右侧的内容按字面意义执行,不会再被其他映射展开:nnoremap j gjnnoremap gj 5j这样按 j 执行 gj(按屏幕行移动),按 gj 执行 5j(向下5行),互不干扰。结论:除非你有明确的递归需求,否则永远用 noremap 系列。 这是 Vim 配置最基本的一条纪律。 键:给你的快捷键加个命名空间Leader 键是一个自定义前缀,用来把你的个人映射和 Vim 默认键位隔离开。不设置的话默认是反斜杠 \,但反斜杠位置偏僻,大多数人会改成逗号或空格:let mapleader = ","" 或者用空格let mapleader = " "之后所有以 <leader> 开头的映射都会把该键作为前缀:nnoremap <leader>w :w<CR>nnoremap <leader>q :q<CR>nnoremap <leader>ev :vsplit $MYVIMRC<CR>如果 Leader 是逗号,按 ,w 就保存文件,按 ,q 就退出,按 ,ev 就垂直分割打开 vimrc。查看当前 Leader 键::echo mapleader还有个 <localleader>,专门给文件类型插件用,和全局 Leader 互不干扰,通常设为反斜杠或其他键。:只对当前文件生效的映射默认情况下映射是全局的,所有缓冲区都生效。加 <buffer> 后映射只在定义它的那个缓冲区里有效:nnoremap <buffer> <F5> :!python3 %<CR>这条映射只在定义时活跃的那个 Python 文件里按 F5 运行脚本,切换到别的文件就没了。FileType 插件里大量使用这个参数,确保不同文件类型有各自的快捷键而不互相覆盖。:不让映射污染命令行普通映射执行时,命令行区域会闪出映射的内容。比如:nnoremap <leader>h :nohlsearch<CR>按 ,h 时命令行会短暂显示 :nohlsearch。加上 <silent> 就不会显示:nnoremap <silent> <leader>h :nohlsearch<CR>对于切换开关类的映射(拼写检查、搜索高亮、折叠),<silent> 几乎是标配,避免命令行闪烁干扰注意力。:动态计算映射内容<expr> 让映射的右侧不是一个固定的字符串,而是一个表达式,Vim 每次按键时都会求值:nnoremap <expr> <leader>n (&number ? ':set nonumber<CR>' : ':set number<CR>')按 <leader>n 时,Vim 先算 &number 的值——如果行号是开着的就执行 set nonumber,关着就执行 set number,实现切换。更实用的例子是让 <Tab> 在插入模式里根据上下文决定是插入制表符还是触发补全:inoremap <expr> <Tab> pumvisible() ? "\<C-N>" : "\<Tab>"补全菜单可见时按 Tab 跳到下一项,否则插入普通 Tab。映射特殊键Vim 里有不少按键没法直接写在映射里,需要用特殊表示法:| 特殊键 | 表示法 | 用途示例 ||--------|--------|----------|| 回车 | <CR> | nnoremap <leader>w :w<CR> || Esc | <Esc> | inoremap jk <Esc> || Tab | <Tab> | 插入模式补全 || 空格 | <Space> | let mapleader = "\<Space>" || Ctrl+X | <C-X> | <C-A> 全选、<C-R> 重做 || Alt+X | <A-X> 或 <M-X> | Meta 键 || 无操作 | <NOP> | 禁用某个键:nnoremap <Up> <NOP> || F1-F12 | <F1>-<F12> | 功能键 |注意 <CR> 是回车(Carriage Return),几乎每个执行冒号命令的映射末尾都要加,否则命令只显示在命令行不会执行。常见映射方案快速保存和退出:nnoremap <leader>w :w<CR>nnoremap <leader>q :q<CR>nnoremap <leader>x :x<CR>插入模式快速回到普通模式:inoremap jk <Esc>inoremap jj <Esc>按屏幕行移动(长行折行时很好用):nnoremap j gjnnoremap k gknnoremap 0 g0nnoremap $ g$窗口导航:nnoremap <C-h> <C-w>hnnoremap <C-j> <C-w>jnnoremap <C-k> <C-w>knnoremap <C-l> <C-w>l清除搜索高亮:nnoremap <silent> <leader><CR> :nohlsearch<CR>可视模式快速缩进:vnoremap < <gvvnoremap > >gv选中文本后按 < 或 > 缩进,gv 重新选中,可以连续按。映射冲突排查当某个按键不按预期工作,用这几个命令诊断:" 查看某个键的所有映射:verbose map <leader>w" 查看当前所有映射:map" 只看普通模式映射:nmapverbose 会显示映射定义在哪个文件的第几行,定位插件冲突的关键信息。如果发现插件覆盖了你的映射,常见处理方式:在插件加载之后再定义你的映射(vimrc 中的顺序很重要)用 Leader 键作为前缀,几乎不会和插件默认映射冲突对特定文件类型使用 <buffer> 局部映射,避免全局冲突用 <nowait> 让映射立即执行,不被更长的映射序列截获:nnoremap <nowait> <leader>a :echo "immediate"<CR>Vim 的键映射体系看起来命令很多,核心就三条:用 noremap 避免递归、用 Leader 组织命名空间、用 <buffer> 限定作用域。把这三点贯彻到配置里,其他参数都是在这个基础上按需叠加。
服务端阅读 05月27日 14:46

Vim 宏录制功能怎么用才能高效重复操作?

每天在 Vim 里重复同样的编辑动作,一遍又一遍地按键、移动、修改——如果有个按钮能把这串操作"录下来,一键回放",效率会怎样?Vim 的宏录制就是这样一个功能:它不是花架子,而是真正能省下大量重复劳动的工具。宏录制的基本流程:q 开始,q 结束宏的核心逻辑很简单:按 q 加一个寄存器名开始录制,再按 q 停止录制。具体步骤:在普通模式下按 qa——把后续操作录制到寄存器 a 中,左下角会出现 recording @a 提示执行你需要的编辑操作(移动、删除、插入、替换……任何普通模式命令都行)按 q 停止录制仅此而已。录完后,寄存器 a 里就存好了你刚才的整个操作序列。一个关键细节:录制前先把光标放到一个"干净"的位置。很多人第一次录宏时,光标在行中间就开始操作,结果回放时位置不对,整个宏就废了。养成习惯,录制前先按 0 回到行首,这样每次回放都从确定的位置开始。回放宏:@ 和 @@录好的宏用 @ 加寄存器名回放:@a —— 执行寄存器 a 中的宏@@ —— 重复执行上次回放的宏(不用再敲寄存器名)5@a —— 把宏执行 5 次最实用的组合是:录好宏之后,先用 @a 跑一次确认效果没问题,然后直接 10@@ 或 50@@ 批量执行。如果中间某次执行出错(比如搜索没匹配到),宏会自动停止,不会一路错下去。追加录制:用大写字母往宏里加步骤录完宏发现漏了一步怎么办?不用重新录。按大写字母可以往已有宏的末尾追加操作。假设之前用 qa 录了一个宏,现在想在里面加一个操作:按 qA(大写 A)开始追加录制执行你要补充的操作按 q 结束这样寄存器 a 中的内容就是原来的操作加上新追加的操作。这在调试宏时特别有用——先录一个基础版本试跑,发现缺什么再追加。宏寄存器:宏就是文本,可以查看和编辑宏存储在 Vim 的命名寄存器(a-z)中,本质上就是一段按键序列的文本。这意味着你可以直接查看和修改它。查看寄存器内容::reg a把宏粘贴出来编辑:"ap这会把寄存器 a 的内容当作普通文本粘贴到当前光标位置。你直接改文本,改完再用 "ayy 把这一行存回寄存器 a。对于比较长的宏,这种编辑方式比重新录制快得多。还有一个技巧:你可以在命令行直接设置寄存器内容::let @a = '0iHello^[其中 ^[ 是 Esc 键的表示,用 Ctrl+V 然后 Esc 输入。这样你甚至可以把宏写成配置文件的一部分。批量执行::normal 命令配合可视模式逐行 @a 效率太低,Vim 提供了两种批量执行宏的方式。方式一:用次数前缀5@a简单直接,但要确保文件确实有足够的行,否则宏在中途找不到目标行会报错停止。方式二:可视模式 + :normal可视模式选中目标行(V 然后 j 或 G)输入 :normal @a这种方式更稳妥——只在你选中的行上执行,不会跑飞。而且即使某一行执行出错,其他行照常执行,互不影响。处理大文件时,这个特性非常关键。方式二的进阶用法:对整个文件执行宏。:%normal @a等价于先 ggVG 全选再 :normal @a,但写法更简洁。复杂宏技巧:计数、搜索与递归宏不只是一堆简单的移动和编辑命令,它完全可以包含搜索、计数等高级操作。在宏中使用搜索录制时按 /pattern<CR> 搜索目标位置,回放时宏会自动执行这个搜索。这对于"找到下一个符合条件的位置再操作"的场景非常有效。但要注意:搜索命令在不同行可能匹配到不同位置。如果你的操作依赖精确的列位置,搜索后最好加一个 0 或 ^ 把光标规范到行首。计数器递增Vim 有个 Ctrl+A 命令可以让光标下的数字加 1。结合宏可以快速生成递增序列:在第一行输入 1qa 开始录制yy 复制当前行,p 粘贴到下一行Ctrl+A 让数字加 1q 结束录制98@a 生成 1 到 100递归宏:让宏自己调用自己在录制宏的最后一步,输入 @a(调用自身),然后再按 q 结束录制。这样宏就会不断递归执行,直到某一步出错自动停止。qa " 开始录制到 a0 " 移到行首/pattern " 搜索目标dd " 删除该行@a " 递归调用自身q " 结束录制递归宏适合处理"不确定有多少行需要操作"的情况——不用先数行数再决定执行几次,它会一直跑到搜索失败为止。宏的持久化:让宏在重启后仍然可用默认情况下,宏只存在于当前 Vim 会话中,退出就没了。要让宏持久化,有几种方式:方式一:写入 vimrclet @a = '0dwelp'把宏内容直接写进配置文件,每次启动 Vim 自动加载。方式二:使用 viminfoVim 默认会将寄存器内容写入 viminfo 文件,下次启动时恢复。确认你的 vimrc 中有:set viminfo='100,<50,s10,h,rA:,rB:其中 '100 表示保存最近 100 个文件的信息,包括寄存器。方式三:保存到文件:call writefile([@a], 'my_macro.txt')下次要用时::let @a = readfile('my_macro.txt')[0]这种方式适合在不同机器间共享宏。常见应用场景批量给行加引号qa " 录制到 aI" " 行首插入引号Esc A" " 行尾插入引号Esc j " 下一行q " 结束:%normal @a " 全文执行CSV 数据提取从一行数据中提取特定列,删掉其余部分:qa " 录制0df, " 删到第一个逗号2f,ld$ " 定位到第三列,删到行尾j " 下一行q " 结束代码批量重命名把所有 old_method 替换成 new_method,同时保留行尾注释:qa " 录制0/new_method" 搜索cwnew_method " 替换Esc j " 下一行q " 结束当然简单替换用 :%s/old_method/new_method/g 更快,但宏的优势在于可以同时处理更复杂的组合操作——比如替换后还要调整缩进、移动位置、插入新行等,这些 :s 命令做不到。Markdown 表格对齐qa " 录制0f|lxA " 删除多余空格Esc f|lxA " 下一列同样操作Esc j " 下一行q " 结束宏与点命令的区别很多人会问:已经有了 .(重复上一次修改),为什么还要宏?两者的核心区别:| 特性 | 点命令 . | 宏 @a ||------|-----------|---------|| 记录范围 | 只记录一次修改 | 记录完整操作序列 || 是否包含移动 | 不包含 | 包含 || 能否保存 | 不能 | 存在寄存器中 || 能否编辑 | 不能 | 可以 || 适用场景 | 单一修改的重复 | 多步操作的重复 |简单说,dot 适合"同一修改,多处应用";宏适合"同一套操作流程,多行执行"。如果你的重复操作里只有一步修改,用 .;如果有移动、搜索、多次修改的组合,用宏。Vim 的宏录制不是什么高深技巧,但它是从"手动重复"到"自动化编辑"的关键一步。核心就三件事:q 开始录、@ 回放、可视模式批量执行。掌握这三点,大部分重复编辑场景都能应对。遇到更复杂的需求,再考虑追加录制、递归宏、寄存器编辑这些进阶手法。录宏时记住一个原则——让每一步操作都位置无关,这样回放时才不会跑偏。