标签

Vim

Vim 是一种高度可配置的文本编辑器,用于有效地创建和更改任何类型的文本。它是从 vi 编辑器衍生出来的,最初由 Bram Moolenaar 于 1991 年发布。Vim 是自由和开源软件,并且包含了许多增强功能,使其成为程序员和文本编辑者广泛使用的工具之一。

Vim
查看更多相关内容
服务端5月27日 15:07
Vim 中删除、复制、粘贴、撤销和替换命令怎么用?Vim 的编辑效率几乎全部来自普通模式下的单键命令——删、复、粘、撤、换,五个动词覆盖了日常文本操作的大半场景。下面逐类拆解,每条命令都给出真实用法,不省略边界情况。 ## 删除:x / dd / D / dw / d$ / db 删除是 Vim 里最频繁的操作,也是理解 Vim「动词+名词」语法的入口。 | 命令 | 作用 | 注意 | |------|------|------| | `x` | 删除光标所在字符 | 等价于 `dl`,不进入插入模式 | | `dd` | 删除整行 | 最常用的行删除,删后下方行上移 | | `D` | 删除到行尾 | 等价于 `d$`,保留行首到光标前的内容 | | `dw` | 删除到下一个词首 | 如果光标在词首,整个词加尾部空格一起删 | | `d$` | 删除到行尾 | 与 `D` 等价,但更显式 | | `db` | 删除到上一个词首 | 向左删除到当前词或前一词的开头 | 一个容易踩的坑:`dw` 在光标位于词尾时,会删到下一个词首,连同中间空格一并删除。如果你只想删当前词而不管光标在哪,用文本对象 `diw` 或 `daw` 更可靠——后面会讲。 删除的内容不会消失,而是进入无名寄存器 `""`,随时可以粘贴回来。如果你想删除但不污染寄存器,用黑洞寄存器:`"_dd`,这行删了就彻底没了。 ## 复制:yy / Y / yw / y$ Vim 把复制叫 yank,对应的动词是 `y`。它的用法和 `d` 完全对称——`d` 能接的 motion,`y` 都能接。 - `yy`:复制整行,最常用。`yy` 后按 `p` 就能在下一行粘贴出同样的内容。 - `Y`:在 Vim 中等同于 `yy`(复制整行),和 `D` 不删到行尾的对称关系不同,这里容易误解。如果你想让 `Y` 行为变成 `y$`,可以在 vimrc 里加 `nnoremap Y y$`。 - `yw`:从光标位置复制到下一个词首。 - `y$`:从光标位置复制到行尾。 复制的文本同样进入无名寄存器。此外,最近一次 yank 的内容还会存入寄存器 `"0`。这意味着即使你之后做了删除操作覆盖了无名寄存器,`"0p` 仍然能粘贴之前 yank 的内容——这个技巧在实际编辑中非常实用。 ## 粘贴:p / P 粘贴只有两个键,但行为取决于你复制/删除的是行还是字符: - `p`:粘贴到光标**之后**。如果寄存器内容是整行,粘贴到当前行下方;如果是字符片段,粘贴到光标右侧。 - `P`:粘贴到光标**之前**。行内容贴到上方,字符内容贴到左侧。 一个经典用法:先用 `yy` 复制一行,再把光标移到目标位置按 `p`,新行就出现在下方。如果想替换已有行,先 `dd` 删掉目标行再 `p`。 可视模式下的粘贴行为略有不同:选中一段文本后按 `p`,选中的内容会被寄存器内容替换,而被替换掉的文本又进入寄存器——可以用来做文本交换。 ## 撤销与重做:u / U / Ctrl-r 编辑出错就要撤回,Vim 提供了三个层级的撤销: - `u`:撤销最近一次修改。连续按 `u` 会依次往前撤,直到文件打开时的状态。 - `U`:撤销当前行的所有修改,把这一行恢复到最近一次进入时的样子。注意 `U` 本身也是一个修改,可以用 `u` 再撤销掉。 - `Ctrl-r`:重做被 `u` 撤销的操作,往前推进。 一个实际建议:如果你不确定撤销到了哪一步,可以按 `g-` 跳到更早的文件状态,按 `g+` 跳到更晚的状态。Vim 用撤销树而非线性撤销,`g-/g+` 按时间遍历,`u/Ctrl-r` 按分支遍历,两者适用场景不同。 ## 替换:r / R / s / S / ~ 替换命令让你在不离开普通模式的情况下修改文本: - `r{char}`:用 `{char}` 替换光标下的单个字符。比如光标在 `a` 上,按 `rx` 就变成 `x`,然后光标停原地,仍在普通模式。 - `R`:进入替换模式(Replace mode),逐字替换,每输入一个字符就覆盖光标下的字符,直到按 `Esc` 退出。 - `s`:删除光标下的字符并进入插入模式。等价于 `cl`,适合只改一个字符的场景。 - `S`:删除当前行内容(保留缩进)并进入插入模式。等价于 `^C`,适合重写一行但保留缩进层级。 - `~`:切换光标下字符的大小写,并自动右移。连续按 `~~~` 可以翻转三个字符。 `~` 在可视模式下尤其好用:选中一段文本后按 `~`,整块内容的大小写全部翻转。 ## 与文本对象结合:diw / daw / ciw 文本对象是 Vim 区别于其他编辑器的核心特性。它的语法是「操作符 + i/a + 对象」,`i` 表示 inner(不含周围空格),`a` 表示 around(含周围空格)。 以 word 对象为例: - `diw`:删除光标所在词,不管光标在词的哪个位置。删完后留下空格。 - `daw`:删除光标所在词及其相邻空格,删完后前后词紧邻。 - `ciw`:删除光标所在词并进入插入模式,可以直接输入新词替换。 和 `dw` 的关键区别:`dw` 是从光标位置删到下一个词首,如果光标在词中间,只会删掉半个词;而 `diw` / `daw` 总是删掉完整词,不依赖光标的精确位置。 文本对象不限于词。`di"` 删除双引号内的内容,`ci(` 删除括号内内容并进入插入模式,`yi'` 复制单引号内的文本——这些组合在日常编码中极其高频。 ## 与计数结合:3dd / 5x Vim 的几乎所有命令都可以在前面加数字表示重复次数: - `3dd`:删除 3 行(从当前行开始)。 - `5x`:删除 5 个字符。 - `2yy`:复制 2 行。 - `3p`:粘贴 3 次。 - `5r-`:用 `-` 替换光标及后面共 5 个字符,变成 `-----`,画分隔线很好用。 计数也可以和文本对象组合:`d3w` 删除 3 个词,`y2iw` 复制 2 个词(注意 `2iw` 把两个词当作一个对象)。 实际使用中,`3dd` 和 `2yy` 这类整行操作最常见,字符级计数用得相对少——毕竟你可以用可视模式更直观地选择范围。 ## 寄存器指定:"ay / "ap Vim 有超过 20 个寄存器,默认操作都走无名寄存器。当你需要保留多段文本时,命名寄存器就派上用场了: - `"ayy`:把当前行复制到寄存器 `a` 中。 - `"ap`:粘贴寄存器 `a` 的内容。 - `"bdd`:把当前行删除并存入寄存器 `b`。 - `"bp`:粘贴寄存器 `b` 的内容。 命名寄存器用小写字母 `a-z`,共 26 个。如果用大写字母 `"Ayy`,则是追加到寄存器 `a` 而非覆盖。 几个特殊寄存器也值得了解: - `"0`:最近一次 yank 的内容,不会被删除操作覆盖。 - `"+`:系统剪贴板,`"+yy` 复制行到系统剪贴板,`"+p` 从系统剪贴板粘贴。 - `"_`:黑洞寄存器,`"_dd` 删除且不存入任何寄存器。 ## 可视模式下的操作 可视模式让你先选中、再操作,对于不规则范围的编辑非常直观: 1. 按 `v` 进入字符可视化,用方向键或 `w/b/e` 扩展选择范围。 2. 按 `V` 进入行可视化,整行整行地选。 3. 按 `Ctrl-v` 进入块可视化,可以选中矩形区域。 选中后可以执行: - `d`:删除选中内容。 - `y`:复制选中内容。 - `p`:用寄存器内容替换选中内容。 - `r{char}`:把选中区域每个字符都替换为 `{char}`,批量注释代码时用 `r#` 非常方便。 - `~` / `U` / `u`:翻转选中内容的大小写。 块可视化还有一个隐藏技巧:选中列后按 `I`(大写 i)输入文本,再按两次 `Esc`,输入的内容会出现在选中列的每一行——这是批量在行首加缩进或注释标记的标准做法。 --- 以上命令全部在普通模式下执行(可视模式的选中阶段除外)。记住一个核心规律:Vim 的命令遵循「计数 + 操作符 + 范围/对象」的语法,掌握了 `d/y/c/r` 这几个操作符,配合 motion 和文本对象,就能组合出绝大多数编辑操作。不需要死记,用两周肌肉记忆就会形成。
服务端5月27日 15:07
Vim 和 Neovim 到底有什么区别?现在该选哪个?## 从一次被拒绝的补丁说起 2014 年,巴西开发者 Thiago de Arruda 向 Vim 项目提交了一组补丁,核心诉求是给 Vim 加上异步执行能力。Vim 的作者 Bram Moolenaar 拒绝了这组补丁——理由是对一个有二十多年历史的代码库做这么大的架构改动,风险太高。Thiago 随后 fork 了 Vim,通过众筹筹到一万美元,Neovim 项目就此诞生。这不是一次意气用事的分叉,而是一场关于"编辑器该往哪走"的根本分歧。 ## 历史分叉:2014 年发生了什么 Vim 的代码库从 1991 年开始积累,到 2014 年已经超过 30 万行 C 代码,包含大量对 Amiga、OS/2 等早已无人使用的平台的支持。Bram 坚持通过邮件列表接收 patch 的开发流程,合并代码的节奏非常保守。Thiago 认为这种模式已经严重阻碍了 Vim 的进化,他的目标很明确:砍掉过时代码、引入现代架构、建立更开放的社区治理。 2015 年 12 月,Neovim 第一个公开版本 0.1 发布。有意思的是,Neovim 的出现反而刺激了 Vim 自身的开发——2016 年 Vim 8.0 发布,加入了异步 job 和 timer 功能,这在一定程度上是对 Neovim 竞争的回应。2023 年 Bram Moolenaar 去世后,Vim 社区的开发节奏明显放缓,而 Neovim 仍在高速迭代。 ## 配置语言:Lua 对阵 VimScript 这是用户感受最直接的区别。Vim 的配置语言是 VimScript,一门诞生于编辑器内部的脚本语言,语法松散、执行效率低、调试困难。Vim9 script 试图改善性能,但生态基本没有跟上。 Neovim 选择 Lua 作为一等配置语言。Lua 本身就是一门成熟的嵌入式脚本语言,执行速度远超 VimScript,JIT 编译后差距更大。实际写起来: - VimScript 配置:`let g:mapleader = ','` - Lua 配置:`vim.g.mapleader = ','` 语法差异不大,但 Lua 的优势在于模块化。你可以把配置拆成多个 `.lua` 文件,用 `require` 加载,而不是在一个巨大的 `init.vim` 里用 `source` 拼接。Neovim 仍然兼容 VimScript,所以迁移不是一步到位的,可以在 `init.lua` 里混用 `vim.cmd` 调用 VimScript 命令。 ## 内置 LSP:IDE 级别的语言支持 Neovim 0.5(2021 年 7 月发布)引入了原生 LSP 客户端 `vim.lsp`。这意味着你不需要安装 coc.nvim 或 ALE 这样的第三方插件,Neovim 开箱就能连接语言服务器,获得代码补全、跳转定义、悬浮文档、重命名、诊断等能力。 Vim 至今没有内置 LSP 客户端。如果你想在 Vim 里获得类似体验,只能依赖 coc.nvim(基于 Node.js)或 vim-lsp,配置复杂度和资源占用都更高。 配合 mason.nvim 插件,Neovim 可以一键安装和管理语言服务器,整个 LSP 工作流已经和 VS Code 一样顺畅。 ## Tree-sitter:语法高亮的范式转变 Vim 的语法高亮基于正则表达式,这是 90 年代的设计。正则匹配无法理解代码结构,所以高亮经常出错,尤其是嵌套模板、JSX、混合语言文件这类场景。 Neovim 集成了 Tree-sitter,一个基于抽象语法树(AST)的增量解析器。它不是在文本上跑正则,而是真正解析代码结构,然后根据语法节点类型做高亮。结果是: - 高亮更准确,不会把字符串里的关键字标红 - 支持语义级高亮(区分函数调用、变量定义、类型注解等) - 增量解析速度极快,编辑时几乎无延迟 - 内置 50 多种语言的 parser Vim 社区也有 tree-sitter 的移植项目,但远不如 Neovim 的深度集成。 ## 异步架构:libuv 带来的质变 Neovim 的底层用 libuv 重写了事件循环,所有 I/O 操作——文件读写、语言服务器通信、插件加载、shell 命令执行——都是异步的。这意味着你在跑测试、格式化代码、等待 LSP 响应的时候,编辑器界面不会卡顿。 Vim 8.0 也加入了 `job_start` 和 `timer_start`,提供了基本的异步能力,但深度不够。很多 Vim 插件仍然在主线程上做同步操作,因为 Vim 的 API 设计没有强制插件作者考虑异步。 实测数据:空配置启动,Neovim 约 12ms,Vim 约 28ms。配置了完整 LSP + 补全 + 文件搜索的开发环境后,Neovim 的响应优势更明显。 ## 浮动窗口与弹窗 Neovim 原生支持浮动窗口(floating window),可以在编辑区上方弹出半透明的面板,用于显示补全菜单、文档预览、诊断信息等。这是现代编辑器体验的关键组件。 Vim 8.2 加入了 popup window,功能类似但 API 灵活度不如 Neovim 的实现。Neovim 的浮动窗口可以叠加、设置透明度、精确控制位置和大小,插件生态围绕这个能力构建了 telescope 的预览窗口、nvim-cmp 的文档浮窗、lspsaga 的代码操作面板等体验。 ## 内置终端 Neovim 内置了终端模拟器,通过 `:terminal` 命令可以直接在编辑器里打开一个 shell。配合浮动窗口插件(如 toggleterm.nvim、FTerm.nvim),可以一键弹出/隐藏终端,不需要离开编辑器切换到外部终端。 Vim 也有 `:terminal`,但 Neovim 的终端实现和窗口系统的集成更紧密,配合浮动窗口和终端模式的键位映射,使用体验更接近 VS Code 的集成终端。 ## 插件生态:两条不同的路 Neovim 的插件生态已经完全 Lua 化,形成了现代化工具链: | 功能 | Neovim 插件 | Vim 插件 | |------|------------|---------| | 插件管理 | lazy.nvim | vim-plug | | 模糊搜索 | telescope.nvim | fzf.vim | | 自动补全 | nvim-cmp | coc.nvim | | LSP 配置 | nvim-lspconfig | coc.nvim | | LSP 安装 | mason.nvim | 手动安装 | | 语法高亮 | nvim-treesitter | 正则语法文件 | | Git 集成 | gitsigns.nvim | vim-fugitive | 关键差异不在单个插件,而在整体协同。Neovim 的 Lua 插件之间可以无缝通信——telescope 的搜索结果可以直接预览文件,nvim-cmp 的补全源可以来自 LSP、Tree-sitter 和 snippet,lazy.nvim 可以延迟加载插件到毫秒级。Vim 的插件生态更成熟但更碎片化,很多流行插件最后更新时间在 2023 年之前。 ## 迁移成本:从 Vim 到 Neovim 有多难 答案是:很低。Neovim 兼容绝大部分 VimScript 配置,你可以直接把 `.vimrc` 软链到 Neovim 的配置路径,几乎不用改任何东西就能跑起来。然后按自己的节奏逐步把 VimScript 配置迁移到 Lua。 迁移路径通常是: 1. 把 `init.vim` 改名为 `init.lua`,内容不变 2. 逐个模块用 Lua 重写,通过 `require` 引入 3. 把 vim-plug 换成 lazy.nvim 4. 加入 LSP 和 Tree-sitter 配置 5. 替换旧插件为 Lua 原生替代品 整个过程可以持续几周甚至几个月,不需要一次性全换。 ## 性能对比 空配置下 Neovim 启动更快(12ms vs 28ms),但空闲内存占用 Vim 略低(12MB vs 18MB)。加载完整开发配置后,Neovim 的异步优势开始显现:大文件编辑、LSP 诊断、插件操作都不会阻塞 UI。在几千行代码的文件里,Tree-sitter 的高亮刷新是增量的,而 Vim 的正则高亮需要重新扫描整个文件。 GitHub 数据也能说明趋势:Neovim 88k+ stars,Vim 35k+ stars。2024-2025 年,Neovim 的提交量是 Vim 的 4-5 倍。这不是说 Vim 不好——它仍然是最稳定的编辑器之一,但社区活力确实在向 Neovim 倾斜。 ## 选型建议 **选 Vim 的情况:** - 你是资深 Vim 用户,现有配置已经很稳定,没有改造的动力 - 你经常在远程服务器上编辑文件,Vim 几乎到处都有预装 - 你的机器资源非常有限,每兆内存都要精打细算 - 你只需要一个可靠的文本编辑器,不需要 IDE 功能 **选 Neovim 的情况:** - 你是新用户,从零开始学,没有历史包袱 - 你想要 LSP、智能补全、代码导航等现代 IDE 功能 - 你对 Lua 配置感兴趣,或者想用 Neovim 搭建个人开发环境 - 你喜欢折腾编辑器,享受配置和优化的过程 **一个务实的策略:** 在服务器上继续用 Vim,在本地开发机上用 Neovim。两者键位操作完全一致,切换没有任何学习成本。Neovim 的配置也可以通过版本管理在多台机器间同步。 ## 写在最后 Vim 和 Neovim 的区别,本质上是两种开发哲学的区别。Vim 追求稳定和向后兼容,三十年来始终如一;Neovim 追求进化和现代化,愿意为了更好的架构砍掉历史包袱。两者不是替代关系——Vim 是 Neovim 的根,Neovim 是 Vim 的一种可能未来。 Bram Moolenaar 拒绝了那组补丁,但那个决定催生了编辑器领域最有活力的开源项目之一,这大概是 2014 年没有人预料到的。
服务端5月27日 15:06
Vim 的文本对象怎么用?## 为什么你一直在用 dw 而不是文本对象 很多 Vim 用户学了 `dw` 删除单词、`dd` 删除行,就觉得够用了。但当你需要删除一对括号里的内容、改写一个 HTML 标签内的文字、或者复制整个段落时,还在用 `dw` 一个个删就太慢了。文本对象(Text Object)是 Vim 里最被低估的特性——它让你不移动光标就能对"语义单元"做操作,而不只是对"字符位置"做操作。 ## i 和 a:文本对象的核心逻辑 所有文本对象都遵循一个模式:`操作符 + i/a + 对象标识`。 - `i` = inner,选"内部",不包含分隔符 - `a` = a/around,选"一个",包含分隔符(有时还包含周围空格) 举一个最直观的例子,假设光标在 `hello` 上: ``` say hello world ``` - `diw` → `say world`(只删单词,留两个空格) - `daw` → `say world`(删单词加后面空格,句子依然通顺) 记住这个区别,后面所有文本对象都是同一套逻辑。 ## 单词:iw / aw / iW / aW `iw` 选中当前单词(不含空格),`aw` 选中当前单词加相邻空格。 `w` 和 `W` 的区别在于分词规则: - `w` 按标点和空白分词,`hello-world` 是三个单词 - `W` 只按空白分词,`hello-world` 是一个整体 实际场景:代码里改变量名用 `ciw`,删除函数参数用 `daw`。 ```vim " 光标在 foo 上 const result = foo + bar ciw→ 输入 baz const result = baz + bar ``` ## 句子:is / as `is` 选中当前句子内部,`as` 选中句子含后面的空格。Vim 以 `.`、`!`、`?` 后跟空格或换行来识别句子边界。 日常编辑中用得不算多,但在写文档或 Markdown 时,`cis` 可以快速重写一句话,`yas` 可以复制整句去引用。 ## 段落:ip / ap `ip` 选中当前段落(不含前后空行),`ap` 选中段落加上下面的空行。 段落在 Vim 中的定义是:由空行分隔的连续非空行。这在编辑 Markdown、邮件、纯文本文档时特别好用: - `vip` 可视选中当前段落 - `yap` 复制整段 - `dap` 删除整段(包含段间空行,删除后不会多出空行) - `gqip` 对当前段落重新排版 ## 括号家族:i( / a( / i{ / a{ / i[ / a[ 这是写代码用得最多的一组。所有成对括号都支持 i/a 变体: | 文本对象 | 选中范围 | 典型用法 | |---------|---------|---------| | `i(` 或 `i)` | 括号内内容,不含括号 | `ci(` 改写函数参数 | | `a(` 或 `a)` | 包含括号本身 | `da(` 删除整个括号及内容 | | `i{` 或 `i}` | 花括号内内容 | `vi{` 选中函数体 | | `a{` 或 `a}` | 包含花括号本身 | `ya{` 复制整个块 | | `i[` 或 `i]` | 方括号内内容 | `ci[` 改写数组元素 | | `a[` 或 `a]` | 包含方括号本身 | `da[` 删除整个数组 | 注意:光标不需要在括号上,只要在括号包围的范围内即可。所以你可以站在函数体中间,直接 `ci(` 改写参数列表。 ## 引号家族:i" / a" / i' / a' / i` / a` 和括号类似,引号也有完整的 i/a 支持: | 文本对象 | 选中范围 | 典型用法 | |---------|---------|---------| | `i"` | 双引号内内容 | `ci"` 改写字符串值 | | `a"` | 包含双引号 | `da"` 删除整个字符串 | | `i'` | 单引号内内容 | `ci"` 改写字符 | | `a'` | 包含单引号 | `ya'` 复制含引号 | | `i`` | 反引号内内容 | `yi`` 复制模板字符串内容 | | `a`` | 包含反引号 | `da`` 删除模板字符串 | 前端开发时 `ci"` 改写属性值、`ci`` 改写模板字符串内容,是最高频的操作之一。 ## HTML 标签:it / at `it` 选中标签内部内容,`at` 选中包含标签本身。 ```html <p class="intro">Hello Vim</p> ``` - 光标在 `Hello` 上,`dit` → `<p class="intro"></p>`(删内容留空标签) - 光标在 `Hello` 上,`dat` → 整行消失(连标签一起删) - `cit` → 删内容并进入插入模式,直接输入新内容 这对编辑 HTML、XML、JSX/Vue 模板非常实用。配合 `vat` 先选中标签块再操作,比手动找开闭标签快得多。 ## 与操作符组合:真正的威力所在 文本对象单独用(如 `iw`)没有意义,它必须和操作符组合才能发挥作用。常见的操作符: - `d` delete:`diw` 删单词、`dib` 删括号内、`dit` 删标签内 - `c` change:`ciw` 改单词、`ci"` 改字符串、`cip` 改段落 - `y` yank:`yiw` 复制单词、`yi{` 复制花括号内容 - `v` visual:`viw` 选中单词、`vip` 选中段落 - `>` / `<` 缩进:`>ip` 缩进段落、`>a{` 缩进整个块 - `gU` / `gu` 大小写:`gUiw` 单词转大写 也可以加数字前缀:`2daw` 删除两个单词,`3ip` 选中三段。 一些高频组合速查: ``` ciw — 改写当前单词 ci" — 改写双引号内字符串 ci( — 改写括号内参数 cit — 改写 HTML 标签内容 diw — 删除当前单词 dib — 删除括号内内容(b 等同于 ( ) dat — 删除整个 HTML 标签 yiw — 复制当前单词 yi{ — 复制花括号内内容 vip — 选中当前段落 ``` ## 自定义文本对象与 vim-textobj-user Vim 内置的文本对象已经覆盖了大部分场景,但你可能会想定义自己的——比如选中函数名、选中注释块、选中 CamelCase 的某个部分。 [vim-textobj-user](https://github.com/kana/vim-textobj-user) 是 Kana Natsuno 写的插件,提供了一个声明式的 API 来创建自定义文本对象,不用手写复杂的 `onoremap` 映射。 最简单的用法——用正则定义一个文本对象: ```vim call textobj#user#plugin('line', { \ '-': { \ 'select-i': 'il', \ 'select-a': 'al', \ 'pattern': '.* ', \ }, \ }) ``` 这样就有了 `il`(行内)和 `al`(含换行)两个文本对象,可以用 `vil`、`dal` 等操作。 社区基于 vim-textobj-user 构建了大量插件,常用的有: - **vim-textobj-comment**:`ic`/`ac` 选中注释块 - **vim-textobj-function**:`if`/`af` 选中函数体 - **vim-textobj-entire**:`ie`/`ae` 选中整个文件 - **vim-textobj-indent**:`ii`/`ai` 选中同缩进层 - **vim-textobj-anyblock**:自动匹配最近的括号/引号对 如果你用 Neovim,还可以通过 Lua 和 treesitter 定义更智能的文本对象(如 `@function.inner`、`@class.outer`),这是另一个话题了。 ## 常见实战场景 **改函数参数**:光标在参数列表中间,`ci(` 清空参数重新输入。 **改字符串值**:光标在字符串上,`ci"` 删值进插入模式,不用移光标到引号里。 **删 HTML 标签但保留内容**:`dat` 会连标签一起删。如果只想删标签保留内容,可以 `vat` 选中后手动删标签行,或者用 vim-surround 插件的 `dst`(delete surrounding tag)。 **复制整个代码块**:`yi{` 复制花括号内所有内容,比 `V{` 手动选快得多。 **重排段落**:写 Markdown 时 `gqip` 对当前段落自动换行重排。 **批量缩进**:`>ap` 缩进当前段落,`>a{` 缩进整个代码块。 **在可视化模式下确认范围**:不确定文本对象会选中什么?先用 `vi(` 看看高亮范围,确认后再换成 `di(` 或 `ci(`。 ## 从今天开始用 如果你之前只用 `dw` 和 `dd`,从这几个操作开始: 1. `ciw` — 改写当前单词(日常最频繁) 2. `ci"` — 改写字符串内容 3. `ci(` — 改写括号内容 4. `dit` — 删除标签内容 5. `vip` — 选中当前段落 不用一次全记,先把 `ciw` 用熟,你会发现越来越不想回到逐字符操作了。文本对象的本质就是:告诉 Vim 你要操作"什么",而不是"从哪到哪"。
服务端5月27日 15:05
Vim 命令行模式有哪些常用命令?Vim 的命令行模式(Command-line Mode)是很多新手容易忽略的一层——按下 `:` 后底部弹出的那个输入框,藏着远比 `:wq` 更多的能力。从文件操作到批量替换、从窗口分割到执行 Shell 命令,命令行模式是 Vim 编辑效率的关键倍增器。 ## 如何进入命令行模式 在普通模式下,以下按键会进入命令行模式: - **`:`** — 输入 Ex 命令,这是最常用的入口 - **`/`** — 正向搜索 - **`?` — 反向搜索 - **`!`** — 在部分场景下直接执行外部命令(如 `:!ls`) 按 `Esc` 或 `Ctrl+c` 可退出命令行模式回到普通模式。 命令行底部会显示一个输入区域,你输入的内容称为"命令行",Vim 会解析并执行。 ## 文件操作命令 文件操作是命令行模式最基础也最常用的功能: | 命令 | 作用 | |------|------| | `:w` | 保存当前文件 | | `:w filename` | 另存为新文件 | | `:q` | 退出当前缓冲区 | | `:q!` | 强制退出,丢弃修改 | | `:wq` 或 `:x` | 保存并退出 | | `:e filename` | 打开文件编辑 | | `:e!` | 重新加载文件,丢弃当前修改 | | `:r filename` | 将文件内容插入到光标下方 | | `:sav filename` | 相当于 `:w filename` 后切换到新文件 | 几个容易混淆的区别: - `:x` 和 `:wq` 的差异:`:x` 仅在文件有修改时才写入,时间戳不会无谓更新。 - `:e!` 是"撤销一切"的最狠方式,比反复按 `u` 更彻底。 - `:r` 是 insert 的意思,在光标行下方插入文件内容,不是"打开"。 ## 行范围与地址 Ex 命令的强大在于可以指定行范围,对指定行批量操作: | 范围写法 | 含义 | |---------|------| | `:3` | 仅操作第 3 行 | | `:1,10` | 第 1 到第 10 行 | | `:1,$` | 第 1 行到文件末尾 | | `:%` | 等同 `1,$`,整个文件 | | `:.,+5` | 当前行到下面 5 行 | | `:'a,'b` | 标记 a 到标记 b 之间的行 | 常见组合: ```vim :1,10d " 删除第1到10行 :%d " 删除整个文件内容 :5,20m30 " 将5-20行移动到第30行之后 :1,5t10 " 将1-5行复制到第10行之后 :%normal A; " 在每一行末尾加分号(执行普通模式命令) ``` `:%` 是最高频的范围写法,尤其在配合替换命令时几乎必用。 ## 搜索与替换 搜索替换是命令行模式的核心能力,也是面试常考点。 ### 替换命令 `:s` 基本语法:`:[范围]s/模式/替换/标志` ```vim :s/foo/bar/ " 当前行,替换第一个匹配 :s/foo/bar/g " 当前行,替换所有匹配 :%s/foo/bar/g " 全文替换 :%s/foo/bar/gc " 全文替换,每次确认 :5,10s/foo/bar/g " 第5-10行替换 ``` 常用标志: - `g` — 替换行内所有匹配(不加则只替换第一个) - `c` — 每次替换前确认 - `i` — 忽略大小写 - `I` — 区分大小写 分隔符不限于 `/`,如果模式本身包含 `/`,可以换用其他字符: ```vim :%s#/usr/local#/opt/homebrew#g " 用 # 当分隔符 ``` ### 全局命令 `:g` 和 `:v` `:g` 对匹配行执行命令,`:v` 对不匹配行执行命令: ```vim :g/pattern/d " 删除所有包含 pattern 的行 :g!/pattern/d " 删除所有不包含 pattern 的行 :v/pattern/d " 同上,:v 等价 :g! :g/^$/d " 删除所有空行 :g/pattern/s/old/new/g " 对匹配行执行替换 :g/pattern/normal @q " 对匹配行执行宏 ``` `:g` 的语法是 `:[范围]g/模式/命令`,它是 Vim 里最接近"脚本"能力的东西——批量操作利器。 ## 窗口命令 Vim 支持多窗口编辑,这些命令在命令行模式中输入: | 命令 | 作用 | |------|------| | `:split` 或 `:sp` | 水平分割窗口 | | `:vsplit` 或 `:vsp` | 垂直分割窗口 | | `:sp filename` | 水平分割并打开文件 | | `:vsp filename` | 垂直分割并打开文件 | | `:close` | 关闭当前窗口 | | `:only` | 关闭其他所有窗口 | | `:resize +5` | 当前窗口高度增加5行 | | `:resize -5` | 当前窗口高度减少5行 | | `:vertical resize 80` | 当前窗口宽度设为80列 | 窗口间移动用 `Ctrl+w` 系列快捷键(`Ctrl+w h/j/k/l`),不属于命令行命令,但经常配合使用。 ## 缓冲区管理 Vim 的缓冲区(Buffer)是比窗口更底层的概念——你可以打开多个文件但不一定要显示它们: | 命令 | 作用 | |------|------| | `:ls` | 列出所有缓冲区 | | `:b N` | 切换到第 N 号缓冲区 | | `:bn` | 下一个缓冲区 | | `:bp` | 上一个缓冲区 | | `:bd` | 关闭当前缓冲区 | | `:bd N` | 关闭第 N 号缓冲区 | | `:bufdo %s/old/new/g` | 对所有缓冲区执行替换 | `:ls` 的输出中,`%` 表示当前缓冲区,`#` 表示轮换缓冲区(按 `Ctrl+^` 可快速切换),`a` 表示已激活。 实际开发中,常用 `:bn` 和 `:bp` 在多个文件间快速跳转,比反复 `:e` 高效。 ## 执行外部命令 Vim 可以在命令行模式中直接调用 Shell: ```vim :!ls " 查看目录列表 :!python3 % " 用 Python 运行当前文件(% 代表当前文件名) :!make " 执行 make :shell " 进入一个子 Shell(exit 返回 Vim) ``` 将外部命令输出插入当前文件: ```vim :read !date " 在光标下方插入当前日期 :r !ls -la " 在光标下方插入目录列表 :5read !whoami " 在第5行后插入命令输出 ``` 注意 `:!cmd` 和 `:read !cmd` 的区别:前者只显示结果,后者把结果插入文件。 还有一个实用技巧——把当前缓冲区的部分内容作为外部命令的输入: ```vim :1,10!sort " 将1-10行通过 sort 命令排序后替换原内容 :%!jq . " 用 jq 格式化整个 JSON 文件 ``` 这种"过滤"用法在处理日志、格式化代码时非常方便。 ## 命令行补全 命令行模式下有两个关键补全快捷键: - **`Tab`** — 补全命令名、文件名、选项名等 - **`Ctrl+d`** — 列出所有可能的补全候选项 ```vim :e<Tab> " 补全以 e 开头的命令 :e /etc/pa<Ctrl+d> " 列出 /etc 下以 pa 开头的文件 :set inc<Tab> " 补全选项名(如 incsearch) ``` Vim 还支持自定义补全来源,`:set wildmenu` 开启后,底部会显示一个可导航的补全菜单,配合方向键选择更直观。 ## 命令历史 命令行模式维护了独立的历史记录: - **`q:`** — 打开命令行历史窗口,可浏览和编辑历史命令 - **`q/`** — 打开搜索历史窗口 - **`q?`** — 打开反向搜索历史窗口 在历史窗口中,可以用 `j/k` 浏览,`Enter` 执行选中的命令,`Ctrl+c` 退出。 上下方向键也可以在命令行中逐条回溯历史,与 Shell 的体验一致。 建议配合 `:set history=200` 增大历史记录条数(默认 50),方便回溯更早的命令。 ## 映射与缩写 命令行模式也用于定义快捷映射和缩写: ### 映射 `:map` 系列 ```vim :map <F5> :w<CR> " 普通/可视/选择/操作符等待模式映射 :nmap <F5> :w<CR> " 仅普通模式 :imap jj <Esc> " 仅插入模式 :vmap <C-c> y " 仅可视模式 :nnoremap <F5> :w<CR> " 非递归普通模式映射(推荐) ``` 实际开发中,**始终优先使用 `noremap` 系列**(`nnoremap`/`inoremap`/`vnoremap`),避免递归映射导致的问题。 ### 缩写 `:abbr` 系列 ```vim :iab adn and " 插入模式将 adn 自动展开为 and :iab @@ user@example.com " 快速输入邮箱 :ab mainfn int main() " 缩写展开 ``` 缩写在输入空格或回车时触发,适合常用代码片段或容易拼错的单词。 --- Vim 命令行模式本质上是 ex 编辑器的接口——ex 是 Vi 的行编辑器前身,所有 `:` 命令都是 ex 命令。理解这一点后,你会发现 Vim 的命令行模式并不是一个"输入框"那么简单,而是一个完整的行编辑器,可以精确地对任意行范围执行操作。掌握行范围、替换和 `:g` 命令,命令行模式就能从"只会 :wq"升级为真正的文本处理工具。
服务端5月27日 14:58
Vim 的寄存器到底有几种,各自用在什么场景?## 为什么你的 Vim 粘贴总是不对 你一定遇到过这种情况:复制了一行代码,删掉另一行,再粘贴时发现粘贴的是刚删掉的内容,而不是你复制的那行。这不是 bug,这是 Vim 寄存器机制在起作用——大多数操作都默认写入同一个无名寄存器,后进来的把前面的覆盖了。 Vim 并不是只有一个剪贴板,它有十几种寄存器,每种都有明确的用途。搞清楚它们,复制粘贴不再踩坑,还能用寄存器做宏录制、表达式计算、跨程序复制等高级操作。 ## 无名寄存器("")——默认的垃圾桶 每次执行 `yank`、`delete`、`change` 等操作,内容都会自动写入无名寄存器 `""`。普通模式下按 `p` 粘贴,用的就是它。 问题在于,`dd` 删除一行和 `yy` 复制一行都会覆盖 `""`。所以你复制之后做了一次删除,粘贴出来的就是删除的内容。 这不代表原来的内容丢了——它还在数字寄存器 `"0` 里。所以下次遇到"粘贴不对",先试 `"0p`,大概率就是你想要的内容。 ## 命名寄存器("a–"z)——手动管理的 26 个抽屉 命名寄存器是最常用的一类,用法简单:操作前加 `"寄存器名`。 ```vim "ayy " 将当前行复制到寄存器 a "ap " 粘贴寄存器 a 的内容 "bdw " 删除一个单词并存入寄存器 b ``` 这样你可以在 a 里存一段代码,b 里存另一段,随时按 `"ap` 和 `"bp` 取出来,互不干扰。 ### 大写字母是追加,不是覆盖 如果寄存器 a 里已经有内容,`"ayy` 会覆盖它。但用大写 `"Ayy` 则是追加: ```vim "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 会在命令行提示你输入一个表达式,计算结果直接插入光标处。 ```vim " 插入模式下: Ctrl+r =3600*24↵ " 插入 86400 Ctrl=r =strftime('%Y-%m-%d')↵ " 插入当前日期 ``` 也可以在命令里引用变量或函数返回值,适合需要动态插入内容的场景。 ## 系统剪贴板("+ 和 "*)——和外部程序互通 Vim 默认不与系统剪贴板交互,需要通过 `"+` 或 `"*` 寄存器: - `"+yy`:复制当前行到系统剪贴板 - `"+p`:从系统剪贴板粘贴 `"+` 和 `"*` 在 Windows 和 macOS 上行为一致,都指向系统剪贴板。在 Linux 上有区别:`"+` 是 CLIPBOARD(Ctrl+C/V),`"*` 是 PRIMARY(鼠标选中即复制,中键粘贴)。 如果你希望每次 yank/paste 自动使用系统剪贴板,可以设置: ```vim set clipboard=unnamedplus ``` 这样普通 `yy` 和 `p` 就直接操作系统剪贴板了。 ## :reg——查看所有寄存器内容 忘了某个寄存器里存了什么?用 `:reg` 或 `:registers` 查看全部,也可以指定只看某几个: ```vim :reg " 查看所有非空寄存器 :reg a b 0 " 只看寄存器 a、b、0 :reg / : " 查看搜索模式和上次 Ex 命令 ``` 输出格式是寄存器名 + 内容,内容中的换行用 `^J` 表示。调试宏或确认寄存器状态时经常用到。 ## 宏与寄存器——本质上是同一套机制 Vim 的宏录制就是把按键序列存进命名寄存器。`qa` 开始录制到寄存器 a,再按 `q` 停止,`@a` 回放。 这意味着: 1. 录制的宏可以用 `:reg a` 查看,内容就是一串按键字符 2. 你可以把宏内容粘贴出来编辑,改好再 yank 回去——修改宏不需要重新录制 3. 用大写追加可以往宏里追加指令:`qA` 追加录制到 a 宏末尾 编辑宏的流程: ```vim :reg a " 先看看宏 a 里的内容 "ap " 把宏内容粘贴到缓冲区 " 编辑这一行按键序列 "ayy " 重新 yank 回寄存器 a ``` 这种可编辑性是 Vim 宏区别于简单"录制回放"的关键——出错了不用重来,改一行就行。 ## 把寄存器用起来 寄存器不是 Vim 里"知道就好"的冷知识,它直接影响日常编辑效率。几个建议: - 复制重要内容时指定命名寄存器(`"ayy`),避免被后续删除覆盖 - 需要干净删除时用黑洞寄存器(`"_dd`),保持寄存器历史干净 - 复制粘贴跨程序时显式用 `"+y` / `"+p`,不要依赖自动剪贴板设置 - 录制宏后用 `:reg` 检查内容,复杂宏直接编辑比重新录制更高效 Vim 的寄存器体系看起来种类多,但核心逻辑就是一条:每次操作前加 `"寄存器名`,就是指定目标寄存器;不加,就是无名寄存器。记住这个规律,其他的都是在此基础上的分类和特例。
服务端5月27日 14:52
Vim 的标签导航怎么用?从 ctags 到 LSP 的完整跳转方案## 为什么需要标签导航 阅读源码时,你会在函数调用处和定义处之间反复切换。如果没有标签系统,只能靠 `grep` 或 `/:function_name` 搜索,效率很低。Vim 的标签导航机制让你在光标处一键跳转到定义,再一键返回,是代码阅读的核心工作流。 ## 生成 tags 文件:ctags 标签导航的前提是有 tags 文件。ctags 扫描源码,把函数、类、变量的定义位置记录到一个索引文件中。 安装 Universal Ctags(Exuberant Ctags 的活跃 fork): ```bash # macOS brew install universal-ctags # Ubuntu/Debian sudo apt install universal-ctags ``` 在项目根目录生成 tags 文件: ```bash ctags -R . ``` `-R` 表示递归扫描子目录,生成的 `tags` 文件存放在当前目录。 如果你的项目有 `node_modules` 或 `build` 目录,建议排除: ```bash ctags -R --exclude=node_modules --exclude=build . ``` C/C++ 项目需要更详细的标签信息,可以加参数: ```bash 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 精确跳转 当光标不在目标标识符上时,可以直接用命令跳转: ```vim :tag main ``` 跳转到 `main` 的定义。支持 Tab 补全,输入 `:tag m<Tab>` 会列出所有 `m` 开头的标签。 如果一个标签有多个定义(比如不同文件中同名函数),`:tag` 只跳到第一个。这时用 `:tselect` 列出所有匹配: ```vim :tselect parse ``` Vim 会显示一个选择列表,输入编号即可跳转。 `:tjump` 是更智能的版本:只有一个匹配时直接跳转,多个匹配时弹出选择列表。相当于 `:tselect` 和 `:tag` 的合体。 在匹配项之间浏览: - `:tnext` — 下一个匹配 - `:tprev` — 上一个匹配 - `:tfirst` — 第一个匹配 - `:tlast` — 最后一个匹配 ## g] :预览式选择跳转 `g ]` 把光标下标识符的所有匹配列出来让你选择,和 `:tjump` 效果类似,但不需要输入命令。日常使用中,`g ]` 比 `Ctrl-]` 更稳妥——遇到多个定义时不会跳错位置。 另外几个预览相关的命令: - `Ctrl-W }` — 在预览窗口中打开定义,不离开当前位置 - `:ptag func_name` — 在预览窗口打开指定标签 - `:pclose` — 关闭预览窗口 ## 标签栈::tags 查看跳转历史 每次 `Ctrl-]` 跳转都会压入标签栈。查看栈内容: ```vim :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`,即在当前文件目录和工作目录查找。 常见配置: ```vim " 向上级目录查找 tags 文件,直到找到为止 set tags=./tags;,tags; " 或者指定固定路径 set tags+=/path/to/project/tags ``` `./tags;` 中的分号表示向上递归查找——Vim 会从当前文件所在目录开始,逐级向上找 `tags` 文件,直到根目录。这解决了在子目录中打开文件时找不到项目根目录 tags 文件的问题。 如果项目有多个 tags 文件,用 `+=` 追加: ```vim set tags+=/path/to/external-lib/tags ``` ## 多项目的 tags 管理 当你同时在多个项目间切换时,每个项目应该有自己的 tags 文件。几个实践建议: 1. **把 tags 文件加到 .gitignore**。tags 文件是本地生成的,不应该提交到仓库。 2. **用 `set autochdir`**。Vim 自动把工作目录切换到当前文件所在目录,配合 `./tags;` 的递归查找,基本可以覆盖大部分场景: ```vim set autochdir set tags=./tags;,tags; ``` 3. **大项目按模块拆分 tags**。在子目录分别生成 tags 文件,Vim 会自动合并所有匹配的标签。 4. **使用 $PROJECT_HOME 环境变量**。在 vimrc 中动态设置 tags 路径: ```vim if $PROJECT_HOME != '' set tags+=$PROJECT_HOME/tags endif ``` ## cscope:标签之外的代码交叉引用 ctags 只能跳转到定义,无法查找"谁调用了这个函数"。cscope 补充了这个能力。 生成 cscope 数据库: ```bash # 在项目根目录 find . -name "*.c" -o -name "*.h" > cscope.files cscope -b ``` `-b` 表示只构建数据库,不进入交互界面。生成的 `cscope.out` 文件就是数据库。 在 Vim 中连接数据库: ```vim :cs add cscope.out ``` 验证连接: ```vim :cs show ``` cscope 的查询类型: | 命令 | 缩写 | 含义 | |------|------|------| | `: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 中设置: ```vim set csto=1 set cst ``` 这样 `Ctrl-]` 会优先查 cscope,再查 tags。 ## gutentags:自动生成 tags 文件 手动跑 `ctags -R` 很容易忘。vim-gutentags 插件在后台自动管理 tags 文件的生成和更新。 安装(以 vim-plug 为例): ```vim Plug 'ludovicchabant/vim-gutentags' ``` 基本配置: ```vim " 指定 tags 文件存放目录(避免污染项目根目录) let g:gutentags_cache_dir = expand('~/.cache/vim/ctags/') " 确保缓存目录存在 if !isdirectory(g:gutentags_cache_dir) silent! mkdir -p g:gutentags_cache_dir endif " 项目根目录标记 let g:gutentags_project_root = ['.git', '.hg', '.root'] ``` gutentags 的行为: - 打开项目中的文件时,自动在后台生成 tags - 保存文件时,增量更新 tags(不是全量重建) - 通过 `.git` 等标记识别项目根目录 - 不依赖 Python 或 Ruby,纯 Vim 脚本 + ctags 状态栏显示生成进度: ```vim set statusline+=%{gutentags#statusline()} ``` 生成 tags 时状态栏会显示 `TAGS`,完成后自动消失。 ## LSP 方案:coc.nvim 和 nvim-lspconfig LSP(Language Server Protocol)提供了比 ctags 更精确的代码导航。LSP 的跳转基于语义分析,能区分同名函数的重载,能跳转到依赖库中的定义,还能查找所有引用。 **coc.nvim**(Vim 8+ / Neovim): ```vim Plug 'neoclide/coc.nvim', {'branch': 'release'} ``` 安装语言服务器后,用 `gd` 跳转到定义,`gr` 查找引用,`K` 查看文档。coc.nvim 还会把 LSP 结果注册为 tags,所以 `Ctrl-]` 也能用。 **nvim-lspconfig**(Neovim 0.5+): ```lua local lspconfig = require('lspconfig') lspconfig.pyright.setup{} -- Python lspconfig.ts_ls.setup{} -- TypeScript lspconfig.clangd.setup{} -- C/C++ ``` 快捷键映射: ```lua 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 配置参考 ```vim " tags 文件查找策略 set tags=./tags;,tags; " cscope 优先于 tags set csto=1 set 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 带来语义精度。根据项目语言和规模选择合适的组合,比追单一方案更实际。
服务端5月27日 14:52
VimScript 脚本编程怎么写?从变量作用域到插件结构全讲清楚## 为什么要学 VimScript Vim 的真正威力不在于快捷键多,而在于你可以用脚本把编辑器改造成自己想要的样子。自动格式化、批量重命名、项目专属配置——这些全靠 VimScript 驱动。即使你现在用 Neovim + Lua,读懂已有插件的 VimScript 源码依然是刚需。 ## 变量与作用域 VimScript 的变量用 `let` 声明,用 `unlet` 删除。关键在于作用域前缀——每个前缀决定了变量的可见范围和生命周期: ```vim 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` 访问环境变量。 ```vim echo &tabstop " 读取选项 let &tabstop = 4 " 设置选项 echo @a " 读取寄存器 a 的内容 echo $HOME " 读取环境变量 ``` ## 字符串操作 VimScript 的字符串有两种引号,行为不同: ```vim let s1 = "hello world" " 双引号:支持 \ 等转义 let s2 = 'hello world' " 单引号:原样输出, 就是两个字符 ``` 常用字符串函数: ```vim echo strlen("hello") " => 5 echo strpart("hello", 1, 3) " => ell(从索引1取3个字符) echo substitute("hello", "l", "L", "g") " => heLLo echo tolower("Hello") " => hello echo toupper("hello") " => HELLO echo stridx("hello", "ll") " => 2(查找子串位置) echo split("a,b,c", ",") " => ['a', 'b', 'c'] echo join(['a', 'b'], "-") " => a-b ``` 拼接字符串推荐用 `.` 运算符: ```vim let msg = "file: " . expand("%") . " line: " . line(".") ``` ## 列表与字典 列表(List)就是数组,字典(Dict)就是哈希表: ```vim " 列表 let fruits = ["apple", "banana", "cherry"] echo fruits[0] " => apple echo 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 " => vim echo user["version"] " => 9 let user.lang = "VimScript" " 添加键 call remove(user, "lang") " 删除键 echo keys(user) " => ['name', 'version'] echo values(user) " => ['vim', 9] ``` ## 控制流 ### if / else / endif ```vim if &filetype ==# "python" echo "Python file" elseif &filetype ==# "javascript" echo "JS file" else echo "Other file" endif ``` 注意 `==#` 是大小写敏感比较,`==?` 是忽略大小写。裸写 `==` 受用户 `ignorecase` 设置影响,不推荐。 ### for 循环 ```vim for item in ["a", "b", "c"] echo item endfor for i in range(1, 10) echo i endfor ``` ### while 循环 ```vim let i = 0 while i < 5 echo i let i += 1 endwhile ``` VimScript 没有 break/continue 的等价物(Vim9script 有了),传统做法是用条件变量控制循环。 ## 函数定义 ```vim function! s:Greet(name) echo "Hello, " . a:name return a:name endfunction ``` 关键规则: - 函数名如果不加 `s:` 前缀,必须以大写字母开头。 - `function!` 加 `!` 表示如果函数已存在则覆盖,插件开发必加。 - 函数参数用 `a:` 前缀访问,如 `a:name`、`a:1`(可变参数)。 - 函数默认返回 0,除非显式 `return`。 - `abort` 关键字让函数在出错时立即中止,而不是继续执行。 ```vim function! s:Min(num1, num2) abort return a:num1 < a:num2 ? a:num1 : a:num2 endfunction ``` 可变参数用 `...`: ```vim function! s:Varargs(name, ...) echo a:name echo a:0 " 可变参数个数 echo a:1 " 第一个可变参数 echo a:000 " 可变参数列表 endfunction ``` ## autocmd 编程 autocmd 是 Vim 事件驱动的核心——在特定事件发生时自动执行命令: ```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 体尽量短,复杂逻辑抽成函数调用。 ```vim function! s:AutoFormatPython() " 复杂格式化逻辑 silent! %!autopep8 - endfunction ``` ## 自定义命令:command 与 <f-args>、<range> 用 `:command` 定义用户命令,比函数调用方便得多: ```vim command! -nargs=1 -complete=file MyEdit :edit <args> ``` `<f-args>` 将命令参数拆分为函数参数列表,是最常用的参数传递方式: ```vim command! -nargs=* Grep call s:Grep(<f-args>) function! s:Grep(...) abort let pattern = join(a:000, ' ') silent! grep! pattern cwindow endfunction ``` `<range>` 让命令支持行范围: ```vim 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`,只有第一次调用时才会加载: ```vim " 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 结构,先让东西跑起来,再重构不迟。
服务端5月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; ``` 操作步骤: 1. 把光标移到第一行行首,按 `Ctrl-v` 进入块可视模式。 2. 按 `jj`(或 `2j`)向下选中三行的第一个字符。 3. 按 `I`(大写)在块左侧插入,输入 `// `,然后按 `Esc`。 三行会同时变成 `// int a = 1;`、`// int b = 2;`、`// int c = 3;`。 ### 实战:批量修改对齐的值 假设你有一组配置项,想把 `= true` 改成 `= false`: ``` debug = true verbose = true log = true ``` 操作步骤: 1. 光标移到第一行的 `t` 上,按 `Ctrl-v` 进入块选择。 2. 按 `2j` 向下选中三行,再按 `e` 向右选中 `true` 整个单词。 3. 按 `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`,以及与 `.` 命令和宏的组合,你几乎不需要离开键盘就能完成任何批量编辑。从今天开始,遇到需要选中多行或多列的场景,强迫自己用可视模式而不是鼠标——一周后你会发现编辑速度上了一个台阶。
服务端5月27日 14:50
Vim 搜索和替换有哪些必须掌握的高级技巧?Vim 的搜索能力远不止输入关键词然后按回车。正则元字符模式、搜索高亮策略、替换确认机制、跨文件搜索——这些才是真正拉开效率差距的地方。 ## 用 / 和 ? 精准定位 `/` 向下搜索,`?` 向上搜索,这是最基本的区分。按 `n` 跳到下一个匹配,`N` 跳到上一个(在 `?` 搜索时方向反转)。 搜索当前光标下的单词有两个快捷操作:`*` 向下搜索整个单词,`#` 向上搜索。Vim 会自动给关键词加上 `\<` 和 `\>` 边界,不会匹配到包含该单词的更长字符串。 如果只想搜索光标下单词的部分匹配(不要求单词边界),用 `g*` 和 `g#`。 ## 四种正则模式:\v \V \m \M Vim 的正则语法有四种模式,这是很多人忽略的关键机制: - **\m(magic)**:默认模式。`.`、`*`、`^`、`$` 等有特殊含义,但 `+`、`?`、`(`、`)`、`{`、`}`、`|` 需要反斜杠转义才生效。写分组是 `\(` 和 `\)`,或逻辑是 `\|`。 - **\v(very magic)**:所有元字符都直接生效,不需要反斜杠。写分组直接用 `()`,或逻辑直接用 `|`,量词直接用 `+` 和 `?`。这是最接近 Perl 正则的模式,写复杂表达式时强烈推荐。 - **\M(nomagic)**:只有 `^` 和 `$` 有特殊含义,其余字符全部当作字面量。 - **\V(very nomagic)**:只有反斜杠本身有特殊含义,搜索的就是字面文本。当你需要搜索包含大量特殊字符的字符串时,用这个模式最省心。 实际用法是在搜索模式开头加上模式修饰符:`/\v\d+\.\d+` 匹配浮点数,`/\Vfoo.bar` 搜索字面的 "foo.bar"。 ## 搜索高亮:hlsearch 和 incsearch 两个选项控制搜索时的视觉反馈: ```vim set hlsearch " 高亮所有匹配项 set incsearch " 输入时实时跳转到第一个匹配 ``` `hlsearch` 打开后,最后一次搜索的所有匹配都会高亮显示。缺点是高亮会一直留在屏幕上,用 `:nohlsearch`(简写 `:noh`)临时关闭。很多人在 vimrc 中映射一个快捷键: ```vim nnoremap <Esc><Esc> :nohlsearch<CR> ``` 连按两次 Esc 清除高亮。 `incsearch` 让你在输入搜索模式的过程中就能看到当前匹配位置,不用等按回车。这对复杂正则特别有用,输入到一半就能判断模式是否正确。 ## 替换命令 :s 和 :%s 基本语法: ```vim :s/old/new/ " 当前行替换第一个匹配 :s/old/new/g " 当前行替换所有匹配 :%s/old/new/g " 全文替换 :5,20s/old/new/g " 第5行到第20行替换 ``` 范围除了行号,还支持这些写法: - `.,$s` — 从当前行到文件末尾 - `1,.s` — 从第一行到当前行 - `'<,'>s` — 可视模式下选中区域(按 `:` 自动填充) ## 确认替换:c 标志 替换最怕改错地方。加 `c` 标志让每次替换前都确认: ```vim :%s/old/new/gc ``` Vim 会逐个高亮匹配,提示你选择: - `y` — 替换当前 - `n` — 跳过当前 - `a` — 替换剩余全部 - `q` — 退出替换 - `l` — 替换当前后退出 这个交互流程让大规模替换变得安全可控。 ## 正则捕获与反向引用 用 `\( \)` 分组(magic 模式)或 `()` 分组(very magic 模式),替换时用 `\1`、`\2` 引用捕获内容: ```vim " 将 "lastName, firstName" 改为 "firstName lastName" :%s/\v(\w+),\s*(\w+)/\2 \1/g ``` 这里 `\1` 是第一个括号匹配的内容(姓),`\2` 是第二个(名)。 另一个实用场景——给函数调用加引号: ```vim :%s/\vfunc\(([^)]+)\)/func("\1")/g ``` ## 大小写控制:\c 和 \C 在搜索模式中直接加修饰符比改设置更灵活: - `/pattern\c` — 忽略大小写搜索 - `/pattern\C` — 强制区分大小写 也可以配合 `ignorecase` 和 `smartcase` 选项: ```vim set ignorecase " 默认忽略大小写 set smartcase " 搜索模式中包含大写字母时自动区分大小写 ``` `smartcase` 的逻辑是:如果你特意输入了大写字母,说明你要精确匹配,否则就忽略大小写。这个组合比单独用 `ignorecase` 更智能。 ## 搜索历史的复用 Vim 保存搜索历史,在搜索模式下按上下方向键可以回溯之前的搜索模式。更高效的做法是先输入部分内容再按上下键,Vim 只显示以该内容开头的历史条目。 命令行窗口是另一个利器:按 `q/` 打开搜索历史窗口,`q:` 打开命令历史窗口。在这个窗口中可以用 Vim 的全部编辑能力修改历史命令,然后按回车执行。 ## 跨文件搜索::vimgrep 当搜索范围需要超出当前文件: ```vim :vimgrep /pattern/ **/*.py ``` 这会递归搜索当前目录下所有 `.py` 文件。搜索结果进入 quickfix 列表。 常用操作: - `:copen` — 打开 quickfix 窗口 - `:cnext` / `:cprev` — 在匹配项之间跳转 - `:cfirst` / `:clast` — 跳到第一个/最后一个匹配 - `:cclose` — 关闭 quickfix 窗口 也可以指定文件范围: ```vim :vimgrep /TODO/ src/**/*.js :vimgrep /FIXME/ *.c ``` quickfix 列表不只服务于 vimgrep,`:grep`、编译错误等都会用到同一套导航命令,值得记住。 ## 从搜索到替换的实战流程 一个高效的工作流是先搜索验证,再替换执行: 1. 用 `/pattern` 搜索,`n` 逐个检查匹配是否符合预期 2. 确认无误后直接执行 `:%s//replacement/g`——注意搜索模式留空,Vim 自动使用最后一次搜索的模式 3. 不确定时加 `c` 标志逐步确认 这个流程把搜索和替换打通,避免了在替换命令中重新输入复杂正则的麻烦。 Vim 的搜索体系从单文件关键词到跨文件正则,覆盖了文本定位的完整链路。掌握这些技巧后,日常编辑中的查找替换操作会从反复试错变成一次到位。
服务端5月27日 14:50
Vim 的折叠功能怎么用?打开一个上千行的配置文件或源码时,满屏文本让人无从下手。Vim 的折叠功能可以把逻辑块收成一行,让代码结构一目了然——但很多人只停留在 `zc`/`zo` 的程度,不知道 Vim 其实提供了六种折叠方式,各有适用场景。 ## 六种折叠方式 Vim 通过 `foldmethod` 选项决定折叠规则,一共有六种: **manual** — 手动折叠。用 `zf` 配合移动命令圈选范围来创建折叠,最灵活但退出后丢失(除非持久化)。适合临时阅读不熟悉的文件。 **indent** — 按缩进折叠。缩进越深,折叠层级越高,Vim 用 `shiftwidth` 的值把缩进空格数折算成折叠级别。Python、YAML 这类缩进敏感的语言用这个最省心: ```vim set foldmethod=indent ``` **expr** — 表达式折叠。通过 `foldexpr` 指定一个 Vim 表达式,对每一行求值返回折叠级别。灵活性最强,写法也最复杂。一个常见用法——按空行分段落折叠: ```vim set foldmethod=expr set foldexpr=getline(v:lnum)=~'^\s*$'?'<1':1 ``` 返回值的含义:正整数表示折叠级别,`>N` 表示 N 级折叠从此行开始,`<N` 表示 N 级折叠到此行结束,`=` 继承上一行级别,`a1`/`s1` 分别在上一行基础上加/减一级。为了性能,建议把逻辑封装成函数: ```vim set foldexpr=MyFoldLevel() function MyFoldLevel() let line = getline(v:lnum) if line =~# '^\s*$' return '<1' else return 1 endif endfunction ``` **syntax** — 语法折叠。依赖语法高亮文件中定义的 `fold` 区域,不需要额外配置,前提是当前文件类型的语法文件支持折叠。大部分主流语言开箱即用: ```vim set foldmethod=syntax ``` **diff** — 差异折叠。只在 diff 模式下生效,自动把未修改的连续行折叠起来,只展示差异部分。用 `vimdiff` 比较文件时自动启用,无需手动设置。 **marker** — 标记折叠。通过文本中的标记符号定义折叠边界,默认是 `{{{` 和 `}}}`。标记会写入文件内容,所以退出后依然存在,且支持撤销/重做: ```vim 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` 控制,默认没有上限: ```vim set foldnestmax=3 ``` 超过最大嵌套层数的折叠会被合并到允许的最深层级。对于结构复杂的代码,适当限制嵌套层级能避免过度折叠导致结构不清晰。 ## 折叠相关的显示选项 **foldcolumn** — 在窗口左侧显示折叠指示列。设置为 0 隐藏,最大 12,建议设为 2 或 3: ```vim set foldcolumn=2 ``` 折叠列里用 `-` 表示折叠打开的行,`+` 表示折叠关闭的位置,`|` 表示折叠层级的延续。有了折叠列,鼠标点击也可以操作折叠。 **foldlevel** — 控制初始折叠深度。设为 0 打开文件时全部折叠,设为 99 等于全部展开。设一个中间值,打开文件就能看到结构骨架: ```vim set foldlevel=2 ``` 配合 `foldlevelstart` 可以单独控制打开文件时的初始折叠级别而不影响后续操作。 **foldminlines** — 折叠最少显示行数。如果一个折叠内容不足指定行数,就不允许折叠它。避免把两三行的小块也折起来: ```vim set foldminlines=5 ``` ## 折叠持久化 manual 模式的折叠退出 Vim 就没了。要持久化,在 vimrc 中加: ```vim augroup FoldPersist autocmd! autocmd BufWinLeave * mkview autocmd BufWinEnter * silent loadview augroup END ``` `mkview` 保存当前窗口的折叠状态、光标位置等信息,`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 折叠配置 把上面这些选项组合起来,一个实用折中的配置: ```vim " 默认使用缩进折叠 set foldmethod=indent set foldlevel=2 set foldcolumn=2 set foldminlines=3 set foldnestmax=6 " 按文件类型覆盖 autocmd FileType vim setlocal foldmethod=marker autocmd FileType python setlocal foldmethod=indent autocmd FileType json,yaml setlocal foldmethod=syntax " 持久化 manual 折叠 augroup FoldPersist autocmd! autocmd BufWinLeave * mkview autocmd BufWinEnter * silent loadview augroup END ``` 折叠不是花哨的功能——它解决的是真实问题:在有限屏幕里看清代码结构。花十分钟配好折叠策略,之后每次打开文件都能直接看到骨架而不是一片文字墙。