用vscode开发 ios/macos App
如何脱离 xcode 开发 iOS 是很多人在探索的方案。毕竟 xcode 编辑体验实在是太差了。 这里说的是脱离 xcode,而不是脱离 macOS。 SweetPad 这是今天要安利的工具 SweetPad。SweetPad 是 vscode 上的一个插件,插件可以实现在 vscode 中进行自动补全、调试、编译和运行、格式化、测试等功能。常见的开发场景中基本可以脱离 xcode 来使用。
youngxhui blog,🌟追求成为一名 indie hack,🚀创造属于自己的编码魔法。专注技术,喜欢探索新鲜事物,对于 golang, neovim 等会做一些分享。
如何脱离 xcode 开发 iOS 是很多人在探索的方案。毕竟 xcode 编辑体验实在是太差了。 这里说的是脱离 xcode,而不是脱离 macOS。 SweetPad 这是今天要安利的工具 SweetPad。SweetPad 是 vscode 上的一个插件,插件可以实现在 vscode 中进行自动补全、调试、编译和运行、格式化、测试等功能。常见的开发场景中基本可以脱离 xcode 来使用。
在使用 Neovim 之前,我也花费了大量的时间进行配置自己的 Neovim 体系,无论是插件,快捷键还是 UI,都花费了大量的心思。 但是维护一套自己的配置说实话很费时间,例如插件的更新,版本升级等。 LazyVim 是一款已经 集成了大量插件的 Neovim 软件。 基本做到了开箱即用,方便快捷,大量的默认配置减少了上手时间,唯一的缺点可能就是快捷键不是自己熟悉的那套,需要修改或者适应。 安装 LazyVim 的安装其实很简单,首先打开官方的启动模板 LazyVim/starter。 点击右上角 Use this template ,将模板 fork 成为自己的仓库。 接下来就是备份本地原有的配置,将自己的仓库 clone 下来。 简单来说就是一个命令进行备份 另外一个命令进行 clone。 这部分可以直接参考官方网站对于安装的介绍文档。 安装完成之后,启动就是一个已经有了大量基础配置的 neovim 了,基本做到了开箱即用。 完成 LazyVim 的配置,记得看一下是否系统安装了 fzf。 配置说明 在 LazyVim 的项目中,配置可以分成两大块:基础配置(config) 和 插件(plugins)。 config 配置 config 配置主要是一些基础的配置,文件夹内区分了四个文件, 分别是基础配置(options.lua),快捷键配置(keymaps.lua),自动化命令(autocmds.lua)和 lazy 初始化配置。 plugins 配置 默认 LazyVim 已经集成了很多插件,有的已经默认开启,有的需要手动开启。如果需要覆盖原有的插件配置或者安装新的插件, 可以在该文件夹下直接填写相关配置,最后在外层的 init.lua 中添加即可。 快捷键 LazyVim 已经集成了大量的插件,并且默认了很多快捷键。默认的 Leader 为 空格。可以针对自己的习惯修改快捷键,也可以直接按下 Leader 来查看快捷键。 默认的快捷键我认为是比较合理,而且使用几次基本也会记住,这里我除了修改了一下 blink 候选提示的上下选择之后,并没有做其他修改。 默认的快捷键也可以在官方文档中找到 keymaps LazyExtras LazyVim 一些默认的插件已经启用,但是除了默认插件,还有一些其他插件可以根据具体的情况按需使用。 通过 :LazyExtras 命令可以查看其他扩展插件。如果有自己使用的,需要的可以直接通过 x 进行启用。 需要注意的是启用后需要重新启动一些 neovim。 自动命令 LazyVim 中自带了一些配置,例如 tab 为 2,在一些缩进比较多语言中,tab 为 2是比较友好的,例如 lua,但是对于一些项目,尤其是很团队合作的项目, tab 改成2 就会让代码一团乱。 在 options.lua 中新增一些 tab 相关的配置。 lua local opt = vim.opt opt.expandtab = true opt.tabstop = 4 opt.shiftwidth = 4 opt.softtabstop = 4 同时为了让 lua 还保持 tab 为2的缩进,在 autocmds.lua 中新增自动化命令。 lua vim.api.nvim_create_autocmd("FileType", { pattern = { "lua" }, callback = function() vim.opt_local.expandtab = true vim.opt_local.tabstop = 2 vim.opt_local.shiftwidth = 2 vim.opt_local.softtabstop = 2 end, }) 还有一个就是中文拼写问题,默认的 spell 一直检查,尤其是在进行 markdown 的时候,大量的波浪线导致编辑的感官太差,可以新增命令来解决。 lua vim.api.nvim_create_autocmd("FileType", { pattern = { "markdown", "txt" }, callback = function() vim.opt_local.spell = false end, }) vscode 使用 LazyVim LazyVim 对 vscode 支持比较好,并且 vscode-neovim 也推荐使用 lazyvim。通过这个插件,可以让我们在 Vscode 中加载一些 Neovim 的插件,做到一个配置两处使用。 首先需要在 LazyExtras 中打开 Vscode 。 然后在 Vscode 中安装 Vscode Neovim 插件。 在 LazyVim 中可以通过 vim.g.vscode 来判断当前的运行环境。 首先是对快捷键进行配置,比如常用的重命名、格式化等。 lua local map = vim.keymap.set if vim.g.vscode then map("n", "<leader>cf", "<Cmd>lua require('vscode').call('editor.action.formatDocument')<CR>", { desc = "Format" }) map("v", "<leader>cf", "<Cmd>lua require('vscode').call('editor.action.formatSelection')<CR>", { desc = "Format" }) map("n", "<leader>cr", "<Cmd>lua require('vscode').call('editor.action.rename')<CR>", { desc = "Rename" }) end Golang 开发 针对 Golang 开发,首先就是语言的相关配置: LSP:通过 Mason 安装 gopls; Testing: 通过 LazyExtras 进行安装 test.core; DAP: 通过 Mason 安装 dlv。 安装完成就可以进行编辑,运行,测试和调试。 可以通过 <Leader>tr 进行测试运行。 通过 <Leader>db 进行打断点。 通过 <Leader>dc 进行 Debug 运行。
CAP 理论 在分布式系统中最重要的一条理论为 CAP 理论。这个理论是由加州大学伯克利分校的计算机科学家 Eric Allen Brewer 在 2000 年提出的一个猜想,由 2002 年,麻省理工两位科学家发表了该猜想的证明,使得该猜想变成了一个定理。 CAP 定理中对分布式系统提出了三点,分别为: 一致性(Consistency):多个副本的数据之间能够保持一致; 可用性(Availability):每次请求都可以获取到正常的、非错误的响应,但是无法保证数据是最新的; 分区容错性(Partition tolerance):系统的某个节点发生故障,仍然还可以对外提供一致性和可用性的服务。 例如当前系统有两个 DB 分别为 DB0 和 DB1。 一致性 C 一致性是指在数据发生变化的时候(也就是写操作发生后),无论是谁获取到的数据(也就是读操作)也是一样的。 当用户1 通过写操作对 DB0 的数据进行修改后,那么无论用户1 还是 用户2 ,无论从 DB0 还是 DB1 读取,读取后的数据都应该是完全一样的,这就是所谓的一致性。 也就是 DB0 的数据发生了修改,应该由相关的机制告诉 DB1 也将相关的数据发生修改,保证该数据在不同的 DB 中是一样的。 可用性 A 当用户发出相关请求,无论 DB0 还是 DB1 都会返回相关的数据,但是这里不需要关心数据是否一致。 分区容错性 P DB0 和 DB1 例如出现了问题,这个问题可能是网络问题,也有可能是其他硬件问题,导致了 DB0 和 DB1 的系统无法通信。这样 DB0 和 DB1 就成为了两个分区。即使 DB0 和 DB1 无法进行通信,但是 DB0 和 DB1 仍旧可以对外提供服务。 但是这种情况,在实际系统中无法避免这种情况,所以分区容错性是一个必选的条件。 CP 还是 AP 既然 CAP 三条规则无法同时满足,那么就出现了上图中的三种情况,满足任意两条规则,也就是 CA,CP 和 AP 三种架构,但是分区容错性是必选的,这样我们就剩下 CP 和 AP 两种关系。 常见的 CP 软件有 Zookeeper,Zookeeper 为了保证数据的一致性,牺牲了可用性。任何时候 Zookeeper 的访问都能获取一致的结果,但是不保证每次服务请求都可以用。 而 AP 架构中,要求数据一致性并不是那么重要,允许不同的服务可以返回不同的数据。 CAP 的不足 CAP 理论并不是完美的,存在很多问题。例如 DB0 和 DB1 要保持数据的一致性,那么就会发生相关通信,通信是需要时间,这就导致了某些时刻数据是不同步的,常见的情况在主从的机器上的主从延迟,当延迟过大的时候,用户读取的数据是不一致的。 CAP 理论也并不完全是三选二(或者说二选一)的问题,例如分区容错性发生的概论很小,那么就没必要牺牲了 A 和 C。 BASE 理论 BASE 理论算是 CAP 理论的延伸,是对 CAP 理论中一致性和可用性的权衡。在 CAP 中,所谓的一致性是指数据时时刻刻的都保持一致,也就是强一致性。上文中 CAP 的不足也说到,要保证时时刻刻数据的一致性是一件很困难的事情。而 BASE 理论就是对改问题的补充,既然很难做到强一致性,那么系统根据自身的业务特点,确保系统中的数据保证最终一致性也是可以的。 BASE 理论是 ebay 工程师 Dan Pritchett BASE 是指 Basically Available, Soft State 和 Eventually Consistent 三个短语的缩写。 Basically Available 基本可用 基本可用指的是系统出现故障后,但是还可以使用,但是可能比较正常系统上可能出现一些问题,例如响应时间上,服务降级牺牲部分功能等。 Soft State 软状态 软状态指的是系统数据允许出现中间状态,例如数据库主从同步过程中会出现中间状态,这就是一种软状态。 Eventually Consistent 最终一致性 最终一致性强调经过上述的软状态后,最后数据保持一致性。 BASE 理论的提出是通过牺牲系统的强一致性来保证系统的可用性,允许系统在一段时间内的数据是不一致的,但是要求最终数据的一致性。 参考文章 一文看懂|分布式系统之CAP理论 分布式架构之CAP理论/AP架构/CP架构
neovim入门指南(三):LSP配置 上 中说了 lsp 是什么如何配置和启动。那么接下来就完成 lsp 的其他配置。本章节主要介绍下面几个方面的介绍:代码高亮,文件格式化,lsp 相关的 UI 美化。 ⚠️错误警告提示 在有了 lsp 之后,当我们编写代码发生错误的时候,就会有相应的提醒。如图下提示 目前的错误还是字母,例如警告是W 。不太美观。对于这个来说,vim 开放了相关的接口,可以通过接口进行设置, neovim-diagnostic。 仍旧在 lsp 的文件夹下建立 ui.lua 目录。 lua -- lsp/ui.lua -- 对错误警告的图标 vim.diagnostic.config({ virtual_text = true, signs = true, -- 在输入模式下也更新提示,设置为 true 也许会影响性能 update_in_insert = true, }) local signs = { Error = "", Info = "", Hint = "", Warn = "" } for type, icon in pairs(signs) do local hl = "DiagnosticSign" .. type vim.fn.sign_define(hl, { text = icon, texthl = hl, numhl = hl }) end 其中 vim.diagnostic.config 是在配置提示文本,virtual_text 为错误信息提示. 🔅 代码高亮 目前的设置中,对于代码的高亮并不完美,需要采用一个插件了完成。tree-sitter 是一个用 rust 编写的高性能代码高亮的渲染工具,很多编辑器会采用它作为高亮功能的实现,比如说 Zed 。基于 tree sitter ,可以让我们的 neovim 的高亮更加完美。 这是来源于 nvim-treesitter 的一张图,图中左侧为未启用 treesitter 的代码高亮,右侧为启用后的效果。启用后的效果还是很明显的。 安装 tree-sitter 后,可以通过 TSInstall 来安装具体的语言高亮,例如 Rust 的高亮,可以直接使用 TSInstall rust 命令,或者使用 TSUpdate rust 命令进行更新。整体安装和配置还是比较简单的,这里不过多的解释。 📃 格式化 代码格式化是一个很重要的功能,也是我们使用频率很高的功能。针对 Neovim 的代码格式化有很多种方式。这里主要介绍 null-ls。不幸的是,null-ls 已经归档来,不再进行维护,万幸的是,它的 fork 项目 none-ls 重新维护起来,而且目前还兼容 null-ls 的配置,如果你之前使用的是 null-ls,那么只需要将依赖地址由 jose-elias-alvarez/null-ls.nvim 改为 nvimtools/none-ls.nvim 即可。 安装好 none-ls,后可以通过 Mason 安装相关的 formatter,例如安装 lua 的 formatter。通过命令 :Mason 打开安装界面。 选择 stylua,进行安装。安装完成后就可以进行配置,在 lua/lsp 下新建文件,命名为 nonels.lua。 lua -- lsp/nonels.lua -- 即使是采用了 none-ls, 这里也是获取 null-ls local status, null_ls = pcall(require, "null-ls") if not status then vim.notify("没有找到 null-ls") return end local formatters = null.builtins.format null_ls.setup({ sources = { -- Stylua formatters.stylua, -- 其他 formatter 方式 }, }) 这样在我们进行编写代码的时候,进行相关格式化。在上一文中也进行了介绍,对快捷键进行了绑定。使用 <Leader>= 进行格式化。 🎨 自动补全美化 关于前面,介绍了通过 cmp 插件进行了自动补全,通过 cmp 可以补充不同来源的代码。 我们使用一些 IDE 或者编辑器的时候,在补全的选项中是会展示一些图标,用来标识补全项的类型,例如变量、类名、方法或是接口等。 RustRover 自动补充样式。 VS Code 自动补全样式。 通过配置,也可以让 neovim 的提示实现上图效果。 当前自动补全的效果如图,前面为补全的字段,后面通过文字来标识补全的类型。 有一个插件叫做 lspkind ,可以通过安装该插件结合我们的 cmp 完成上述的样式。 在 lsp 下新建文件 kind.lua,配置 kind,这个可以在 lspkind,找到相关配置。 lua -- lsp/kind.lua local lspkind = require("lspkind") lspkind.init({ mode = "symbol_text", preset = "codicons", symbol_map = { Text = "", Method = "", Function = "", Constructor = "", Field = "", Variable = "", Class = "", Interface = "", Module = "", Property = "", Unit = "", Value = "", Enum = "", Keyword = "", Snippet = "", Color = "", File = "", Reference = "", Folder = "", EnumMember = "", Constant = "", Struct = "", Event = "", Operator = "", TypeParameter = "" }, }) 这里可以配置每个类别的图标,配置完成后在 cmp 中进行配置。主要是修改提示的样式。这里主要参考了 Github 上的一个样式,。在 formatting 中进行配置。 lua -- cmp.lua cmp.setup({ -- 省略其他代码 formatting = { completion = { border = { "╭", "─", "╮", "│", "╯", "─", "╰", "│" }, scrollbar = "║" }, documentation = { border = { "╭", "─", "╮", "│", "╯", "─", "╰", "│" }, scrollbar = "║", }, format = lspkind.cmp_format({ mode = "symbol", maxwidth = 20, ellipsis_char = "...", before = function(entry, vim_item) -- Get the full snippet (and only keep first line) local word = entry:get_insert_text() if entry.completion_item.insertTextFormat == types.lsp.InsertTextFormat.Snippet then word = vim.lsp.util.parse_snippet(word) end word = str.oneline(word) if entry.completion_item.insertTextFormat == types.lsp.InsertTextFormat.Snippet and string.sub(vim_item.abbr, -1, -1) == "~" then word = word .. "~" end vim_item.abbr = word return vim_item end, }), } -- 省略其他代码 }) 完成后的样式如图,基本和 VS Code 一样。 lspsage lspsage 可以极大的提高 nvim lsp 的体验。lspsage 的功能基本都是在增强和美化原有的 lsp 。具体的文档可以查看 nvimdev。 lspsage 主要功能: Finder: 用于高级 LSP 符号搜索的 UI 上面来自于官方文档的介绍。 Finder Finder 的功能是一个类似 VS Code 的变量/函数 查看的界面,可以在界面上看到这个变量/函数的定义和调用地方。 lua -- keybindings.lua -- 其他代码 pluginKeys.mapLSP = function(mapbuf) { -- 其他代码 mapbuf("n", "gf", ":Lspsaga lsp_finder<CR>", opt) -- 新增 -- 其他代码 } 这样在 normal 情况下按下 gf 就会出现上面的 finder 窗口。 Code Action Code Action 是一个非常重要的功能,可以根据代码上下文给出相关的提示或者代码改进。例如这块代码,Code Action 提醒我们可以直接转换为字面量的拼接。 Code Action 并不是 lspsaga 提供的,但是 lspsaga 给提供了一个漂亮的界面。在有 Code Action 的地方,会有一个黄色灯泡 💡 的提示。 下面绑定我们的快捷键。 lua -- keybindings.lua pluginKeys.lspKeybinding = function(mapbuf) -- 省略其他代码 -- code action -- mapbuf("n", "<leader>ca", ":lua vim.lsp.buf.code_action()<CR>", opt) -- 原有的 mapbuf("n", "<leader>ca", ":Lspsaga code_action<CR>", opt) -- 替换为这个 end Float Terminal lspsaga 提供了一个浮动终端窗口,可以在终端上执行任何命令。可以在 keybindings 中绑定这个快捷键,或者直接使用默认的快捷键 t ,来进行终端的打开和关闭。 lspsaga 还提供了很多功能,基本每个功能对我们都有帮助,这里就不做过多介绍了,大家可以看官方文档。 这里基本介绍完了 lsp 的常见功能和配置,剩下的大家可以通过需要进行安装和配置。 这里是我的配置 youngxhui/nvim 大家可以适当参考。 小结 vim/neovim 使用的时候,的确是有一个较高的上手成本,尤其是方向键的习惯,当完全习惯了 h,j,k 和 l 进行光标移动的时候,其实就离入门 vim/neovim 不远了。 当习惯了 vim 这一套风格之后,就会尝试把手头的编辑器/IDE 的快捷键都采用 vim ,而且在熟悉 vim 的快捷键后,真正的感觉到了手指在键盘上飞舞。 无论是 neovim 还是相关插件,更新迭代是很快的,也许当你看到这篇文章的时候,上述的很多配置都已经失效,或者相关插件已经停止维护,文章只是抛砖引玉,我相信你会找到合适自己的 nvim 配置。 我相信喜欢 vim/neovim 的人都是有一颗专研,喜欢折腾的心。正是这样的心才陪伴这个你我一步一步前进。 写到这里有感而发,下一篇我们将会介绍 DAP 。
🧩 什么是 LSP 对于一个编辑器来说,如果要完成例如自动补全,查找相关定义等功能是需要进行大量的开发的。不同的编辑器为了不同的语言都需要进行开发,而 LSP 的存在就是将这个过程检化。LSP 的全称为 Language Server Protocol,定义了编辑器和语言服务之间使用的协议。只要相关语言支持 LSP,那么编辑器只要符合相关要求实现即可完成例如自动补全等功能,而且不同的编辑器使用的体验是一致的。 目前支持 LSP 的编辑器有很多,例如大名鼎鼎的 Vscode。当然 vim 8 以后版本和 neovim 也都支持,具体支持的编辑器/IDE 列表可以看 LSP 的官方网站,同时支持 LSP 的语言也可以找到 支持语言。 neovim 已经是支持 LSP 了,具体可以在相关的配置文档看到,该文档详细的描述了如何配置一个 LSP。相对来说,配置过程比较繁琐,所以官方又提供了另一个库 nvim-lspconfig。接下来我们就通过这个插件来配置 neovim 的 lsp。 nvim-lspconfig 与安装其他插件是一样的,只需要我们在 plugins_config.lua 中添加相关配置即可,这里不进行赘述了。安装完成后其实进行配置就可以启用 LSP 了,就是这么简单。例如支持 rust 的 LSP,只需要进行简单的配置。 lua lspconfig.rust_analyzer.setup { settings = { ['rust-analyzer'] = {}, }, } 但是,nvim 只是 lsp 的客户端,那么就存在 lsp 的服务端。上面配置的 rust_analyzer 就是 rust 语言的服务端,就需要我们进行服务端的安装。rust_analyzer 的服务端地址是 rust-lang/rust-analyzer,需要将服务端下载并且安装好,这样每次编写rust的时候就会享受 lsp 的服务加成了。 但是这样做有几个问题,当然也不能算问题,只是不太方便。 对于多语言使用者来说,需要手动安装多个 lsp 服务端,目前 lsp 的服务端应该是没有统一的下载安装地址,需要手动寻找; 每次服务端进行更新,都需要重新下载安装; 新换设备之后,无法开箱即用,需要重复上述的方式重新开始一次。 面对上面的不方便,你可能已经想到很多解决方法,例如写个脚本进行一键安装和更新常用的 lsp 服务端。这样基本解决了上面说的所有问题。正如你想的那样,今天的第二位主角 williamboman/mason.nvim。 🗃️ mason.nvim mason 是一个可以方便的管理 LSP 服务端,DAP 服务端,Linter 和 格式化工具的插件。安装它之后,上面所说的问题将不是问题。 为了让 mason 和 nvim-lspconfig 更好的配合,这里还需要安装另一个插件 williamboman/mason-lspconfig.nvim 同样的安装这里不多赘述,主要是进行相关的配置。这里为了区别其他的插件,我们在 lua 目录下建立新的文件夹 lsp,用来专门存放 lsp 的配置。 配置 mason 首先还是加载我们的插件。在 lsp 文件夹中新建 mason.lua 文件,在文件中新增下面的配置。配置主要是在加载插件。 lua -- mason.lua local mason_status, mason = pcall(require, "mason") if not mason_status then vim.notify("没有找到 mason") return end local nlsp_status, nvim_lsp = pcall(require, "lspconfig") if not nlsp_status then vim.notify("没有找到 lspconfig") return end local mlsp_status, mason_lspconfig = pcall(require, "mason-lspconfig") if not mlsp_status then vim.notify("没有找到 mason-lspconfig") return end mason.setup() mason_lspconfig.setup({}) 📤 安装 lsp 服务端 配置完成后,重新启动 nvim,此时就可以采用 mason 进行 LSP 的服务端进行管理了。只需要按下 :Mason 即可。你将会看到如下的界面。 通过界面上的帮助可以看到如何使用,通过数字可以选择不同的服务端项目,2 为 LSP , 3 为 DSP 等。今天只是使用 LSP,可以直接按 2,选择到 LSP 界面,进行 LSP 安装。仍旧是通过 j 和 k 进行滑动。第一个安装的 lsp 服务端为 lua 语言的服务端:lua-language-server。这个是 lua 语言的语言服务,有 lsp 之后,我们之后无论是配置 nvim 还是编写 lua 都会有 lsp 服务的加持。按下 i 进行安装。 稍等片刻,安装完成。接下来就是配置,让 nvim 知道我们的 lsp 已经安装,在合适的时候进行启动。 lua -- mason.lua nvim_lsp.lua_ls.setup({ on_init = function(client) local path = client.workspace_folders[1].name if not vim.loop.fs_stat(path .. "/.luarc.json") and not vim.loop.fs_stat(path .. "/.luarc.jsonc") then client.config.settings = vim.tbl_deep_extend("force", client.config.settings, { Lua = { runtime = { version = "LuaJIT", }, workspace = { checkThirdParty = false, library = { vim.env.VIMRUNTIME, }, }, }, }) client.notify("workspace/didChangeConfiguration", { settings = client.config.settings }) end return true end, }) 这样 lua 的 lsp 就配置成功了,当我们编写 lua 脚本的时候,如果发生错误就会有相关提醒。当然这只是 lsp 最基础的功能,例如代码跳转,代码补全等需要我们进行配置。 基本所有的 lsp 的配置都可以在 server_configurations.md 中找到,当然 lua_ls 也不例外,上面的配置就是直接从文档中复制的 😄。 当前的 LSP 配置已经支持代码跳转,code action 等功能。例如查看当前变量或者函数的文档,可以使用这个命令 :lua vim.lsp.buf.hover()。 相关的命令还有其他 功能 命令 文档显示 :lua vim.lsp.buf.hover() 查看定义 :lua vim.lsp.buf.definition() 重命名 :lua vim.lsp.buf.rename() 查询实现 :lua vim.lsp.buf.implementation() 查询引用 :lua vim.lsp.buf.refreences() 查询声明 :lua vim.lsp.buf.declaration() 格式化 :lua vim.lsp.buf.format() Code action :lua vim.lsp.buf.code_action() 对于这些基础功能来说,每次需要的时候都在命令模式下敲一堆,速度的确是很慢的。所以,可以将上述的命令定义为快捷键,这样每次只需要进行快捷键进行完成上述功能。 ⌨️ 快捷键绑定 打开我们之前设置快捷键的配置文件 keybinding.lua,新增上述功能的配置。 lua -- keybinding.lua -- lsp 快捷键设置 pluginKeys.lspKeybinding = function(mapbuf) -- rename mapbuf("n", "<leader>r", ":lua vim.lsp.buf.rename<CR>", opt) -- code action mapbuf("n", "<leader>ca", ":lua vim.lsp.buf.code_action()<CR>", opt) -- go to definition mapbuf("n", "gd", ":lua vim.lsp.buf.definition()<CR>", opt) -- show hover mapbuf("n", "gh", ":lua vim.lsp.buf.hover()<CR>", opt) -- format mapbuf("n", "<leader>=", ":lua vim.lsp.buf.format { async = true }<CR>", opt) end 完成快捷键的配置,那么就可以将快捷键绑定到刚刚配置的 lsp 服务端了。 lua -- mason.lua function LspKeybind(client, bufnr) local function buf_set_keymap(...) vim.api.nvim_buf_set_keymap(bufnr, ...) end -- 绑定快捷键 require("keybinding").lspKeybinding(buf_set_keymap) end 接下来可以完成快捷键的绑定。 lua -- mason.lua nvim_lsp.lua_ls.setup({ on_attach = LspKeybind, on_init = function(client) -- 省略其他配置 end, }) 这样就完成了 lua 的 lsp 的配置,在编写 lua 的时候就可以使用文档查看,code Action 等功能。 目前这些 lsp 的服务都要手动下载,对于一些日常使用的服务,我们可以通过配置,在第一次加载配置的时候,当机器上没有相关的服务的时候,自动下载,这样来说,基本实现了我们上述提出的问题。 lua -- mason.lua mason_lspconfig.setup({ automatic_installation = true, ensure_installed = { "lua_ls", "rust_analyzer" }, }) 这样配置,如果我们本地没有安装 lua 和 rust 的 lsp,会自动进行下载安装。 自动补全 直到目前,在 lsp 的加持下,其实编辑体验已经变得非常棒了,而且开发速率也会大幅提升,虽然 lsp 是支持自动补全功能的,但是上面其实一直没有提及。主要是单单靠 neovim 的功能还不够强大,需要插件的配置。 hrsh7th/nvim-cmp 是一个采用 lua 编写的补全引擎,通过 cmp 及 cmp 的相关插件,会将 neovim 的自动补全达到一个新的高度。 这里除了 nvim-cmp,再推荐几个 cmp 的相关插件。更多的相关插件可以在 wiki 中找到 hrsh7th/cmp-nvim-lsp 对 lsp 提供的补全信息进行提示; hrsh7th/cmp-path 对文件内的路径进行补全; hrsh7th/cmp-vsnip 常见的代码 snip 片段。 配置补全 同样不赘述安装。在 lsp 文件夹中新建 cmp.lua 文件夹。 lua -- cmp.lua local status, cmp = pcall(require, "cmp") if not status then vim.notify("找不到 cmp") return end 剩下了就可以将之前的补全源进行配置。这里贴出我的补全源。 lua sources = cmp.config.sources({ { name = "codeium" }, -- 需要安装 codeium.nvim { name = "nvim_lsp" }, -- For vsnip users. { name = "vsnip" }, }, { { name = "buffer" }, { name = "path" }, }), 此时当我们进行输入的时候就可以看到自动补全的提示了。对于自动补全的提示,上下选择并且上屏,我们可以设置快捷键,来满足我们的使用习惯。 和之前设置快捷键一样,在 keybindings.lua 中添加相关配置。 lua -- keybindings.lua pluginKeys.cmp = function(cmp) return { -- 出现补全 ["<A-.>"] = cmp.mapping(cmp.mapping.complete(), { "i", "c" }), -- 取消 ["<A-,>"] = cmp.mapping({ i = cmp.mapping.abort(), c = cmp.mapping.close(), }), -- 上一个 ["<C-k>"] = cmp.mapping.select_prev_item(), -- 下一个 ["<C-j>"] = cmp.mapping.select_next_item(), -- 确认 ["<CR>"] = cmp.mapping.confirm({ select = true, behavior = cmp.ConfirmBehavior.Replace, }), } end 最后在 cmp.lua 中使用这些快捷键即可。 lua cmp.setup({ -- 省略其他配置 mapping = require("keybindings").cmp(cmp), }) 这样便可以完成自动补全的配置了。 小结 目前为止,已经完成 nvim 的 lsp 的相关配置,并且添加了自动补全。篇幅限制,剩下如何美化 lsp 提示,美化自动补全等我们下篇再说。 我的 neovim 相关配置,可以提供大家进行参考 youngxhui/nvim。
常用插件配置 经过前面章节的介绍,当前的 neovim 已经可以开始基本的使用了。下面会推荐一下常见的插件,让我们快速的开始。 📂 nvim-tree nvim tree 是一个文件浏览器,可以实现侧栏快速选择文件。 当前 neovim 的插件安装都很简单,根据我们之前所了解的方式,先在 Github 上找到相关仓库:https://github.com/nvim-tree/nvim-tree.lua,然后安装。具体如何安装,其实在项目的 README 中会有详细说明。 这里需要安装两个包,第一个就是 nvim-tree,而第二个是一个可选包,主要是用来显示图标的。在 plugins.lua 中新增配置。 lua -- plugins.lua require("lazy").setup( -- 省略其他配置 { "kyazdani42/nvim-tree.lua", event = "VimEnter", dependencies = "nvim-tree/nvim-web-devicons" }, ) 之后在 lua 目录中新建一个 plugins-config 目录,目录中新建 nvim-tree.lua 文件。之后就可以开始我们的配置了。大部分配置其实可以参考官方的 Wiki。 这里我们通过 pcall 函数来加载相关插件。 为什么要使用 pcall ? 当插件没有安装或者出现其他问题的时候,nvim 在启动时,无法加载相关查询,就会抛出异常,通过 pcall 就可以进行相关捕获,从而不影响 nvim 的使用。 lua local status, nvim_tree = pcall(require, "nvim-tree") if not status then vim.notify("没有找到 nvim-tree") return end 这样在加载不到 nvim-tree 的时候,就会通过 vim.notify 报出来。 剩下的就是配置我们的 nvim-tree 了。 lua nvim_tree.setup({ sort_by = "case_sensitive", -- 是否显示 git 状态 git = { enable = true, }, -- 过滤文件 filters = { dotfiles = true, -- 过滤 dotfile custom = { "node_modules" }, -- 其他过滤目录 }, view = { -- 文件浏览器展示位置,左侧:left, 右侧:right side = "left", -- 行号是否显示 number = false, relativenumber = false, signcolumn = "yes" -- 显示图标 width = 30, }, renderer = { group_empty = true, }, }) 最后在最外层的 init.lua 中添加配置。 lua require("plugins-config.nvim-tree") 重新退出后打开,通过命令 :NvimTreeToggle 或者 A-m,可以打开或关闭侧边栏。 如果需要修改快捷键,可以在 keybindings.lua 中新增相关快捷键,之后在 nvim-tree 中引用。 lua -- keybindings.lua local pluginKeybinding = {} pluginKeyBinding.nvim-tree = { { key = "<F5>", action = "refresh" }, } 在 nvim-tree 中,设置相关快捷键。首先要在 nvim-tree.lua 中引入这个变量,并在 setup 中设置相关值。 lua local keymap = require('keybindings').nvim-tree nvim_tree.setup({ -- ... view = { mappings = { custom_only = false, list = keymap, } } -- ... }) 📨 bufferline bufferline 是一个对 buffer 进行管理的插件,可以像现代IDE或者编辑器一样打开多个 Tab,而且可以快速进行切换。安装部分不多赘述,在 README 中上有详细描述。bufferline 地址:https://github.com/akinsho/bufferline.nvim 在 plugins-configs 目录下新建 bufferline.lua 文件,进行配置,仍旧是采用 pcall 进行加载,之后就可以进行配置。 lua -- bufferline.lua local status, bufferline = pcall(require, "bufferline") if not status then vim.notify("没有找到 bufferline") return end bufferline.setup({}) 再次打开 neovim,同时打开多个文件,就会发现在上方出现了新的 tab。 需要进行一些设置,例如当前打开的前面安装的 nvim-tree , tab 会显示在 nvim-tree 上面,这显然是不符合预期的。通过命令 :h bufferline-configuration 来查看插件支持的配置。 通过帮助文档可以看到 offset 配置和 nvim tree 有关系,这样我们就可以通过相关的配置完成配置。 lua -- bufferline.lua bufferline.setup({ options = { offsets = { { filetype = "NvimTree", text = "File Explorer", text_align = "left", separator = true, }, }, }, }) 通过这样的设置就可以发现,当打开 nvim tree 的时候,tab 栏会自动向右偏移,而不会出现在 nvim tree 上方。 其他的设置可以通过上述的方式,通过帮助来查看。这里贴出我的配置。 lua -- bufferline.lua options = { close_command = "bdelete! %d", -- 点击关闭按钮关闭 right_mouse_command = "bdelete! %d", -- 右键点击关闭 indicator = { icon = '▎', -- 分割线 style = 'underline', }, buffer_close_icon = '', modified_icon = '●', close_icon = '', offsets = { { filetype = "NvimTree", text = "File Explorer" , text_align = "left", separator = true, } }, } 这里使用了 bdelete 相关命令,这个是 moll/vim-bbye 这个插件提供的,使用上面的配置需要安装该插件。 同时,可以在 keybinding.lua 中设置切换 tab 的快捷键。我这里设置为 Ctrl + h/l 来左右切换。 lua -- keybindings.lua map("n", "<C-h>", ":BufferLineCyclePrev<CR>", opt) map("n", "<C-l>", ":BufferLineCycleNext<CR>", opt) ✏️ lualine lualine 插件可以提供类似的效果,在编辑器上提供一些信息提示,例如 Git 状态,文本编码等,下图是 Github 上的效果。 如何安装可以直接看 nvim-lualine/lualine.nvim 上介绍,不多赘述。同样在 plugins-configs 下建立相关的配置文件 lualine.lua,通过 pcall 方式引入,使用 lualine.setup({}) 引入插件,最后在 init.lua 中添加配置文件。每个插件的安装使用方式都基本相似,如果有不同情况,会另外说明,后续的插件安装将不在说明这些步骤。 在 lualine 中,显示区域被分成了 6 个部分,分被用 A,B,C,X,Y,Z 组成。 txt +-------------------------------------------------+ | A | B | C X | Y | Z | +-------------------------------------------------+ 上面的每个部分都可以进行定制。在仓库中有三种样例,可以直接在 example 中找到。 相关配置: lua local status, lualine = pcall(require, "lualine") if not status then vim.notify("没有找到 lualine") return end lualine.setup({ options = { theme = "auto", component_separators = { left = "|", right = "|" }, section_separators = { left = " ", right = "" }, }, extensions = { "nvim-tree", "toggleterm" }, sections = { lualine_c = { "filename", }, lualine_x = { "filesize", { "fileformat", symbols = { unix = '', -- e712 dos = '', -- e70f mac = "", -- e711 }, }, "encoding", "filetype", }, }, }) 经过配置就可以看到这样的效果了。 最近(2024 年 03 月 02 日)我更新了一下 lualine 样式,相较于之前更加简洁。相关样式如下 相关配置 lua lualine.setup({ options = { theme = "auto", component_separators = { left = "", right = "" }, section_separators = { left = " ", right = "" }, }, extensions = { "nvim-tree", "toggleterm" }, sections = { lualine_a = { { "mode", icons_enabled = true, }, }, lualine_b = { { "filetype", colored = true, icon_only = true, }, "filename", }, lualine_c = { "branch", "diff", }, lualine_x = {}, lualine_y = {}, lualine_z = { "progress", }, }, }) 🔍 telescope 经过上面的配置,Neovim 的编辑界面已经逐渐“现代化”。Telescope 可以让我们的查找过程更加丝滑,主要可以用来做模糊查询。 telescope 安装也比较简单,可以参考 github 上的 README 进行安装。但是往往安装后无法进行模糊查询。这里就需要我们的另外的配置了。其实在 README 中也写的很明白,安装完成后,需要运行 :checkhealth telescope 命令。 通过该命令,可以看到当前插件的状态,是否可用,如果是首次安装,会提示 ERROR 和 WARNING,如图所示: 从提示可以看到缺少 rg 和 fd,同时在后面的说明中给出了相关的安装地址。这两个软件是进行模糊搜索的关键,可以通过以下两个地址进行安装,在 Github 的 README 中都明确的写出了两个软件在不同的系统上的安装方式。 rg:https://github.com/burntsushi/ripgrep fd: https://github.com/sharkdp/fd 如果你是用 mac,恰好已经安装 brew,那么只需要下面的两行命令即可完成安装。 shell brew install rg brew install fd 安装完成后,重新 :checkhealth telescope,如果都是 OK,则证明安装正确,如下图所示: 安装完成后就可以通过命令使用 telescope,进行快捷的模糊查询。:Telescope find_file 为查找文件,Telescope live_grep 为全局查询。为了方便,可以在 keybindings.lua 配置中绑定为对应的快捷键。下面是我快捷键的对应绑定。 lua -- keybindings.lua -- 查找文件 map("n", "<C-p>", ":Telescope find_files<CR>", opt) -- 全局搜索 map("n", "<C-f>", ":Telescope live_grep<CR>", opt) 更多有趣的配置可以看 Github 仓库。 附录 Nerd 字体安装 在上面的很多配置中,很多图标是无法显示的,会导致文本中或者界面中显示有问题,常常会显示为一个⊠,这里就需要一个 nerd 字体进行支持。 Nerd 字体可以看做原始字体的一个补丁,这个补丁对原始字体新增了大量的图标。 针对上面无法显示的图标,需要安装好相关的 Nerd 字体。相关字体可以在官方网站下载,一些知名的字体都会有 nerd 版本,例如:FiraMono、JetBrainsMono、UbuntuMono 等。大家可以依照自己的喜好进行下载安装。 字体配置 不同的终端模拟器有不同的配置方式,这里列出常见的配置方式。 iTerm2 iterm2 设置如下 Profiles -> Text 中,将 font 修改为 Nerd 字体即可。 WezTerm WezTern 修改配置文件 ~/.config/wezterm/wezterm.lua。 lua local wezterm = require("wezterm") local config = {} config.font = wezterm.font_with_fallback({ { family = "JetBrainsMono Nerd Font Propo", weight = "Bold" }, { family = "苹方-简", weight = "Bold" }, "Noto Color Emoji", })
在编程的世界中,有两个上古神器。一个叫做 “神之编辑器 Emacs”,另一个叫做 “编辑器之神 vim”。这两个编辑器从诞生到现在,圣战从未结束。无论是 vim 还是 emacs 都在不断的进化和发展,渐渐的 vim 的分支上出现了一颗夺目的新星,他就是 neovim。 什么是 neovim neovim 从名称来看:新星的vim。按照官方说明: nvim 是一个 vim 的 fork,主要关注了扩展性和易用性。大量的 vim 用户迁移到 nvim,而 vim 的魅力出了经典的快捷键还有丰富的插件系统,这些 nvim 都继承了下来,同时内置了 LSP,增加了 异步 IO 等新特性。 以下是 Neovim 的一些主要特点和优势: 兼容性:Neovim 是 Vim 的兼容版本,几乎可以无缝地使用现有的 Vim 配置文件和插件。它支持 Vim 的命令和操作方式,因此 Vim 用户可以很容易地切换到 Neovim。 异步支持:Neovim 引入了异步任务处理的机制,使得编辑器可以在后台执行长时间运行的任务,而不会阻塞用户界面。这使得插件和脚本可以更高效地处理耗时操作,提高了编辑器的响应性。 现代化的插件系统:Neovim 提供了更灵活、更易于扩展的插件系统。它支持各种编程语言编写的插件,并提供了对外部进程通信的接口,使得插件可以与其他程序进行交互。 社区活跃:Neovim 拥有一个活跃的社区,不断推动编辑器的发展和改进。社区提供了大量的插件、主题和配置文件,以及对新功能的贡献和支持。 📃基础配置 neovim 中配置可以通过 init.vim 或者 init.lua 进行配置,当前大部分的配置都采用了 lua ,本文也将会通过 lua 进行配置 nvim。如果你还不会使用 lua 也不需要担心,lua 可以快速上手。你可以直接通过 :h lua-guide 进行查看 lua 教程。 init.lua 在类 unix 系统中,该配置文件位于 ~/.config/nvim/ 目录下,而在 windows 系统中,该目录位于 %USERPROFILE%\AppData\Local\nvim\。nvim 启动时会加载该目录下的 init.lua 文件,那么只需要在该文件中进行配置即可。 首先对文件的编码格式设置,统一为 UTF-8 。只需要在 init.lua 中添加相关配置。当然这是 nvim 的默认配置,不进行添加也是可以的。 lua vim.g.encoding = "UTF-8" vim.o.fileencoding = "UTF-8" 这里的 o 和 g 是什么意思呢? vim.o 意味着可以对全局的选项进行设置,而 vim.g 是进行全局设置。 类似的相关方法还有 vim.wo 设置 window-local 选项、vim.bo 设置 buffer-local 选项等。 对 tab 进行设置, tab 默认为 4 个空格。 lua vim.o.tabstop = 4 vim.bo.tabstop = 4 vim.o.softtabstop = 4 vim.o.shiftround = true 还可以将其他的基础配置添加进来,例如显示行号等,具体配置如下 lua -- 编码方式 utf8 vim.g.encoding = "UTF-8" vim.o.fileencoding = "utf-8" -- jkhl 移动时光标周围保留8行 vim.o.scrolloff = 8 vim.o.sidescrolloff = 8 -- 显示行号 vim.wo.number = true -- 使用相对行号 vim.wo.relativenumber = true -- 高亮所在行 vim.wo.cursorline = true -- 显示左侧图标指示列 vim.wo.signcolumn = "yes" -- 右侧参考线 vim.wo.colorcolumn = "160" -- 缩进字符 vim.o.tabstop = 4 vim.bo.tabstop = 4 vim.o.softtabstop = 4 vim.o.shiftround = true -- >> << 时移动长度 vim.o.shiftwidth = 4 vim.bo.shiftwidth = 4 -- 空格替代 vim.o.expandtab = true vim.bo.expandtab = true -- 新行对齐当前行 vim.o.autoindent = true vim.bo.autoindent = true vim.o.smartindent = true -- 搜索大小写不敏感,除非包含大写 vim.o.ignorecase = true vim.o.smartcase = true -- 搜索不要高亮 vim.o.hlsearch = false vim.o.incsearch = true -- 命令模式行高 vim.o.cmdheight = 1 -- 自动加载外部修改 vim.o.autoread = true vim.bo.autoread = true -- 禁止折行 vim.wo.wrap = false -- 光标在行首尾时<Left><Right>可以跳到下一行 vim.o.whichwrap = "<,>,[,]" -- 允许隐藏被修改过的buffer vim.o.hidden = true -- 鼠标支持 vim.o.mouse = "a" -- 禁止创建备份文件 vim.o.backup = false vim.o.writebackup = false vim.o.swapfile = false -- smaller updatetime vim.o.updatetime = 300 vim.o.timeoutlen = 500 vim.o.splitbelow = true vim.o.splitright = true -- 自动补全不自动选中 vim.g.completeopt = "menu,menuone,noselect,noinsert" -- 样式 vim.o.background = "dark" vim.o.termguicolors = true vim.opt.termguicolors = true -- 不可见字符的显示,这里只把空格显示为一个点 vim.o.list = false vim.o.listchars = "space:·,tab:>-" vim.o.wildmenu = true vim.o.shortmess = vim.o.shortmess .. "c" -- 补全显示10行 vim.o.pumheight = 10 vim.o.clipboard = "unnamedplus" 📦插件系统 vim/neovim 经过多年的发展仍旧活跃,其中插件系统功不可没。丰富的插件可以使得 neovie 分分钟化身为 IDE 。 在社区的发展过程中,vim 的插件系统也不断的壮大,目前 vim 的插件基本可以涵盖编辑的方方面面。针对 neovim,可以通过 awesome-neovim 这个项目查询相关插件。 无论是 vim 还是 neovim,本身并没有插件管理器,并不像 vscode 或者其他编辑器一样,可以方便的对查件进行添加,删除或者更新等操作。当然你也无需要担心,各路大神已经开发出了多个插件管理工具。目前,对于 neovim 来说,比较有名的插件管理工具有 packer.nvim 和 lazy.nvim (注意:不要和 LazyVim 混淆)。 本文采用将会采用 lazy.nvim 作为插件管理工具进行插件管理。如果你使用了 packer.nvim 作为插件管理,并不影响阅读,可以跳过插件管理器章节。 插件管理器 lazy.nvim 作为现在比较流行的插件管理工具,安装其实很简单。在 init.lua 中添加相关代码。 lua local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim" if not vim.loop.fs_stat(lazypath) then vim.fn.system({ "git", "clone", "--filter=blob:none", "https://github.com/folke/lazy.nvim.git", "--branch=stable", -- latest stable release lazypath, }) end vim.opt.rtp:prepend(lazypath) require("lazy").setup() 保存退出后,再次进入 nvim。 nvim 会检查是否存在 lazy, 如果不存在会从 github 上 clone 下来。 通过 :Lazy 命令,如果看到下面的图像,那么就证明 lazy.nvim 安装成功了。 有了 lazy,之后就可以快速的安装、更新和卸载插件。 第一个插件 为了验证 lazy 功能是否正常,那么先安装个主题插件看看。主题插件我选择 catppuccin.nvim。 通过 catppuccin 文档可以查到如何安装,将插件添加到 lazy 配置中。 lua require("lazy").setup( { "catppuccin/nvim", name = "catppuccin", priority = 1000 } ) 退出 neovim,再次进入,就会看到 lazy 在下载相关插件。 安装完成后,发现我们的主题已经被应用,但是等到退出 neovim 再次进入的时候,发现主题颜色又恢复了默认配色,这里我们只需要在配置文件末尾添加一句,用于设置相关的配色。 lua vim.cmd.colorscheme("catppuccin") 其中 catppuccin 还有很多配置,这里不多赘述,可以到相关仓库中查看配置内容。 有了插件管理器,那么就可以添加不同的插件了。 配置的组织 经过上面简单的了解,目前所有的配置都写在 init.lua 中,当添加更多的插件的时候, lua 中的代码也会不断的增加,那么当需要进行修改的时候,查询相关配置也成了一大问题,而通过 lua 的特性,我们可以将不同的配置文件进行拆分,做到高内聚低耦合。 对于 lua 来说,可以通过 require 函数来将不同的 lua 脚本进行导入。 现在同 init.lua 目录下建立 lua 文件夹,在其中新建两个 lua 文件,分别是 basic.lua 和 plugins.lua。 text . ├── init.lua └── lua ├── basic.lua └── plugins.lua 结构如上目录树所示,之后可以将配置内容分被复制到相关的文件中,将基础配置放在 basic.lua 中,将 lazy 相关的配置复制到 plugins.lua 中。 最后将 init.lua 中引入相关的配置文件即可。 lua require("basic") require("plugins") 其中,对于颜色主题的配置我们可以新建一个 theme.lua 文件,将主题配色相关的配置存放,最后不要忘记在 init.lua 中添加该文件。 最后的目录结构如下: text . ├── init.lua ├── lazy-lock.json └── lua ├── basic.lua ├── plugins.lua └── theme.lua ⌨️ 快捷键绑定 为了让 vim 更加的顺手,方便我们的使用,针对一些操作需要绑定我们的快捷键,这样让你的操作效率如虎添翼。按照上面的配置规则,新建文件为 .lua,并在 init.lua 中添加。 认识快捷键 vim 的一个重要点就是可以通过快捷键快速高效的完成任务,指哪里打哪里,而在配置快捷键中,其实有怎么几个键需要我们熟知,分被为 Ctrl 、 Alt 和 Shift 键。这里并不是让大家知道这些按键在哪里,而是说配置中,这些键位很常用,并且在配置中这些键位常常简写。 键位 简写 Ctrl C- shift S- Alt A- 这三个键位在配置中会非常常见。当然,并不是只有这三个键位可以找到相关的缩写,可以通过 :h key-notations 看到所有的键位说明。 另外,对于 mac 来说是没有 Alt 键的,这样就需要我们修改 Option 键位,这部分可以看 附录/修改 Alt 键位。 Leader 键 leader 键对于 vim 来说是一个非常重要的键,它可以说是在 vim 中使用频率最高的键之一。 leader 顾名思义,处于领导位置,一般作为组合快捷键的先驱,使用其他按键的时候先按下 leader。对于 leader,vim 并没有规定谁是 leader,只要你想,哪个键也可以为 leader。一般会将 空格 等按键设置为 leader 键。 上面这些都是我们配置前的基础知识。下面开始设置相关快捷键。 设置快捷键 在 lua 文件夹下新建文件, keybindings.lua,并且在 init.lua 中添加, require("keybindings") 首先便是上面说过的 leader 键。这里我采用空格作为 leader 键。 lua -- keybindings.lua vim.g.mapleader = " " vim.g.maplocalleader = " " 关于 mapleader 和 maplocalleader 的区别可以看 https://luciaca.cn/posts/vimscript-learning-on-leaders 相关文档。 在 neovim 中,需要通过 vim.keymap.set() 函数进设置。这个函数需要传入四个参数,分别是 mode , lhs , rhs , opts 。 参数名 说明 mode 模式的简写,常见的有 n(normal), i(insert), v(view) 等 lhs 可以理解为对应的按键 rhs 对应的功能 opts 相关的设置 lua vim.g.mapleader = " " vim.g.maplocalleader = " " local opt = { noremap = true, silent = true } -- visual模式下缩进代码 vim.keymap.set("v", "<", "<gv", opt) vim.keymap.set("v", ">", ">gv", opt) -- 左右Tab切换 map("n", "<C-h>", ":BufferLineCyclePrev<CR>", opt) map("n", "<C-l>", ":BufferLineCycleNext<CR>", opt) -- 省略其他基础配置 这样就可以自定义添加相关按键。 我的相关配置,提供给大家参考 youngxhui/nvim。 附录 修改 Alt 键位 iterm2 在 setting 中, Profiles-Keys 中, 将 Left Option Key 设置为 Esc+ 。 Alacritty 编辑 alacritty 的配置文件中window.option_as_alt yaml window: # Make `Option` key behave as `Alt` (macOS only): # - OnlyLeft # - OnlyRight # - Both # - None (default) option_as_alt: Both Ghostty Ghostty 是最近发布的一款终端模拟器。 需要在配置文件中新增 macos-option-as-alt ini macos-option-as-alt = true 参考资料 为何 Emacs 和 Vim 被称为两大神器 BurntSushi/ripgrep
在 HTTP 标准库 中解释了 go 的标准库是如何处理请求的,但是通过源码的分析,可以发现,标准库对于这部分的处理比较简单,例如对 url 中携带参数就不支持,面对这种情况,社区中出现了大量的框架,对原有的 http 进行补充。 大部分的 Go 的 HTTP 框架都是在重写路由部分,已实现更快的更准确的路由查找,减少路由解析过程中消耗的时间,来提高框架的处理速度。 标准库 HTTP 的处理 先来回顾一下标准库的 HTTP 处理流程。 启动 HTTP 服务器:使用 http.ListenAndServe 或 http.ListenAndServeTLS 函数启动 HTTP 服务器。 处理请求:当服务器接收到 HTTP 请求时,它会使用与路径相对应的 http.Handler 实现处理请求。 调用处理程序:服务器会调用 ServeHTTP 方法,并将请求相关的信息作为参数传递给该方法。 路由匹配:在 ServeHTTP 方法中,通过比较请求的路径和已注册的路由,找到与请求匹配的路由。 调用处理函数:如果找到了匹配的路由,则调用与该路由相关的处理函数。 写入响应:处理函数通过 ResponseWriter 接口写入响应数据,以返回给客户端。 根据上述的处理方式,目前需要关注两个函数:ServeHTTP 和 ResponseWriter 。这是两个主要的处理方式。 http.ListenAndServe 和 http.ListenAndServeTLS 用于启动 HTTP 服务器;以及 http.ResponseWriter 和 http.Request 分别用于写入响应和处理请求。 同时通过源码分析,如果在 Listen 的时候不传入 handler, 那么就会采用 http 默认的 Handler,也就是 ServeMux ,所以只要我们按照标准实现一个 Handler,那么就会替换原有的处理逻辑,而且 Handler 的实现也是非常简单,只要实现 ServeHTTP 这个接口即可。 gin 启动 采用 gin 实现一个最简单的 ping/pong 服务。这是官方 README 提供的例子。 Go package main import ( "net/http" "github.com/gin-gonic/gin" ) func main() { r := gin.Default() r.GET("/ping", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "message": "pong", }) }) r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080") } 例子中实现也是非常简单,先通过 gin.Default() 生成一个 engine。 之后添加相关的处理方式,最后用 r.Run() 启动。下面将会从最开始的方法开始剖析 gin 框架。 Engine Default 是 gin 使用的第一个函数。这个函数看起来很简单,一共就五行代码。 Go func Default() *Engine { debugPrintWARNINGDefault() engine := New() engine.Use(Logger(), Recovery()) return engine } 其中主要通过 New() 函数初始化 Engine 结构体。在 gin 中,通过 Engine 这个结构体进行管理,这个结构体其实是实现了 ServeHTTP 这个方法。 在这里 Engine 的功能和标准库中的 ServeMux 的地位其实是一模一样的,那么他的主要功能也就是用来保存注册的 handler,等到使用的时候进行查找调用。那么就先看看路由和 handler 是如何注册的。 注册路由 在上面的例子中, 路由和 handler 是通过 r.GET() 方法进行注册。从源码来看,可以发现不仅仅是 GET 方法,其他的请求方法也一样,都是直接调用了 group.handler 方法。 Go func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes { // 计算绝对路径 absolutePath := group.calculateAbsolutePath(relativePath) // 合并 handler handlers = group.combineHandlers(handlers) // 添加相关路由 group.engine.addRoute(httpMethod, absolutePath, handlers) return group.returnObj() } 从代码中可以看到这个方法,其实是比较简单,从代码上的命名来看,基本进行了以下操作:先进行绝对路径的计算,之后对 handler 进行合并,最后添加路由。 为什么要 Combine 在combineHandlers方法中,首先计算了当前handlers的长度,并判断是否超过了最大长度(当前最大长度为63)。如果超过最大长度,则会引发panic异常。在这里,源代码采用了两次复制(copy)操作。第一次是将group.Handlers中的数据复制到mergedHandlers中,第二次是将handlers的数据传入mergedHandlers中。 Go func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain { finalSize := len(group.Handlers) + len(handlers) // ... mergedHandlers := make(HandlersChain, finalSize) copy(mergedHandlers, group.Handlers) copy(mergedHandlers[len(group.Handlers):], handlers) // ... } 这样 mergeHandler 中就存在两部分数据, handlers 可以通过源码看出来是在项目中注册的 handler,那么 group.Handlers 中又是什么呢? 其实就是在项目启动的时候注册的中间件。在 Engine 中的 Default 方法可以看到,engine.Use(Logger(), Recovery()) 项目在初始化的时候注册了两个中间件,而这个 Use 方法,其实就是将中间件添加到上面的 group.Handlers 中,这里不多赘述,只是简单的说明一下,具体的中间件的流程会在 中间件(Middleware) 章节讲述。 确切地说,mergeHandlers的目的是将中间件和用户定义的处理函数合并为一个处理函数切片,并按照一定的顺序注册到路由中。 添加路由 路由添加方法也比较简单,核心代码一共就 6 行。 Go root := engine.trees.get(method) if root == nil { root = new(node) root.fullPath = "/" engine.trees = append(engine.trees, methodTree{method: method, root: root}) } root.addRoute(path, handlers) 先通过请求方法获取树的根节点,如果根节点不存在就创建一个,最后添加相关的路由和 handlers。这里关于路由如何添加,路由数据结构在路由(Router)章节会进行讲解。 直到目前,路由的注册工作已经完成。 如何Run 在之前详细的了解过 go 的 net/http 包中是如何启动一个 http 服务之后,其实现在回过头来看 gin,其实一切变得很简单。 在 gin 的 Run 方法中,主要通过标准库的 http.ListenAndServe 方法启动,而这个方法在 HTTP 标准库中有过详细的分析 ,剩下的方法流程和标准库中的流程基本一致,唯独不同的一点是将原有的默认 Handler 换成了 gin.Handler。 在之前说过,要想成为一个 Handler,只要实现 ServeHTTP 方法即可,而 gin 的 engine 就实现了这个方法。 根据之前对 http 了解的处理流程来看,在 gin 收到相关的请求,都会统一调用 ServeHTTP 方法,该方法会将接收到的参数等进行处理,例如寻找合适的处理器(Handler),最后返回统一的处理结果。 Go // ServeHTTP conforms to the http.Handler interface. func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { c := engine.pool.Get().(*Context) c.writermem.reset(w) c.Request = req c.reset() engine.handleHTTPRequest(c) engine.pool.Put(c) } 首先使用到的变量有 pool。这里的 pool 使用的 sync.Pool 这个类型,主要是用来重复使用的 Context。这里直接从 pool 中取出 Context,并对 Context 的一些参数进行设置,最后调用 engine.handleHTTPRequest 方法。 这也是目前常常使用 engine.handleHTTPRequest 这个方法主要处理用户的 HTTP 请求,确定该请求的处理方法。简单的来说就:首先,获取请求的 HTTP 方法(如 GET 或 POST)和 URL 路径,并在需要时解码路径。然后,它搜索匹配该请求的处理树。如果找到了一个匹配的节点,它会将处理程序分配给请求的上下文(c),并写入 HTTP 响应头。如果未找到匹配的节点,则会通过 serverError 写入 “405 Method Not Allowed” 或 “404 Not Found” 的错误响应。 这样基本就是一个简单的 http 请求的处理过程。在代码中可以看到很多事情其实是由上下文 (Context) 进行处理的。
前言 Go 语言以其出色的并发性能和优雅的编程模型而闻名,对于 http 服务可以做到开箱即用,无需第三方框架,而且使用起来也很简单。即便如此,还会有很多 http 框架的诞生,例如 gin,echo 等,说明自带的 http 服务还有不完美的地方,导致了用户选择第三方开发的框架。 这是一个采用标准库开发的 http 服务。 Go package main import "net/http" func main() { http.HandleFunc("/ping", pingHandler) http.ListenAndServe(":8000", nil) } func pingHandler(w http.ResponseWriter, r *http.Request) { w.Write([]byte("pong")) } 这是一个简单的HTTP服务。在启动服务后,通过访问 http://localhost:8000/ping,可以访问相关的处理程序并返回响应信息:“pong”。 这段简单的代码中,直接运行了一个高性能的HTTP服务。代码中涉及了两个与HTTP相关的函数,分别是:http.HandleFunc和http.ListenAndServe。 接下来,对这两个函数进行解析,让您了解每个步骤的具体操作。 路由注册 HandleFunc函数的作用是将指定的处理函数handle注册到HTTP框架中,使得特定的URL路径和该处理函数建立映射关系。 以下是HandleFunc()函数的源代码实现 Go func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) { DefaultServeMux.HandleFunc(pattern, handler) } 在这段代码中,HandleFunc函数接收两个参数:pattern和handle。pattern是URL路径的模式,用于匹配请求的URL;handle是处理函数,接收ResponseWriter和Request作为参数,用于处理请求并生成相应的响应。 函数内部调用了DefaultServeMux.HandleFunc()函数,将pattern和handle注册到默认的 ServeMux(多路复用器)中,建立URL和处理函数之间的映射关系。 Lua func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) { if handler == nil { panic("http: nil handler") } mux.Handle(pattern, HandlerFunc(handler)) } HandleFunc函数是ServeMux类型的方法。ServeMux是一个HTTP请求多路复用器,用于将请求路由到相应的处理程序。在这个方法中,我们传入了一个pattern参数和一个handler函数参数。 Go type HandlerFunc func(ResponseWriter, *Request) // ServeHTTP calls f(w, r). func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { f(w, r) } HandlerFunc 的定义很简单,并且实现了 ServeHTTP 方法。这个方法主要是调用本身。 mux.Handler实现同样比较简单,可以看到将路由(pattern)和具体实现(handler)注册到 DefaultServeMux 这个对象上。通过不断的往下看源码,可以找到 ServeMux.Handler 这个方法上。这个方法主要是将服务中的路由进行注册。将服务注册到 ServerMux 对象上,也就是上文所提到的 DefaultServeMux。 对于 ServeMux 来说,是一个比较简单的结构。其中可以看到 m 和 es 保存了路由 Go var DefaultServeMux = &defaultServeMux var defaultServeMux ServeMux type ServeMux struct { mu sync.RWMutex m map[string]muxEntry es []muxEntry hosts bool } type muxEntry struct { h Handler pattern string } func (mux *ServeMux) Handle(pattern string, handler Handler) { // 省略代码 ... if mux.m == nil { mux.m = make(map[string]muxEntry) } e := muxEntry{h: handler, pattern: pattern} mux.m[pattern] = e if pattern[len(pattern)-1] == '/' { mux.es = appendSorted(mux.es, e) } if pattern[0] != '/' { mux.hosts = true } } 如果 ServerMux.m 为空,会进行初始化,之后将 handler 和 pattern 存放到 muxEntry 中,最后将 muxEntry 存放到 m 中,m 的 key 是 pattern。这里的 pattern 就是 url 路径。 在 ServeMux 中,mux.es 切片是用来保存以斜杠结尾的路由模式对应的 muxEntry 对象的。它的作用是在请求的 URL 中去掉末尾的斜杠后进行匹配,从而避免重复处理类似 /path 和 /path/ 这样的 URL。 例如,如果有两个路由模式分别为 /path 和 /path/,请求的 URL 为 /path/,如果没有 mux.es 切片,将会尝试匹配 /path 和 /path/ 两个路由模式,最终会选择匹配 /path/ 的路由模式进行处理。这会导致处理器被重复调用。而使用 mux.es 切片,请求的 URL 会被处理为 /path,只会匹配到 /path 这一个路由模式,避免了处理器被重复调用的问题。 因此,mux.es 切片的作用是为了提高 ServeMux 的匹配效率,避免重复处理请求。 所有的 url 和 Handler 的映射关系都是通过 map[string]muxEntry 进行保存。这样就会出现问题,稍微复杂一些的 url 就无法很好的匹配。这也就是为什么会有大量的 go web 框架,而这些框架都是改写路由的匹配算法。 关于 DefaultServeMux 是一个实现了 HandlerServe 接口的结构体。 上面就是一个主要的注册过程。 ListenAndServe 这个方法主要对端口进行监听。 Go func ListenAndServe(addr string, handler Handler) error { server := &Server{Addr: addr, Handler: handler} return server.ListenAndServe() } 从源码可以看到,这里需要一个 handler 参数,并且新生成一个 server 对象。通过调用 ListenAndServe 方法进行处理。 Go func (srv *Server) ListenAndServe() error { if srv.shuttingDown() { return ErrServerClosed } addr := srv.Addr if addr == "" { addr = ":http" } ln, err := net.Listen("tcp", addr) if err != nil { return err } return srv.Serve(ln) } 在 ListenAndServe 中,首先对服务的状态进行了判断,如果是 shuttingDown 就提示 http: Server closed。这里的 shuttingDown 主要是通过一个叫 atomicBool 进行判断的。咋一看以为是原子操作,仔细看其实是定义了一个 int32 类型,通过 int32 的原子操作保证了并发安全。 Go type atomicBool int32 func (b *atomicBool) isSet() bool { return atomic.LoadInt32((*int32)(b)) != 0 } func (b *atomicBool) setTrue() { atomic.StoreInt32((*int32)(b), 1) } func (b *atomicBool) setFalse() { atomic.StoreInt32((*int32)(b), 0) } 之后通过 net.Listen() 方法进行监听,这里对这个方法不做过多的赘述,之后通过 Serve 方法。 下面是主要的核心方法 Go ctx := context.WithValue(baseCtx, ServerContextKey, srv) for { rw, err := l.Accept() if err != nil { select { case <-srv.getDoneChan(): return ErrServerClosed default: } if ne, ok := err.(net.Error); ok && ne.Temporary() { if tempDelay == 0 { tempDelay = 5 * time.Millisecond } else { tempDelay *= 2 } if max := 1 * time.Second; tempDelay > max { tempDelay = max } srv.logf("http: Accept error: %v; retrying in %v", err, tempDelay) time.Sleep(tempDelay) continue } return err } connCtx := ctx if cc := srv.ConnContext; cc != nil { connCtx = cc(connCtx, rw) if connCtx == nil { panic("ConnContext returned nil") } } tempDelay = 0 c := srv.newConn(rw) c.setState(c.rwc, StateNew, runHooks) // before Serve can return go c.serve(connCtx) } l 为 net.Listener 对象,当每次接收到信息的时候,首先会进行一个错误判断。 如果是 down 的信号,就会直接返回相关错误,否则先对错误进行断言,检查是否为 net.Error,这个是一个接口,其中 Temporary 方法官方已经标记为启用,这个方法更多的表示为超时。如果有超时,你们就会对延时 tempDelay,进行增加,起初是 5 毫秒,之后每次增加 2 倍,最大为 1 秒钟,之后会进行重试。 通过 srv.ConnContext 会生成一个新的 ctx,否则就使用之前的 ctx,也就是 context.Backgroud()。然后开启一个协程进行服务。 在协程中,通过 readRequest 方法进行获取,返回 response。通过 response 对象获取 request。通过 request 判断请求是否要继续。 Go req := w.req if req.expectsContinue() { if req.ProtoAtLeast(1, 1) && req.ContentLength != 0 { req.Body = &expectContinueReader{readCloser: req.Body, resp: w} w.canWriteContinue.Store(true) } } else if req.Header.get("Expect") != "" { w.sendExpectationFailed() return } 这里判断首先通过 expectsContinue 方法,这个方法中获取请求头中的 Expect 字段是否等于 100-continue。当等于的时候要继续进行判断,其中请求的协议为 HTTP 1.1 和 ContentLength 不为 0。这样就可以获取到请求体。 当请求头中的 Expect 和上述条件不相同的时候,直接返回 417 错误。 之后创建了一个 serverHandler 并且调用了 ServeHTTP 并且传入了 response 和 request。 ServeHTTP 再一次出现,其中第一步就是获取 Handler。 Go handler := sh.srv.Handler if handler == nil { handler = DefaultServeMux } 那么,这里获取的 handler 应该是什么呢?经过多个方法或函数,可以已经对 sh.srv.Handler 一步一步的向上推到。这里我将这个过程画了一张图,图上箭头表示关系之间的依赖,红色表示持有 handler 数据。 通过这个依赖图可以看到,handler 是由最开始的 ListenAndServe 方法进行传入的,而我们的示例代码中这部分传入的是 nil,也就是从开始到现在 handler 一直为 nil。这也就是为什么会一个判断,当 handler 为空的时候使用 DefaultServeMux。其实关于默认的 handler 为 DefaultServeMux 这个事情,在 ListenAndServe 这个代码的注释中就已经说明。 之后就是调用 handler.ServeHTTP 方法,ServeHTTP 方法主要是一个请求分发的作用。源码中调用了 Handler 方法。这个方法主要的作用就是查找请求对应的 Handler 是那个。 Go // ServeHTTP dispatches the request to the handler whose // pattern most closely matches the request URL. func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) { if r.RequestURI == "*" { if r.ProtoAtLeast(1, 1) { w.Header().Set("Connection", "close") } w.WriteHeader(StatusBadRequest) return } h, _ := mux.Handler(r) h.ServeHTTP(w, r) } 先对方法为 CONNECT 的请求做了处理。通过 redirecToPathSlash 方法,这个方法主要是要判断给定的路径是否要加 /,而这个方法中加锁后调用了 shouldRedirectRLocked 这个方法。通过查找 ServeMux 的 m 这个属性中是否存在相关的路径。这个 m 在上文中介绍过,是一个 map 结构。这也就是为什么会加锁,这里的 map 是并发不安全的。查询的方式也很简单,就是在之前的 map 中查询是否存在,如果存在就返回 false,如果没有找到会进行一些特殊的处理,在路径上加上后缀 / 进行查找,同时为了防止路径为 //,所以在返回的时候又进行了一次判断。 在上述的方法结束后,会返回一个 bool 值,来确定是否需要添加末尾的 /。从而返回相关的 url。通过新的 url 生成一个 RedirectHandler 的结构体。如果没有找到通过 通过 handler 这个方法。 handler 这个方法通过 match 这个方法进行查找。 Go func (mux *ServeMux) match(path string) (h Handler, pattern string) { v, ok := mux.m[path] if ok { return v.h, v.pattern } for _, e := range mux.es { if strings.HasPrefix(path, e.pattern) { return e.h, e.pattern } } return nil, "" } 这个方法比较简单,现在 m 中进行查找,如果没有找到,从 es 中查找。之前我们说过 es 是保存了后缀有 / 的 handler。如果都无法查找到就返回 nil。 这里的 handler 就是在一开始注册的 HandleFunc(pingHandler) 。 最后在 ServeMux.ServeHTTP 这个方法中调用 handler 相关的 ServeHTTP。这样就调用成功了。 总结 通过两个方法基本可以做到路由的注册方式和路由的查询方式,并且对请求来临的时候相关的处理过程。这些方法对之后研究其他框架源码或者工作方式更加清晰。
说起来已经很久很久都没有写博客了。从毕业到工作,这段时间感觉很忙很忙,失去了自己思考的时间,每天感觉像流水线上的机器,不停的写代码写代码,自己本身却没有多大的提升。 新手不要使用终端 vim。 入门建议 之前断断续续的写过一些 vim 的相关文章,和断断续续的使用 vim。 之前一直想着把 vim 当作 IDE 使用。但是繁琐的配置和一些不敬如人意的地方,导致在使用过程中会逐步的放弃。 而最近两个月,我开始使用了 vim。使用 vim 来说,就像打字盲打一样,主要靠肌肉记忆,而不是思考。 不是把 vim 当作 IDE,而是把 IDE 当 vim。 目前,大多数的 IDE 都会有 vim 的相关 keymap 设置,而我目前使用的工具主要是 Vscode 和 Goland。 这两款工具都对 vim 有良好的支持。 Vim 的模式 针对 vim 上手困难,首先的一个困难我觉得来自于它的模式,说实话,从我接触电脑来说,无模式感觉是一种正常的情况。打开 word,可以直接进行编辑,打开 IDE,可以直接进行编码,这种情况对与我来说那就是理所当然,就应该是这样。 而这些对于 vim 来看,有显示的那么反直觉,打开后无从下手,但是当你学会编辑,需要移动的时候,不停的在 normal 和 insert 两个模式换来换去。 这些对于刚刚入手 vim 来说,都将成为放弃的理由。 命令 当你逐渐了解了 vim 的模式之后,剩下的就是 vim 的命令,或者说是快捷键。 首先就是移动,通过 h,j,k,l 四个键来进行光标移动,而不是我们习惯的方向键(虽然方向键也可以移动),熟练使用这四个键进行光标移动绝对是入门 vim 的重要点,刚刚开始的时候是在强迫自己使用这四个键来进行移动,下意识的练习才能使自己不断的掌握 vim 的基本操作。 除了光标的移动,那么剩下的最重要的功能就是编辑了。在编辑过程中,单单就插入来说就有多个快捷键,例如 i,I,a,A,o,O 等,都可以从 normal 模式转化为 insert 模式,但是每个命令又有细微的差别,例如 a 可以理解为 append ,那么就是在光标都追加单词,而 i 可以记忆为 insert ,表示为光标前添加。 为了熟练的掌握这些命令,在刚刚开始使用的时候,嘴里或者心里都会默念一些 “口诀” 。 例如我修改引号内代码,就念 change inner " 手上对应的 c,i,⇧ '。 但是还有一个问题,你需要知道这些快捷键,你才能在心中默念出相关“口诀”,vim的相关命令比较多,而且同一键在不同的情况下代表的含义是不一样的,比如上述提到的 i,可以是 insert,还可以是 inner。 这些操作都不需要任何的插件,也不需要任何的设置,对于 vim 来说,这些操作都是一致的。 配置 这里我贴出我的 vim 配置。分别是 vscode 和 goland 的。 Vscode json { "vim.easymotion": true, "vim.incsearch": true, "vim.useSystemClipboard": true, "vim.useCtrlKeys": true, "vim.hlsearch": true, "vim.insertModeKeyBindings": [ { "before": [ "j", "j" ], "after": [ "<Esc>" ] } ], "vim.normalModeKeyBindingsNonRecursive": [ { "before": [ "<leader>", "d" ], "after": [ "d", "d" ] }, { "before": [ "<C-n>" ], "commands": [ ":nohl" ] }, { "before": [ "K" ], "commands": [ "lineBreakInsert" ], "silent": true }, { "before": [ "leader", "=" ], "commands": [ { "command": "editor.action.formatDocument" } ] }, { "before": [ "leader", "g" ], "commands": [ { "command": "go.test.generate.function" } ] } ], "vim.leader": "<space>", "vim.handleKeys": { "<C-a>": false, "<C-f>": false }, "editor.lineNumbers": "relative" } Goland (IDEA) text "" Source your .vimrc "source ~/.vimrc let mapleader=" " "" -- Suggested options -- " Show a few lines of context around the cursor. Note that this makes the " text scroll if you mouse-click near the start or end of the window. set scrolloff=5 set so=5 "set ideajoin "set idearefactormode=keep "set relativenumber set nu set showmode set relativenumber set clipboard+=unnamed set clipboard+=ideaput set virtualedit=onemore "set cursorline set keep-english-in-normal " Do incremental searching. set incsearch " Don't use Ex mode, use Q for formatting. map Q gq set surround set multiple-cursors set commentary set argtextobj " set easymotion set textobj-entire set ReplaceWithRegister set exchange "" -- Map IDE actions to IdeaVim -- https://jb.gg/abva4t "" Map \r to the Reformat Code action "" Map <leader>d to start debug "map <leader>d <Action>(Debug) "" Map \b to toggle the breakpoint on the current line "map \b <Action>(ToggleLineBreakpoint) " map <leader>f <Plug>(easymotion-s) " map <leader>e <Plug>(easymotion-f) nmap <leader>d <Action>(Debug) " Debug 当前方法 map <leader>r <Action>(RenameElement) " 重命名变量 noremap <leader>c <Action>(Stop) noremap <leader>z <Action>(ToggleDistractionFreeMode) " 禅模式 map <leader>s <Action>(SelectInProjectView) map <leader>a <Action>(Annotate) map <leader>h <Action>(Vcs.ShowTabbedFileHistory) map <S-Space> <Action>(GotoNextError) map <leader>b <Action>(ToggleLineBreakpoint) " 打断点 map <leader>o <Action>(FileStructurePopup) " Editor nmap <leader>= <Action>(ReformatCode) " Find more examples here: https://jb.gg/share-ideavimrc " 全局查找 noremap g/ <ESC>:action FindInPath<CR> noremap / <ESC>:action Find<CR> noremap <leader>g <ESC>:action Generate<CR> set ideajoin
youngxhui blog,🌟追求成为一名 indie hack,🚀创造属于自己的编码魔法。专注技术,喜欢探索新鲜事物,对于 golang, neovim 等会做一些分享。
如何脱离 xcode 开发 iOS 是很多人在探索的方案。毕竟 xcode 编辑体验实在是太差了。 这里说的是脱离 xcode,而不是脱离 macOS。 SweetPad 这是今天要安利的工具 SweetPad。SweetPad 是 vscode 上的一个插件,插件可以实现在 vscode 中进行自动补全、调试、编译和运行、格式化、测试等功能。常见的开发场景中基本可以脱离 xcode 来使用。
在使用 Neovim 之前,我也花费了大量的时间进行配置自己的 Neovim 体系,无论是插件,快捷键还是 UI,都花费了大量的心思。 但是维护一套自己的配置说实话很费时间,例如插件的更新,版本升级等。 LazyVim 是一款已经 集成了大量插件的 Neovim 软件。 基本做到了开箱即用,方便快捷,大量的默认配置减少了上手时间,唯一的缺点可能就是快捷键不是自己熟悉的那套,需要修改或者适应。 安装 LazyVim 的安装其实很简单,首先打开官方的启动模板 LazyVim/starter。 点击右上角 Use this template ,将模板 fork 成为自己的仓库。 接下来就是备份本地原有的配置,将自己的仓库 clone 下来。 简单来说就是一个命令进行备份 另外一个命令进行 clone。 这部分可以直接参考官方网站对于安装的介绍文档。 安装完成之后,启动就是一个已经有了大量基础配置的 neovim 了,基本做到了开箱即用。 完成 LazyVim 的配置,记得看一下是否系统安装了 fzf。 配置说明 在 LazyVim 的项目中,配置可以分成两大块:基础配置(config) 和 插件(plugins)。 config 配置 config 配置主要是一些基础的配置,文件夹内区分了四个文件, 分别是基础配置(options.lua),快捷键配置(keymaps.lua),自动化命令(autocmds.lua)和 lazy 初始化配置。 plugins 配置 默认 LazyVim 已经集成了很多插件,有的已经默认开启,有的需要手动开启。如果需要覆盖原有的插件配置或者安装新的插件, 可以在该文件夹下直接填写相关配置,最后在外层的 init.lua 中添加即可。 快捷键 LazyVim 已经集成了大量的插件,并且默认了很多快捷键。默认的 Leader 为 空格。可以针对自己的习惯修改快捷键,也可以直接按下 Leader 来查看快捷键。 默认的快捷键我认为是比较合理,而且使用几次基本也会记住,这里我除了修改了一下 blink 候选提示的上下选择之后,并没有做其他修改。 默认的快捷键也可以在官方文档中找到 keymaps LazyExtras LazyVim 一些默认的插件已经启用,但是除了默认插件,还有一些其他插件可以根据具体的情况按需使用。 通过 :LazyExtras 命令可以查看其他扩展插件。如果有自己使用的,需要的可以直接通过 x 进行启用。 需要注意的是启用后需要重新启动一些 neovim。 自动命令 LazyVim 中自带了一些配置,例如 tab 为 2,在一些缩进比较多语言中,tab 为 2是比较友好的,例如 lua,但是对于一些项目,尤其是很团队合作的项目, tab 改成2 就会让代码一团乱。 在 options.lua 中新增一些 tab 相关的配置。 lua local opt = vim.opt opt.expandtab = true opt.tabstop = 4 opt.shiftwidth = 4 opt.softtabstop = 4 同时为了让 lua 还保持 tab 为2的缩进,在 autocmds.lua 中新增自动化命令。 lua vim.api.nvim_create_autocmd("FileType", { pattern = { "lua" }, callback = function() vim.opt_local.expandtab = true vim.opt_local.tabstop = 2 vim.opt_local.shiftwidth = 2 vim.opt_local.softtabstop = 2 end, }) 还有一个就是中文拼写问题,默认的 spell 一直检查,尤其是在进行 markdown 的时候,大量的波浪线导致编辑的感官太差,可以新增命令来解决。 lua vim.api.nvim_create_autocmd("FileType", { pattern = { "markdown", "txt" }, callback = function() vim.opt_local.spell = false end, }) vscode 使用 LazyVim LazyVim 对 vscode 支持比较好,并且 vscode-neovim 也推荐使用 lazyvim。通过这个插件,可以让我们在 Vscode 中加载一些 Neovim 的插件,做到一个配置两处使用。 首先需要在 LazyExtras 中打开 Vscode 。 然后在 Vscode 中安装 Vscode Neovim 插件。 在 LazyVim 中可以通过 vim.g.vscode 来判断当前的运行环境。 首先是对快捷键进行配置,比如常用的重命名、格式化等。 lua local map = vim.keymap.set if vim.g.vscode then map("n", "<leader>cf", "<Cmd>lua require('vscode').call('editor.action.formatDocument')<CR>", { desc = "Format" }) map("v", "<leader>cf", "<Cmd>lua require('vscode').call('editor.action.formatSelection')<CR>", { desc = "Format" }) map("n", "<leader>cr", "<Cmd>lua require('vscode').call('editor.action.rename')<CR>", { desc = "Rename" }) end Golang 开发 针对 Golang 开发,首先就是语言的相关配置: LSP:通过 Mason 安装 gopls; Testing: 通过 LazyExtras 进行安装 test.core; DAP: 通过 Mason 安装 dlv。 安装完成就可以进行编辑,运行,测试和调试。 可以通过 <Leader>tr 进行测试运行。 通过 <Leader>db 进行打断点。 通过 <Leader>dc 进行 Debug 运行。
CAP 理论 在分布式系统中最重要的一条理论为 CAP 理论。这个理论是由加州大学伯克利分校的计算机科学家 Eric Allen Brewer 在 2000 年提出的一个猜想,由 2002 年,麻省理工两位科学家发表了该猜想的证明,使得该猜想变成了一个定理。 CAP 定理中对分布式系统提出了三点,分别为: 一致性(Consistency):多个副本的数据之间能够保持一致; 可用性(Availability):每次请求都可以获取到正常的、非错误的响应,但是无法保证数据是最新的; 分区容错性(Partition tolerance):系统的某个节点发生故障,仍然还可以对外提供一致性和可用性的服务。 例如当前系统有两个 DB 分别为 DB0 和 DB1。 一致性 C 一致性是指在数据发生变化的时候(也就是写操作发生后),无论是谁获取到的数据(也就是读操作)也是一样的。 当用户1 通过写操作对 DB0 的数据进行修改后,那么无论用户1 还是 用户2 ,无论从 DB0 还是 DB1 读取,读取后的数据都应该是完全一样的,这就是所谓的一致性。 也就是 DB0 的数据发生了修改,应该由相关的机制告诉 DB1 也将相关的数据发生修改,保证该数据在不同的 DB 中是一样的。 可用性 A 当用户发出相关请求,无论 DB0 还是 DB1 都会返回相关的数据,但是这里不需要关心数据是否一致。 分区容错性 P DB0 和 DB1 例如出现了问题,这个问题可能是网络问题,也有可能是其他硬件问题,导致了 DB0 和 DB1 的系统无法通信。这样 DB0 和 DB1 就成为了两个分区。即使 DB0 和 DB1 无法进行通信,但是 DB0 和 DB1 仍旧可以对外提供服务。 但是这种情况,在实际系统中无法避免这种情况,所以分区容错性是一个必选的条件。 CP 还是 AP 既然 CAP 三条规则无法同时满足,那么就出现了上图中的三种情况,满足任意两条规则,也就是 CA,CP 和 AP 三种架构,但是分区容错性是必选的,这样我们就剩下 CP 和 AP 两种关系。 常见的 CP 软件有 Zookeeper,Zookeeper 为了保证数据的一致性,牺牲了可用性。任何时候 Zookeeper 的访问都能获取一致的结果,但是不保证每次服务请求都可以用。 而 AP 架构中,要求数据一致性并不是那么重要,允许不同的服务可以返回不同的数据。 CAP 的不足 CAP 理论并不是完美的,存在很多问题。例如 DB0 和 DB1 要保持数据的一致性,那么就会发生相关通信,通信是需要时间,这就导致了某些时刻数据是不同步的,常见的情况在主从的机器上的主从延迟,当延迟过大的时候,用户读取的数据是不一致的。 CAP 理论也并不完全是三选二(或者说二选一)的问题,例如分区容错性发生的概论很小,那么就没必要牺牲了 A 和 C。 BASE 理论 BASE 理论算是 CAP 理论的延伸,是对 CAP 理论中一致性和可用性的权衡。在 CAP 中,所谓的一致性是指数据时时刻刻的都保持一致,也就是强一致性。上文中 CAP 的不足也说到,要保证时时刻刻数据的一致性是一件很困难的事情。而 BASE 理论就是对改问题的补充,既然很难做到强一致性,那么系统根据自身的业务特点,确保系统中的数据保证最终一致性也是可以的。 BASE 理论是 ebay 工程师 Dan Pritchett BASE 是指 Basically Available, Soft State 和 Eventually Consistent 三个短语的缩写。 Basically Available 基本可用 基本可用指的是系统出现故障后,但是还可以使用,但是可能比较正常系统上可能出现一些问题,例如响应时间上,服务降级牺牲部分功能等。 Soft State 软状态 软状态指的是系统数据允许出现中间状态,例如数据库主从同步过程中会出现中间状态,这就是一种软状态。 Eventually Consistent 最终一致性 最终一致性强调经过上述的软状态后,最后数据保持一致性。 BASE 理论的提出是通过牺牲系统的强一致性来保证系统的可用性,允许系统在一段时间内的数据是不一致的,但是要求最终数据的一致性。 参考文章 一文看懂|分布式系统之CAP理论 分布式架构之CAP理论/AP架构/CP架构
neovim入门指南(三):LSP配置 上 中说了 lsp 是什么如何配置和启动。那么接下来就完成 lsp 的其他配置。本章节主要介绍下面几个方面的介绍:代码高亮,文件格式化,lsp 相关的 UI 美化。 ⚠️错误警告提示 在有了 lsp 之后,当我们编写代码发生错误的时候,就会有相应的提醒。如图下提示 目前的错误还是字母,例如警告是W 。不太美观。对于这个来说,vim 开放了相关的接口,可以通过接口进行设置, neovim-diagnostic。 仍旧在 lsp 的文件夹下建立 ui.lua 目录。 lua -- lsp/ui.lua -- 对错误警告的图标 vim.diagnostic.config({ virtual_text = true, signs = true, -- 在输入模式下也更新提示,设置为 true 也许会影响性能 update_in_insert = true, }) local signs = { Error = "", Info = "", Hint = "", Warn = "" } for type, icon in pairs(signs) do local hl = "DiagnosticSign" .. type vim.fn.sign_define(hl, { text = icon, texthl = hl, numhl = hl }) end 其中 vim.diagnostic.config 是在配置提示文本,virtual_text 为错误信息提示. 🔅 代码高亮 目前的设置中,对于代码的高亮并不完美,需要采用一个插件了完成。tree-sitter 是一个用 rust 编写的高性能代码高亮的渲染工具,很多编辑器会采用它作为高亮功能的实现,比如说 Zed 。基于 tree sitter ,可以让我们的 neovim 的高亮更加完美。 这是来源于 nvim-treesitter 的一张图,图中左侧为未启用 treesitter 的代码高亮,右侧为启用后的效果。启用后的效果还是很明显的。 安装 tree-sitter 后,可以通过 TSInstall 来安装具体的语言高亮,例如 Rust 的高亮,可以直接使用 TSInstall rust 命令,或者使用 TSUpdate rust 命令进行更新。整体安装和配置还是比较简单的,这里不过多的解释。 📃 格式化 代码格式化是一个很重要的功能,也是我们使用频率很高的功能。针对 Neovim 的代码格式化有很多种方式。这里主要介绍 null-ls。不幸的是,null-ls 已经归档来,不再进行维护,万幸的是,它的 fork 项目 none-ls 重新维护起来,而且目前还兼容 null-ls 的配置,如果你之前使用的是 null-ls,那么只需要将依赖地址由 jose-elias-alvarez/null-ls.nvim 改为 nvimtools/none-ls.nvim 即可。 安装好 none-ls,后可以通过 Mason 安装相关的 formatter,例如安装 lua 的 formatter。通过命令 :Mason 打开安装界面。 选择 stylua,进行安装。安装完成后就可以进行配置,在 lua/lsp 下新建文件,命名为 nonels.lua。 lua -- lsp/nonels.lua -- 即使是采用了 none-ls, 这里也是获取 null-ls local status, null_ls = pcall(require, "null-ls") if not status then vim.notify("没有找到 null-ls") return end local formatters = null.builtins.format null_ls.setup({ sources = { -- Stylua formatters.stylua, -- 其他 formatter 方式 }, }) 这样在我们进行编写代码的时候,进行相关格式化。在上一文中也进行了介绍,对快捷键进行了绑定。使用 <Leader>= 进行格式化。 🎨 自动补全美化 关于前面,介绍了通过 cmp 插件进行了自动补全,通过 cmp 可以补充不同来源的代码。 我们使用一些 IDE 或者编辑器的时候,在补全的选项中是会展示一些图标,用来标识补全项的类型,例如变量、类名、方法或是接口等。 RustRover 自动补充样式。 VS Code 自动补全样式。 通过配置,也可以让 neovim 的提示实现上图效果。 当前自动补全的效果如图,前面为补全的字段,后面通过文字来标识补全的类型。 有一个插件叫做 lspkind ,可以通过安装该插件结合我们的 cmp 完成上述的样式。 在 lsp 下新建文件 kind.lua,配置 kind,这个可以在 lspkind,找到相关配置。 lua -- lsp/kind.lua local lspkind = require("lspkind") lspkind.init({ mode = "symbol_text", preset = "codicons", symbol_map = { Text = "", Method = "", Function = "", Constructor = "", Field = "", Variable = "", Class = "", Interface = "", Module = "", Property = "", Unit = "", Value = "", Enum = "", Keyword = "", Snippet = "", Color = "", File = "", Reference = "", Folder = "", EnumMember = "", Constant = "", Struct = "", Event = "", Operator = "", TypeParameter = "" }, }) 这里可以配置每个类别的图标,配置完成后在 cmp 中进行配置。主要是修改提示的样式。这里主要参考了 Github 上的一个样式,。在 formatting 中进行配置。 lua -- cmp.lua cmp.setup({ -- 省略其他代码 formatting = { completion = { border = { "╭", "─", "╮", "│", "╯", "─", "╰", "│" }, scrollbar = "║" }, documentation = { border = { "╭", "─", "╮", "│", "╯", "─", "╰", "│" }, scrollbar = "║", }, format = lspkind.cmp_format({ mode = "symbol", maxwidth = 20, ellipsis_char = "...", before = function(entry, vim_item) -- Get the full snippet (and only keep first line) local word = entry:get_insert_text() if entry.completion_item.insertTextFormat == types.lsp.InsertTextFormat.Snippet then word = vim.lsp.util.parse_snippet(word) end word = str.oneline(word) if entry.completion_item.insertTextFormat == types.lsp.InsertTextFormat.Snippet and string.sub(vim_item.abbr, -1, -1) == "~" then word = word .. "~" end vim_item.abbr = word return vim_item end, }), } -- 省略其他代码 }) 完成后的样式如图,基本和 VS Code 一样。 lspsage lspsage 可以极大的提高 nvim lsp 的体验。lspsage 的功能基本都是在增强和美化原有的 lsp 。具体的文档可以查看 nvimdev。 lspsage 主要功能: Finder: 用于高级 LSP 符号搜索的 UI 上面来自于官方文档的介绍。 Finder Finder 的功能是一个类似 VS Code 的变量/函数 查看的界面,可以在界面上看到这个变量/函数的定义和调用地方。 lua -- keybindings.lua -- 其他代码 pluginKeys.mapLSP = function(mapbuf) { -- 其他代码 mapbuf("n", "gf", ":Lspsaga lsp_finder<CR>", opt) -- 新增 -- 其他代码 } 这样在 normal 情况下按下 gf 就会出现上面的 finder 窗口。 Code Action Code Action 是一个非常重要的功能,可以根据代码上下文给出相关的提示或者代码改进。例如这块代码,Code Action 提醒我们可以直接转换为字面量的拼接。 Code Action 并不是 lspsaga 提供的,但是 lspsaga 给提供了一个漂亮的界面。在有 Code Action 的地方,会有一个黄色灯泡 💡 的提示。 下面绑定我们的快捷键。 lua -- keybindings.lua pluginKeys.lspKeybinding = function(mapbuf) -- 省略其他代码 -- code action -- mapbuf("n", "<leader>ca", ":lua vim.lsp.buf.code_action()<CR>", opt) -- 原有的 mapbuf("n", "<leader>ca", ":Lspsaga code_action<CR>", opt) -- 替换为这个 end Float Terminal lspsaga 提供了一个浮动终端窗口,可以在终端上执行任何命令。可以在 keybindings 中绑定这个快捷键,或者直接使用默认的快捷键 t ,来进行终端的打开和关闭。 lspsaga 还提供了很多功能,基本每个功能对我们都有帮助,这里就不做过多介绍了,大家可以看官方文档。 这里基本介绍完了 lsp 的常见功能和配置,剩下的大家可以通过需要进行安装和配置。 这里是我的配置 youngxhui/nvim 大家可以适当参考。 小结 vim/neovim 使用的时候,的确是有一个较高的上手成本,尤其是方向键的习惯,当完全习惯了 h,j,k 和 l 进行光标移动的时候,其实就离入门 vim/neovim 不远了。 当习惯了 vim 这一套风格之后,就会尝试把手头的编辑器/IDE 的快捷键都采用 vim ,而且在熟悉 vim 的快捷键后,真正的感觉到了手指在键盘上飞舞。 无论是 neovim 还是相关插件,更新迭代是很快的,也许当你看到这篇文章的时候,上述的很多配置都已经失效,或者相关插件已经停止维护,文章只是抛砖引玉,我相信你会找到合适自己的 nvim 配置。 我相信喜欢 vim/neovim 的人都是有一颗专研,喜欢折腾的心。正是这样的心才陪伴这个你我一步一步前进。 写到这里有感而发,下一篇我们将会介绍 DAP 。
🧩 什么是 LSP 对于一个编辑器来说,如果要完成例如自动补全,查找相关定义等功能是需要进行大量的开发的。不同的编辑器为了不同的语言都需要进行开发,而 LSP 的存在就是将这个过程检化。LSP 的全称为 Language Server Protocol,定义了编辑器和语言服务之间使用的协议。只要相关语言支持 LSP,那么编辑器只要符合相关要求实现即可完成例如自动补全等功能,而且不同的编辑器使用的体验是一致的。 目前支持 LSP 的编辑器有很多,例如大名鼎鼎的 Vscode。当然 vim 8 以后版本和 neovim 也都支持,具体支持的编辑器/IDE 列表可以看 LSP 的官方网站,同时支持 LSP 的语言也可以找到 支持语言。 neovim 已经是支持 LSP 了,具体可以在相关的配置文档看到,该文档详细的描述了如何配置一个 LSP。相对来说,配置过程比较繁琐,所以官方又提供了另一个库 nvim-lspconfig。接下来我们就通过这个插件来配置 neovim 的 lsp。 nvim-lspconfig 与安装其他插件是一样的,只需要我们在 plugins_config.lua 中添加相关配置即可,这里不进行赘述了。安装完成后其实进行配置就可以启用 LSP 了,就是这么简单。例如支持 rust 的 LSP,只需要进行简单的配置。 lua lspconfig.rust_analyzer.setup { settings = { ['rust-analyzer'] = {}, }, } 但是,nvim 只是 lsp 的客户端,那么就存在 lsp 的服务端。上面配置的 rust_analyzer 就是 rust 语言的服务端,就需要我们进行服务端的安装。rust_analyzer 的服务端地址是 rust-lang/rust-analyzer,需要将服务端下载并且安装好,这样每次编写rust的时候就会享受 lsp 的服务加成了。 但是这样做有几个问题,当然也不能算问题,只是不太方便。 对于多语言使用者来说,需要手动安装多个 lsp 服务端,目前 lsp 的服务端应该是没有统一的下载安装地址,需要手动寻找; 每次服务端进行更新,都需要重新下载安装; 新换设备之后,无法开箱即用,需要重复上述的方式重新开始一次。 面对上面的不方便,你可能已经想到很多解决方法,例如写个脚本进行一键安装和更新常用的 lsp 服务端。这样基本解决了上面说的所有问题。正如你想的那样,今天的第二位主角 williamboman/mason.nvim。 🗃️ mason.nvim mason 是一个可以方便的管理 LSP 服务端,DAP 服务端,Linter 和 格式化工具的插件。安装它之后,上面所说的问题将不是问题。 为了让 mason 和 nvim-lspconfig 更好的配合,这里还需要安装另一个插件 williamboman/mason-lspconfig.nvim 同样的安装这里不多赘述,主要是进行相关的配置。这里为了区别其他的插件,我们在 lua 目录下建立新的文件夹 lsp,用来专门存放 lsp 的配置。 配置 mason 首先还是加载我们的插件。在 lsp 文件夹中新建 mason.lua 文件,在文件中新增下面的配置。配置主要是在加载插件。 lua -- mason.lua local mason_status, mason = pcall(require, "mason") if not mason_status then vim.notify("没有找到 mason") return end local nlsp_status, nvim_lsp = pcall(require, "lspconfig") if not nlsp_status then vim.notify("没有找到 lspconfig") return end local mlsp_status, mason_lspconfig = pcall(require, "mason-lspconfig") if not mlsp_status then vim.notify("没有找到 mason-lspconfig") return end mason.setup() mason_lspconfig.setup({}) 📤 安装 lsp 服务端 配置完成后,重新启动 nvim,此时就可以采用 mason 进行 LSP 的服务端进行管理了。只需要按下 :Mason 即可。你将会看到如下的界面。 通过界面上的帮助可以看到如何使用,通过数字可以选择不同的服务端项目,2 为 LSP , 3 为 DSP 等。今天只是使用 LSP,可以直接按 2,选择到 LSP 界面,进行 LSP 安装。仍旧是通过 j 和 k 进行滑动。第一个安装的 lsp 服务端为 lua 语言的服务端:lua-language-server。这个是 lua 语言的语言服务,有 lsp 之后,我们之后无论是配置 nvim 还是编写 lua 都会有 lsp 服务的加持。按下 i 进行安装。 稍等片刻,安装完成。接下来就是配置,让 nvim 知道我们的 lsp 已经安装,在合适的时候进行启动。 lua -- mason.lua nvim_lsp.lua_ls.setup({ on_init = function(client) local path = client.workspace_folders[1].name if not vim.loop.fs_stat(path .. "/.luarc.json") and not vim.loop.fs_stat(path .. "/.luarc.jsonc") then client.config.settings = vim.tbl_deep_extend("force", client.config.settings, { Lua = { runtime = { version = "LuaJIT", }, workspace = { checkThirdParty = false, library = { vim.env.VIMRUNTIME, }, }, }, }) client.notify("workspace/didChangeConfiguration", { settings = client.config.settings }) end return true end, }) 这样 lua 的 lsp 就配置成功了,当我们编写 lua 脚本的时候,如果发生错误就会有相关提醒。当然这只是 lsp 最基础的功能,例如代码跳转,代码补全等需要我们进行配置。 基本所有的 lsp 的配置都可以在 server_configurations.md 中找到,当然 lua_ls 也不例外,上面的配置就是直接从文档中复制的 😄。 当前的 LSP 配置已经支持代码跳转,code action 等功能。例如查看当前变量或者函数的文档,可以使用这个命令 :lua vim.lsp.buf.hover()。 相关的命令还有其他 功能 命令 文档显示 :lua vim.lsp.buf.hover() 查看定义 :lua vim.lsp.buf.definition() 重命名 :lua vim.lsp.buf.rename() 查询实现 :lua vim.lsp.buf.implementation() 查询引用 :lua vim.lsp.buf.refreences() 查询声明 :lua vim.lsp.buf.declaration() 格式化 :lua vim.lsp.buf.format() Code action :lua vim.lsp.buf.code_action() 对于这些基础功能来说,每次需要的时候都在命令模式下敲一堆,速度的确是很慢的。所以,可以将上述的命令定义为快捷键,这样每次只需要进行快捷键进行完成上述功能。 ⌨️ 快捷键绑定 打开我们之前设置快捷键的配置文件 keybinding.lua,新增上述功能的配置。 lua -- keybinding.lua -- lsp 快捷键设置 pluginKeys.lspKeybinding = function(mapbuf) -- rename mapbuf("n", "<leader>r", ":lua vim.lsp.buf.rename<CR>", opt) -- code action mapbuf("n", "<leader>ca", ":lua vim.lsp.buf.code_action()<CR>", opt) -- go to definition mapbuf("n", "gd", ":lua vim.lsp.buf.definition()<CR>", opt) -- show hover mapbuf("n", "gh", ":lua vim.lsp.buf.hover()<CR>", opt) -- format mapbuf("n", "<leader>=", ":lua vim.lsp.buf.format { async = true }<CR>", opt) end 完成快捷键的配置,那么就可以将快捷键绑定到刚刚配置的 lsp 服务端了。 lua -- mason.lua function LspKeybind(client, bufnr) local function buf_set_keymap(...) vim.api.nvim_buf_set_keymap(bufnr, ...) end -- 绑定快捷键 require("keybinding").lspKeybinding(buf_set_keymap) end 接下来可以完成快捷键的绑定。 lua -- mason.lua nvim_lsp.lua_ls.setup({ on_attach = LspKeybind, on_init = function(client) -- 省略其他配置 end, }) 这样就完成了 lua 的 lsp 的配置,在编写 lua 的时候就可以使用文档查看,code Action 等功能。 目前这些 lsp 的服务都要手动下载,对于一些日常使用的服务,我们可以通过配置,在第一次加载配置的时候,当机器上没有相关的服务的时候,自动下载,这样来说,基本实现了我们上述提出的问题。 lua -- mason.lua mason_lspconfig.setup({ automatic_installation = true, ensure_installed = { "lua_ls", "rust_analyzer" }, }) 这样配置,如果我们本地没有安装 lua 和 rust 的 lsp,会自动进行下载安装。 自动补全 直到目前,在 lsp 的加持下,其实编辑体验已经变得非常棒了,而且开发速率也会大幅提升,虽然 lsp 是支持自动补全功能的,但是上面其实一直没有提及。主要是单单靠 neovim 的功能还不够强大,需要插件的配置。 hrsh7th/nvim-cmp 是一个采用 lua 编写的补全引擎,通过 cmp 及 cmp 的相关插件,会将 neovim 的自动补全达到一个新的高度。 这里除了 nvim-cmp,再推荐几个 cmp 的相关插件。更多的相关插件可以在 wiki 中找到 hrsh7th/cmp-nvim-lsp 对 lsp 提供的补全信息进行提示; hrsh7th/cmp-path 对文件内的路径进行补全; hrsh7th/cmp-vsnip 常见的代码 snip 片段。 配置补全 同样不赘述安装。在 lsp 文件夹中新建 cmp.lua 文件夹。 lua -- cmp.lua local status, cmp = pcall(require, "cmp") if not status then vim.notify("找不到 cmp") return end 剩下了就可以将之前的补全源进行配置。这里贴出我的补全源。 lua sources = cmp.config.sources({ { name = "codeium" }, -- 需要安装 codeium.nvim { name = "nvim_lsp" }, -- For vsnip users. { name = "vsnip" }, }, { { name = "buffer" }, { name = "path" }, }), 此时当我们进行输入的时候就可以看到自动补全的提示了。对于自动补全的提示,上下选择并且上屏,我们可以设置快捷键,来满足我们的使用习惯。 和之前设置快捷键一样,在 keybindings.lua 中添加相关配置。 lua -- keybindings.lua pluginKeys.cmp = function(cmp) return { -- 出现补全 ["<A-.>"] = cmp.mapping(cmp.mapping.complete(), { "i", "c" }), -- 取消 ["<A-,>"] = cmp.mapping({ i = cmp.mapping.abort(), c = cmp.mapping.close(), }), -- 上一个 ["<C-k>"] = cmp.mapping.select_prev_item(), -- 下一个 ["<C-j>"] = cmp.mapping.select_next_item(), -- 确认 ["<CR>"] = cmp.mapping.confirm({ select = true, behavior = cmp.ConfirmBehavior.Replace, }), } end 最后在 cmp.lua 中使用这些快捷键即可。 lua cmp.setup({ -- 省略其他配置 mapping = require("keybindings").cmp(cmp), }) 这样便可以完成自动补全的配置了。 小结 目前为止,已经完成 nvim 的 lsp 的相关配置,并且添加了自动补全。篇幅限制,剩下如何美化 lsp 提示,美化自动补全等我们下篇再说。 我的 neovim 相关配置,可以提供大家进行参考 youngxhui/nvim。
常用插件配置 经过前面章节的介绍,当前的 neovim 已经可以开始基本的使用了。下面会推荐一下常见的插件,让我们快速的开始。 📂 nvim-tree nvim tree 是一个文件浏览器,可以实现侧栏快速选择文件。 当前 neovim 的插件安装都很简单,根据我们之前所了解的方式,先在 Github 上找到相关仓库:https://github.com/nvim-tree/nvim-tree.lua,然后安装。具体如何安装,其实在项目的 README 中会有详细说明。 这里需要安装两个包,第一个就是 nvim-tree,而第二个是一个可选包,主要是用来显示图标的。在 plugins.lua 中新增配置。 lua -- plugins.lua require("lazy").setup( -- 省略其他配置 { "kyazdani42/nvim-tree.lua", event = "VimEnter", dependencies = "nvim-tree/nvim-web-devicons" }, ) 之后在 lua 目录中新建一个 plugins-config 目录,目录中新建 nvim-tree.lua 文件。之后就可以开始我们的配置了。大部分配置其实可以参考官方的 Wiki。 这里我们通过 pcall 函数来加载相关插件。 为什么要使用 pcall ? 当插件没有安装或者出现其他问题的时候,nvim 在启动时,无法加载相关查询,就会抛出异常,通过 pcall 就可以进行相关捕获,从而不影响 nvim 的使用。 lua local status, nvim_tree = pcall(require, "nvim-tree") if not status then vim.notify("没有找到 nvim-tree") return end 这样在加载不到 nvim-tree 的时候,就会通过 vim.notify 报出来。 剩下的就是配置我们的 nvim-tree 了。 lua nvim_tree.setup({ sort_by = "case_sensitive", -- 是否显示 git 状态 git = { enable = true, }, -- 过滤文件 filters = { dotfiles = true, -- 过滤 dotfile custom = { "node_modules" }, -- 其他过滤目录 }, view = { -- 文件浏览器展示位置,左侧:left, 右侧:right side = "left", -- 行号是否显示 number = false, relativenumber = false, signcolumn = "yes" -- 显示图标 width = 30, }, renderer = { group_empty = true, }, }) 最后在最外层的 init.lua 中添加配置。 lua require("plugins-config.nvim-tree") 重新退出后打开,通过命令 :NvimTreeToggle 或者 A-m,可以打开或关闭侧边栏。 如果需要修改快捷键,可以在 keybindings.lua 中新增相关快捷键,之后在 nvim-tree 中引用。 lua -- keybindings.lua local pluginKeybinding = {} pluginKeyBinding.nvim-tree = { { key = "<F5>", action = "refresh" }, } 在 nvim-tree 中,设置相关快捷键。首先要在 nvim-tree.lua 中引入这个变量,并在 setup 中设置相关值。 lua local keymap = require('keybindings').nvim-tree nvim_tree.setup({ -- ... view = { mappings = { custom_only = false, list = keymap, } } -- ... }) 📨 bufferline bufferline 是一个对 buffer 进行管理的插件,可以像现代IDE或者编辑器一样打开多个 Tab,而且可以快速进行切换。安装部分不多赘述,在 README 中上有详细描述。bufferline 地址:https://github.com/akinsho/bufferline.nvim 在 plugins-configs 目录下新建 bufferline.lua 文件,进行配置,仍旧是采用 pcall 进行加载,之后就可以进行配置。 lua -- bufferline.lua local status, bufferline = pcall(require, "bufferline") if not status then vim.notify("没有找到 bufferline") return end bufferline.setup({}) 再次打开 neovim,同时打开多个文件,就会发现在上方出现了新的 tab。 需要进行一些设置,例如当前打开的前面安装的 nvim-tree , tab 会显示在 nvim-tree 上面,这显然是不符合预期的。通过命令 :h bufferline-configuration 来查看插件支持的配置。 通过帮助文档可以看到 offset 配置和 nvim tree 有关系,这样我们就可以通过相关的配置完成配置。 lua -- bufferline.lua bufferline.setup({ options = { offsets = { { filetype = "NvimTree", text = "File Explorer", text_align = "left", separator = true, }, }, }, }) 通过这样的设置就可以发现,当打开 nvim tree 的时候,tab 栏会自动向右偏移,而不会出现在 nvim tree 上方。 其他的设置可以通过上述的方式,通过帮助来查看。这里贴出我的配置。 lua -- bufferline.lua options = { close_command = "bdelete! %d", -- 点击关闭按钮关闭 right_mouse_command = "bdelete! %d", -- 右键点击关闭 indicator = { icon = '▎', -- 分割线 style = 'underline', }, buffer_close_icon = '', modified_icon = '●', close_icon = '', offsets = { { filetype = "NvimTree", text = "File Explorer" , text_align = "left", separator = true, } }, } 这里使用了 bdelete 相关命令,这个是 moll/vim-bbye 这个插件提供的,使用上面的配置需要安装该插件。 同时,可以在 keybinding.lua 中设置切换 tab 的快捷键。我这里设置为 Ctrl + h/l 来左右切换。 lua -- keybindings.lua map("n", "<C-h>", ":BufferLineCyclePrev<CR>", opt) map("n", "<C-l>", ":BufferLineCycleNext<CR>", opt) ✏️ lualine lualine 插件可以提供类似的效果,在编辑器上提供一些信息提示,例如 Git 状态,文本编码等,下图是 Github 上的效果。 如何安装可以直接看 nvim-lualine/lualine.nvim 上介绍,不多赘述。同样在 plugins-configs 下建立相关的配置文件 lualine.lua,通过 pcall 方式引入,使用 lualine.setup({}) 引入插件,最后在 init.lua 中添加配置文件。每个插件的安装使用方式都基本相似,如果有不同情况,会另外说明,后续的插件安装将不在说明这些步骤。 在 lualine 中,显示区域被分成了 6 个部分,分被用 A,B,C,X,Y,Z 组成。 txt +-------------------------------------------------+ | A | B | C X | Y | Z | +-------------------------------------------------+ 上面的每个部分都可以进行定制。在仓库中有三种样例,可以直接在 example 中找到。 相关配置: lua local status, lualine = pcall(require, "lualine") if not status then vim.notify("没有找到 lualine") return end lualine.setup({ options = { theme = "auto", component_separators = { left = "|", right = "|" }, section_separators = { left = " ", right = "" }, }, extensions = { "nvim-tree", "toggleterm" }, sections = { lualine_c = { "filename", }, lualine_x = { "filesize", { "fileformat", symbols = { unix = '', -- e712 dos = '', -- e70f mac = "", -- e711 }, }, "encoding", "filetype", }, }, }) 经过配置就可以看到这样的效果了。 最近(2024 年 03 月 02 日)我更新了一下 lualine 样式,相较于之前更加简洁。相关样式如下 相关配置 lua lualine.setup({ options = { theme = "auto", component_separators = { left = "", right = "" }, section_separators = { left = " ", right = "" }, }, extensions = { "nvim-tree", "toggleterm" }, sections = { lualine_a = { { "mode", icons_enabled = true, }, }, lualine_b = { { "filetype", colored = true, icon_only = true, }, "filename", }, lualine_c = { "branch", "diff", }, lualine_x = {}, lualine_y = {}, lualine_z = { "progress", }, }, }) 🔍 telescope 经过上面的配置,Neovim 的编辑界面已经逐渐“现代化”。Telescope 可以让我们的查找过程更加丝滑,主要可以用来做模糊查询。 telescope 安装也比较简单,可以参考 github 上的 README 进行安装。但是往往安装后无法进行模糊查询。这里就需要我们的另外的配置了。其实在 README 中也写的很明白,安装完成后,需要运行 :checkhealth telescope 命令。 通过该命令,可以看到当前插件的状态,是否可用,如果是首次安装,会提示 ERROR 和 WARNING,如图所示: 从提示可以看到缺少 rg 和 fd,同时在后面的说明中给出了相关的安装地址。这两个软件是进行模糊搜索的关键,可以通过以下两个地址进行安装,在 Github 的 README 中都明确的写出了两个软件在不同的系统上的安装方式。 rg:https://github.com/burntsushi/ripgrep fd: https://github.com/sharkdp/fd 如果你是用 mac,恰好已经安装 brew,那么只需要下面的两行命令即可完成安装。 shell brew install rg brew install fd 安装完成后,重新 :checkhealth telescope,如果都是 OK,则证明安装正确,如下图所示: 安装完成后就可以通过命令使用 telescope,进行快捷的模糊查询。:Telescope find_file 为查找文件,Telescope live_grep 为全局查询。为了方便,可以在 keybindings.lua 配置中绑定为对应的快捷键。下面是我快捷键的对应绑定。 lua -- keybindings.lua -- 查找文件 map("n", "<C-p>", ":Telescope find_files<CR>", opt) -- 全局搜索 map("n", "<C-f>", ":Telescope live_grep<CR>", opt) 更多有趣的配置可以看 Github 仓库。 附录 Nerd 字体安装 在上面的很多配置中,很多图标是无法显示的,会导致文本中或者界面中显示有问题,常常会显示为一个⊠,这里就需要一个 nerd 字体进行支持。 Nerd 字体可以看做原始字体的一个补丁,这个补丁对原始字体新增了大量的图标。 针对上面无法显示的图标,需要安装好相关的 Nerd 字体。相关字体可以在官方网站下载,一些知名的字体都会有 nerd 版本,例如:FiraMono、JetBrainsMono、UbuntuMono 等。大家可以依照自己的喜好进行下载安装。 字体配置 不同的终端模拟器有不同的配置方式,这里列出常见的配置方式。 iTerm2 iterm2 设置如下 Profiles -> Text 中,将 font 修改为 Nerd 字体即可。 WezTerm WezTern 修改配置文件 ~/.config/wezterm/wezterm.lua。 lua local wezterm = require("wezterm") local config = {} config.font = wezterm.font_with_fallback({ { family = "JetBrainsMono Nerd Font Propo", weight = "Bold" }, { family = "苹方-简", weight = "Bold" }, "Noto Color Emoji", })
在编程的世界中,有两个上古神器。一个叫做 “神之编辑器 Emacs”,另一个叫做 “编辑器之神 vim”。这两个编辑器从诞生到现在,圣战从未结束。无论是 vim 还是 emacs 都在不断的进化和发展,渐渐的 vim 的分支上出现了一颗夺目的新星,他就是 neovim。 什么是 neovim neovim 从名称来看:新星的vim。按照官方说明: nvim 是一个 vim 的 fork,主要关注了扩展性和易用性。大量的 vim 用户迁移到 nvim,而 vim 的魅力出了经典的快捷键还有丰富的插件系统,这些 nvim 都继承了下来,同时内置了 LSP,增加了 异步 IO 等新特性。 以下是 Neovim 的一些主要特点和优势: 兼容性:Neovim 是 Vim 的兼容版本,几乎可以无缝地使用现有的 Vim 配置文件和插件。它支持 Vim 的命令和操作方式,因此 Vim 用户可以很容易地切换到 Neovim。 异步支持:Neovim 引入了异步任务处理的机制,使得编辑器可以在后台执行长时间运行的任务,而不会阻塞用户界面。这使得插件和脚本可以更高效地处理耗时操作,提高了编辑器的响应性。 现代化的插件系统:Neovim 提供了更灵活、更易于扩展的插件系统。它支持各种编程语言编写的插件,并提供了对外部进程通信的接口,使得插件可以与其他程序进行交互。 社区活跃:Neovim 拥有一个活跃的社区,不断推动编辑器的发展和改进。社区提供了大量的插件、主题和配置文件,以及对新功能的贡献和支持。 📃基础配置 neovim 中配置可以通过 init.vim 或者 init.lua 进行配置,当前大部分的配置都采用了 lua ,本文也将会通过 lua 进行配置 nvim。如果你还不会使用 lua 也不需要担心,lua 可以快速上手。你可以直接通过 :h lua-guide 进行查看 lua 教程。 init.lua 在类 unix 系统中,该配置文件位于 ~/.config/nvim/ 目录下,而在 windows 系统中,该目录位于 %USERPROFILE%\AppData\Local\nvim\。nvim 启动时会加载该目录下的 init.lua 文件,那么只需要在该文件中进行配置即可。 首先对文件的编码格式设置,统一为 UTF-8 。只需要在 init.lua 中添加相关配置。当然这是 nvim 的默认配置,不进行添加也是可以的。 lua vim.g.encoding = "UTF-8" vim.o.fileencoding = "UTF-8" 这里的 o 和 g 是什么意思呢? vim.o 意味着可以对全局的选项进行设置,而 vim.g 是进行全局设置。 类似的相关方法还有 vim.wo 设置 window-local 选项、vim.bo 设置 buffer-local 选项等。 对 tab 进行设置, tab 默认为 4 个空格。 lua vim.o.tabstop = 4 vim.bo.tabstop = 4 vim.o.softtabstop = 4 vim.o.shiftround = true 还可以将其他的基础配置添加进来,例如显示行号等,具体配置如下 lua -- 编码方式 utf8 vim.g.encoding = "UTF-8" vim.o.fileencoding = "utf-8" -- jkhl 移动时光标周围保留8行 vim.o.scrolloff = 8 vim.o.sidescrolloff = 8 -- 显示行号 vim.wo.number = true -- 使用相对行号 vim.wo.relativenumber = true -- 高亮所在行 vim.wo.cursorline = true -- 显示左侧图标指示列 vim.wo.signcolumn = "yes" -- 右侧参考线 vim.wo.colorcolumn = "160" -- 缩进字符 vim.o.tabstop = 4 vim.bo.tabstop = 4 vim.o.softtabstop = 4 vim.o.shiftround = true -- >> << 时移动长度 vim.o.shiftwidth = 4 vim.bo.shiftwidth = 4 -- 空格替代 vim.o.expandtab = true vim.bo.expandtab = true -- 新行对齐当前行 vim.o.autoindent = true vim.bo.autoindent = true vim.o.smartindent = true -- 搜索大小写不敏感,除非包含大写 vim.o.ignorecase = true vim.o.smartcase = true -- 搜索不要高亮 vim.o.hlsearch = false vim.o.incsearch = true -- 命令模式行高 vim.o.cmdheight = 1 -- 自动加载外部修改 vim.o.autoread = true vim.bo.autoread = true -- 禁止折行 vim.wo.wrap = false -- 光标在行首尾时<Left><Right>可以跳到下一行 vim.o.whichwrap = "<,>,[,]" -- 允许隐藏被修改过的buffer vim.o.hidden = true -- 鼠标支持 vim.o.mouse = "a" -- 禁止创建备份文件 vim.o.backup = false vim.o.writebackup = false vim.o.swapfile = false -- smaller updatetime vim.o.updatetime = 300 vim.o.timeoutlen = 500 vim.o.splitbelow = true vim.o.splitright = true -- 自动补全不自动选中 vim.g.completeopt = "menu,menuone,noselect,noinsert" -- 样式 vim.o.background = "dark" vim.o.termguicolors = true vim.opt.termguicolors = true -- 不可见字符的显示,这里只把空格显示为一个点 vim.o.list = false vim.o.listchars = "space:·,tab:>-" vim.o.wildmenu = true vim.o.shortmess = vim.o.shortmess .. "c" -- 补全显示10行 vim.o.pumheight = 10 vim.o.clipboard = "unnamedplus" 📦插件系统 vim/neovim 经过多年的发展仍旧活跃,其中插件系统功不可没。丰富的插件可以使得 neovie 分分钟化身为 IDE 。 在社区的发展过程中,vim 的插件系统也不断的壮大,目前 vim 的插件基本可以涵盖编辑的方方面面。针对 neovim,可以通过 awesome-neovim 这个项目查询相关插件。 无论是 vim 还是 neovim,本身并没有插件管理器,并不像 vscode 或者其他编辑器一样,可以方便的对查件进行添加,删除或者更新等操作。当然你也无需要担心,各路大神已经开发出了多个插件管理工具。目前,对于 neovim 来说,比较有名的插件管理工具有 packer.nvim 和 lazy.nvim (注意:不要和 LazyVim 混淆)。 本文采用将会采用 lazy.nvim 作为插件管理工具进行插件管理。如果你使用了 packer.nvim 作为插件管理,并不影响阅读,可以跳过插件管理器章节。 插件管理器 lazy.nvim 作为现在比较流行的插件管理工具,安装其实很简单。在 init.lua 中添加相关代码。 lua local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim" if not vim.loop.fs_stat(lazypath) then vim.fn.system({ "git", "clone", "--filter=blob:none", "https://github.com/folke/lazy.nvim.git", "--branch=stable", -- latest stable release lazypath, }) end vim.opt.rtp:prepend(lazypath) require("lazy").setup() 保存退出后,再次进入 nvim。 nvim 会检查是否存在 lazy, 如果不存在会从 github 上 clone 下来。 通过 :Lazy 命令,如果看到下面的图像,那么就证明 lazy.nvim 安装成功了。 有了 lazy,之后就可以快速的安装、更新和卸载插件。 第一个插件 为了验证 lazy 功能是否正常,那么先安装个主题插件看看。主题插件我选择 catppuccin.nvim。 通过 catppuccin 文档可以查到如何安装,将插件添加到 lazy 配置中。 lua require("lazy").setup( { "catppuccin/nvim", name = "catppuccin", priority = 1000 } ) 退出 neovim,再次进入,就会看到 lazy 在下载相关插件。 安装完成后,发现我们的主题已经被应用,但是等到退出 neovim 再次进入的时候,发现主题颜色又恢复了默认配色,这里我们只需要在配置文件末尾添加一句,用于设置相关的配色。 lua vim.cmd.colorscheme("catppuccin") 其中 catppuccin 还有很多配置,这里不多赘述,可以到相关仓库中查看配置内容。 有了插件管理器,那么就可以添加不同的插件了。 配置的组织 经过上面简单的了解,目前所有的配置都写在 init.lua 中,当添加更多的插件的时候, lua 中的代码也会不断的增加,那么当需要进行修改的时候,查询相关配置也成了一大问题,而通过 lua 的特性,我们可以将不同的配置文件进行拆分,做到高内聚低耦合。 对于 lua 来说,可以通过 require 函数来将不同的 lua 脚本进行导入。 现在同 init.lua 目录下建立 lua 文件夹,在其中新建两个 lua 文件,分别是 basic.lua 和 plugins.lua。 text . ├── init.lua └── lua ├── basic.lua └── plugins.lua 结构如上目录树所示,之后可以将配置内容分被复制到相关的文件中,将基础配置放在 basic.lua 中,将 lazy 相关的配置复制到 plugins.lua 中。 最后将 init.lua 中引入相关的配置文件即可。 lua require("basic") require("plugins") 其中,对于颜色主题的配置我们可以新建一个 theme.lua 文件,将主题配色相关的配置存放,最后不要忘记在 init.lua 中添加该文件。 最后的目录结构如下: text . ├── init.lua ├── lazy-lock.json └── lua ├── basic.lua ├── plugins.lua └── theme.lua ⌨️ 快捷键绑定 为了让 vim 更加的顺手,方便我们的使用,针对一些操作需要绑定我们的快捷键,这样让你的操作效率如虎添翼。按照上面的配置规则,新建文件为 .lua,并在 init.lua 中添加。 认识快捷键 vim 的一个重要点就是可以通过快捷键快速高效的完成任务,指哪里打哪里,而在配置快捷键中,其实有怎么几个键需要我们熟知,分被为 Ctrl 、 Alt 和 Shift 键。这里并不是让大家知道这些按键在哪里,而是说配置中,这些键位很常用,并且在配置中这些键位常常简写。 键位 简写 Ctrl C- shift S- Alt A- 这三个键位在配置中会非常常见。当然,并不是只有这三个键位可以找到相关的缩写,可以通过 :h key-notations 看到所有的键位说明。 另外,对于 mac 来说是没有 Alt 键的,这样就需要我们修改 Option 键位,这部分可以看 附录/修改 Alt 键位。 Leader 键 leader 键对于 vim 来说是一个非常重要的键,它可以说是在 vim 中使用频率最高的键之一。 leader 顾名思义,处于领导位置,一般作为组合快捷键的先驱,使用其他按键的时候先按下 leader。对于 leader,vim 并没有规定谁是 leader,只要你想,哪个键也可以为 leader。一般会将 空格 等按键设置为 leader 键。 上面这些都是我们配置前的基础知识。下面开始设置相关快捷键。 设置快捷键 在 lua 文件夹下新建文件, keybindings.lua,并且在 init.lua 中添加, require("keybindings") 首先便是上面说过的 leader 键。这里我采用空格作为 leader 键。 lua -- keybindings.lua vim.g.mapleader = " " vim.g.maplocalleader = " " 关于 mapleader 和 maplocalleader 的区别可以看 https://luciaca.cn/posts/vimscript-learning-on-leaders 相关文档。 在 neovim 中,需要通过 vim.keymap.set() 函数进设置。这个函数需要传入四个参数,分别是 mode , lhs , rhs , opts 。 参数名 说明 mode 模式的简写,常见的有 n(normal), i(insert), v(view) 等 lhs 可以理解为对应的按键 rhs 对应的功能 opts 相关的设置 lua vim.g.mapleader = " " vim.g.maplocalleader = " " local opt = { noremap = true, silent = true } -- visual模式下缩进代码 vim.keymap.set("v", "<", "<gv", opt) vim.keymap.set("v", ">", ">gv", opt) -- 左右Tab切换 map("n", "<C-h>", ":BufferLineCyclePrev<CR>", opt) map("n", "<C-l>", ":BufferLineCycleNext<CR>", opt) -- 省略其他基础配置 这样就可以自定义添加相关按键。 我的相关配置,提供给大家参考 youngxhui/nvim。 附录 修改 Alt 键位 iterm2 在 setting 中, Profiles-Keys 中, 将 Left Option Key 设置为 Esc+ 。 Alacritty 编辑 alacritty 的配置文件中window.option_as_alt yaml window: # Make `Option` key behave as `Alt` (macOS only): # - OnlyLeft # - OnlyRight # - Both # - None (default) option_as_alt: Both Ghostty Ghostty 是最近发布的一款终端模拟器。 需要在配置文件中新增 macos-option-as-alt ini macos-option-as-alt = true 参考资料 为何 Emacs 和 Vim 被称为两大神器 BurntSushi/ripgrep
在 HTTP 标准库 中解释了 go 的标准库是如何处理请求的,但是通过源码的分析,可以发现,标准库对于这部分的处理比较简单,例如对 url 中携带参数就不支持,面对这种情况,社区中出现了大量的框架,对原有的 http 进行补充。 大部分的 Go 的 HTTP 框架都是在重写路由部分,已实现更快的更准确的路由查找,减少路由解析过程中消耗的时间,来提高框架的处理速度。 标准库 HTTP 的处理 先来回顾一下标准库的 HTTP 处理流程。 启动 HTTP 服务器:使用 http.ListenAndServe 或 http.ListenAndServeTLS 函数启动 HTTP 服务器。 处理请求:当服务器接收到 HTTP 请求时,它会使用与路径相对应的 http.Handler 实现处理请求。 调用处理程序:服务器会调用 ServeHTTP 方法,并将请求相关的信息作为参数传递给该方法。 路由匹配:在 ServeHTTP 方法中,通过比较请求的路径和已注册的路由,找到与请求匹配的路由。 调用处理函数:如果找到了匹配的路由,则调用与该路由相关的处理函数。 写入响应:处理函数通过 ResponseWriter 接口写入响应数据,以返回给客户端。 根据上述的处理方式,目前需要关注两个函数:ServeHTTP 和 ResponseWriter 。这是两个主要的处理方式。 http.ListenAndServe 和 http.ListenAndServeTLS 用于启动 HTTP 服务器;以及 http.ResponseWriter 和 http.Request 分别用于写入响应和处理请求。 同时通过源码分析,如果在 Listen 的时候不传入 handler, 那么就会采用 http 默认的 Handler,也就是 ServeMux ,所以只要我们按照标准实现一个 Handler,那么就会替换原有的处理逻辑,而且 Handler 的实现也是非常简单,只要实现 ServeHTTP 这个接口即可。 gin 启动 采用 gin 实现一个最简单的 ping/pong 服务。这是官方 README 提供的例子。 Go package main import ( "net/http" "github.com/gin-gonic/gin" ) func main() { r := gin.Default() r.GET("/ping", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "message": "pong", }) }) r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080") } 例子中实现也是非常简单,先通过 gin.Default() 生成一个 engine。 之后添加相关的处理方式,最后用 r.Run() 启动。下面将会从最开始的方法开始剖析 gin 框架。 Engine Default 是 gin 使用的第一个函数。这个函数看起来很简单,一共就五行代码。 Go func Default() *Engine { debugPrintWARNINGDefault() engine := New() engine.Use(Logger(), Recovery()) return engine } 其中主要通过 New() 函数初始化 Engine 结构体。在 gin 中,通过 Engine 这个结构体进行管理,这个结构体其实是实现了 ServeHTTP 这个方法。 在这里 Engine 的功能和标准库中的 ServeMux 的地位其实是一模一样的,那么他的主要功能也就是用来保存注册的 handler,等到使用的时候进行查找调用。那么就先看看路由和 handler 是如何注册的。 注册路由 在上面的例子中, 路由和 handler 是通过 r.GET() 方法进行注册。从源码来看,可以发现不仅仅是 GET 方法,其他的请求方法也一样,都是直接调用了 group.handler 方法。 Go func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes { // 计算绝对路径 absolutePath := group.calculateAbsolutePath(relativePath) // 合并 handler handlers = group.combineHandlers(handlers) // 添加相关路由 group.engine.addRoute(httpMethod, absolutePath, handlers) return group.returnObj() } 从代码中可以看到这个方法,其实是比较简单,从代码上的命名来看,基本进行了以下操作:先进行绝对路径的计算,之后对 handler 进行合并,最后添加路由。 为什么要 Combine 在combineHandlers方法中,首先计算了当前handlers的长度,并判断是否超过了最大长度(当前最大长度为63)。如果超过最大长度,则会引发panic异常。在这里,源代码采用了两次复制(copy)操作。第一次是将group.Handlers中的数据复制到mergedHandlers中,第二次是将handlers的数据传入mergedHandlers中。 Go func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain { finalSize := len(group.Handlers) + len(handlers) // ... mergedHandlers := make(HandlersChain, finalSize) copy(mergedHandlers, group.Handlers) copy(mergedHandlers[len(group.Handlers):], handlers) // ... } 这样 mergeHandler 中就存在两部分数据, handlers 可以通过源码看出来是在项目中注册的 handler,那么 group.Handlers 中又是什么呢? 其实就是在项目启动的时候注册的中间件。在 Engine 中的 Default 方法可以看到,engine.Use(Logger(), Recovery()) 项目在初始化的时候注册了两个中间件,而这个 Use 方法,其实就是将中间件添加到上面的 group.Handlers 中,这里不多赘述,只是简单的说明一下,具体的中间件的流程会在 中间件(Middleware) 章节讲述。 确切地说,mergeHandlers的目的是将中间件和用户定义的处理函数合并为一个处理函数切片,并按照一定的顺序注册到路由中。 添加路由 路由添加方法也比较简单,核心代码一共就 6 行。 Go root := engine.trees.get(method) if root == nil { root = new(node) root.fullPath = "/" engine.trees = append(engine.trees, methodTree{method: method, root: root}) } root.addRoute(path, handlers) 先通过请求方法获取树的根节点,如果根节点不存在就创建一个,最后添加相关的路由和 handlers。这里关于路由如何添加,路由数据结构在路由(Router)章节会进行讲解。 直到目前,路由的注册工作已经完成。 如何Run 在之前详细的了解过 go 的 net/http 包中是如何启动一个 http 服务之后,其实现在回过头来看 gin,其实一切变得很简单。 在 gin 的 Run 方法中,主要通过标准库的 http.ListenAndServe 方法启动,而这个方法在 HTTP 标准库中有过详细的分析 ,剩下的方法流程和标准库中的流程基本一致,唯独不同的一点是将原有的默认 Handler 换成了 gin.Handler。 在之前说过,要想成为一个 Handler,只要实现 ServeHTTP 方法即可,而 gin 的 engine 就实现了这个方法。 根据之前对 http 了解的处理流程来看,在 gin 收到相关的请求,都会统一调用 ServeHTTP 方法,该方法会将接收到的参数等进行处理,例如寻找合适的处理器(Handler),最后返回统一的处理结果。 Go // ServeHTTP conforms to the http.Handler interface. func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { c := engine.pool.Get().(*Context) c.writermem.reset(w) c.Request = req c.reset() engine.handleHTTPRequest(c) engine.pool.Put(c) } 首先使用到的变量有 pool。这里的 pool 使用的 sync.Pool 这个类型,主要是用来重复使用的 Context。这里直接从 pool 中取出 Context,并对 Context 的一些参数进行设置,最后调用 engine.handleHTTPRequest 方法。 这也是目前常常使用 engine.handleHTTPRequest 这个方法主要处理用户的 HTTP 请求,确定该请求的处理方法。简单的来说就:首先,获取请求的 HTTP 方法(如 GET 或 POST)和 URL 路径,并在需要时解码路径。然后,它搜索匹配该请求的处理树。如果找到了一个匹配的节点,它会将处理程序分配给请求的上下文(c),并写入 HTTP 响应头。如果未找到匹配的节点,则会通过 serverError 写入 “405 Method Not Allowed” 或 “404 Not Found” 的错误响应。 这样基本就是一个简单的 http 请求的处理过程。在代码中可以看到很多事情其实是由上下文 (Context) 进行处理的。
前言 Go 语言以其出色的并发性能和优雅的编程模型而闻名,对于 http 服务可以做到开箱即用,无需第三方框架,而且使用起来也很简单。即便如此,还会有很多 http 框架的诞生,例如 gin,echo 等,说明自带的 http 服务还有不完美的地方,导致了用户选择第三方开发的框架。 这是一个采用标准库开发的 http 服务。 Go package main import "net/http" func main() { http.HandleFunc("/ping", pingHandler) http.ListenAndServe(":8000", nil) } func pingHandler(w http.ResponseWriter, r *http.Request) { w.Write([]byte("pong")) } 这是一个简单的HTTP服务。在启动服务后,通过访问 http://localhost:8000/ping,可以访问相关的处理程序并返回响应信息:“pong”。 这段简单的代码中,直接运行了一个高性能的HTTP服务。代码中涉及了两个与HTTP相关的函数,分别是:http.HandleFunc和http.ListenAndServe。 接下来,对这两个函数进行解析,让您了解每个步骤的具体操作。 路由注册 HandleFunc函数的作用是将指定的处理函数handle注册到HTTP框架中,使得特定的URL路径和该处理函数建立映射关系。 以下是HandleFunc()函数的源代码实现 Go func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) { DefaultServeMux.HandleFunc(pattern, handler) } 在这段代码中,HandleFunc函数接收两个参数:pattern和handle。pattern是URL路径的模式,用于匹配请求的URL;handle是处理函数,接收ResponseWriter和Request作为参数,用于处理请求并生成相应的响应。 函数内部调用了DefaultServeMux.HandleFunc()函数,将pattern和handle注册到默认的 ServeMux(多路复用器)中,建立URL和处理函数之间的映射关系。 Lua func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) { if handler == nil { panic("http: nil handler") } mux.Handle(pattern, HandlerFunc(handler)) } HandleFunc函数是ServeMux类型的方法。ServeMux是一个HTTP请求多路复用器,用于将请求路由到相应的处理程序。在这个方法中,我们传入了一个pattern参数和一个handler函数参数。 Go type HandlerFunc func(ResponseWriter, *Request) // ServeHTTP calls f(w, r). func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { f(w, r) } HandlerFunc 的定义很简单,并且实现了 ServeHTTP 方法。这个方法主要是调用本身。 mux.Handler实现同样比较简单,可以看到将路由(pattern)和具体实现(handler)注册到 DefaultServeMux 这个对象上。通过不断的往下看源码,可以找到 ServeMux.Handler 这个方法上。这个方法主要是将服务中的路由进行注册。将服务注册到 ServerMux 对象上,也就是上文所提到的 DefaultServeMux。 对于 ServeMux 来说,是一个比较简单的结构。其中可以看到 m 和 es 保存了路由 Go var DefaultServeMux = &defaultServeMux var defaultServeMux ServeMux type ServeMux struct { mu sync.RWMutex m map[string]muxEntry es []muxEntry hosts bool } type muxEntry struct { h Handler pattern string } func (mux *ServeMux) Handle(pattern string, handler Handler) { // 省略代码 ... if mux.m == nil { mux.m = make(map[string]muxEntry) } e := muxEntry{h: handler, pattern: pattern} mux.m[pattern] = e if pattern[len(pattern)-1] == '/' { mux.es = appendSorted(mux.es, e) } if pattern[0] != '/' { mux.hosts = true } } 如果 ServerMux.m 为空,会进行初始化,之后将 handler 和 pattern 存放到 muxEntry 中,最后将 muxEntry 存放到 m 中,m 的 key 是 pattern。这里的 pattern 就是 url 路径。 在 ServeMux 中,mux.es 切片是用来保存以斜杠结尾的路由模式对应的 muxEntry 对象的。它的作用是在请求的 URL 中去掉末尾的斜杠后进行匹配,从而避免重复处理类似 /path 和 /path/ 这样的 URL。 例如,如果有两个路由模式分别为 /path 和 /path/,请求的 URL 为 /path/,如果没有 mux.es 切片,将会尝试匹配 /path 和 /path/ 两个路由模式,最终会选择匹配 /path/ 的路由模式进行处理。这会导致处理器被重复调用。而使用 mux.es 切片,请求的 URL 会被处理为 /path,只会匹配到 /path 这一个路由模式,避免了处理器被重复调用的问题。 因此,mux.es 切片的作用是为了提高 ServeMux 的匹配效率,避免重复处理请求。 所有的 url 和 Handler 的映射关系都是通过 map[string]muxEntry 进行保存。这样就会出现问题,稍微复杂一些的 url 就无法很好的匹配。这也就是为什么会有大量的 go web 框架,而这些框架都是改写路由的匹配算法。 关于 DefaultServeMux 是一个实现了 HandlerServe 接口的结构体。 上面就是一个主要的注册过程。 ListenAndServe 这个方法主要对端口进行监听。 Go func ListenAndServe(addr string, handler Handler) error { server := &Server{Addr: addr, Handler: handler} return server.ListenAndServe() } 从源码可以看到,这里需要一个 handler 参数,并且新生成一个 server 对象。通过调用 ListenAndServe 方法进行处理。 Go func (srv *Server) ListenAndServe() error { if srv.shuttingDown() { return ErrServerClosed } addr := srv.Addr if addr == "" { addr = ":http" } ln, err := net.Listen("tcp", addr) if err != nil { return err } return srv.Serve(ln) } 在 ListenAndServe 中,首先对服务的状态进行了判断,如果是 shuttingDown 就提示 http: Server closed。这里的 shuttingDown 主要是通过一个叫 atomicBool 进行判断的。咋一看以为是原子操作,仔细看其实是定义了一个 int32 类型,通过 int32 的原子操作保证了并发安全。 Go type atomicBool int32 func (b *atomicBool) isSet() bool { return atomic.LoadInt32((*int32)(b)) != 0 } func (b *atomicBool) setTrue() { atomic.StoreInt32((*int32)(b), 1) } func (b *atomicBool) setFalse() { atomic.StoreInt32((*int32)(b), 0) } 之后通过 net.Listen() 方法进行监听,这里对这个方法不做过多的赘述,之后通过 Serve 方法。 下面是主要的核心方法 Go ctx := context.WithValue(baseCtx, ServerContextKey, srv) for { rw, err := l.Accept() if err != nil { select { case <-srv.getDoneChan(): return ErrServerClosed default: } if ne, ok := err.(net.Error); ok && ne.Temporary() { if tempDelay == 0 { tempDelay = 5 * time.Millisecond } else { tempDelay *= 2 } if max := 1 * time.Second; tempDelay > max { tempDelay = max } srv.logf("http: Accept error: %v; retrying in %v", err, tempDelay) time.Sleep(tempDelay) continue } return err } connCtx := ctx if cc := srv.ConnContext; cc != nil { connCtx = cc(connCtx, rw) if connCtx == nil { panic("ConnContext returned nil") } } tempDelay = 0 c := srv.newConn(rw) c.setState(c.rwc, StateNew, runHooks) // before Serve can return go c.serve(connCtx) } l 为 net.Listener 对象,当每次接收到信息的时候,首先会进行一个错误判断。 如果是 down 的信号,就会直接返回相关错误,否则先对错误进行断言,检查是否为 net.Error,这个是一个接口,其中 Temporary 方法官方已经标记为启用,这个方法更多的表示为超时。如果有超时,你们就会对延时 tempDelay,进行增加,起初是 5 毫秒,之后每次增加 2 倍,最大为 1 秒钟,之后会进行重试。 通过 srv.ConnContext 会生成一个新的 ctx,否则就使用之前的 ctx,也就是 context.Backgroud()。然后开启一个协程进行服务。 在协程中,通过 readRequest 方法进行获取,返回 response。通过 response 对象获取 request。通过 request 判断请求是否要继续。 Go req := w.req if req.expectsContinue() { if req.ProtoAtLeast(1, 1) && req.ContentLength != 0 { req.Body = &expectContinueReader{readCloser: req.Body, resp: w} w.canWriteContinue.Store(true) } } else if req.Header.get("Expect") != "" { w.sendExpectationFailed() return } 这里判断首先通过 expectsContinue 方法,这个方法中获取请求头中的 Expect 字段是否等于 100-continue。当等于的时候要继续进行判断,其中请求的协议为 HTTP 1.1 和 ContentLength 不为 0。这样就可以获取到请求体。 当请求头中的 Expect 和上述条件不相同的时候,直接返回 417 错误。 之后创建了一个 serverHandler 并且调用了 ServeHTTP 并且传入了 response 和 request。 ServeHTTP 再一次出现,其中第一步就是获取 Handler。 Go handler := sh.srv.Handler if handler == nil { handler = DefaultServeMux } 那么,这里获取的 handler 应该是什么呢?经过多个方法或函数,可以已经对 sh.srv.Handler 一步一步的向上推到。这里我将这个过程画了一张图,图上箭头表示关系之间的依赖,红色表示持有 handler 数据。 通过这个依赖图可以看到,handler 是由最开始的 ListenAndServe 方法进行传入的,而我们的示例代码中这部分传入的是 nil,也就是从开始到现在 handler 一直为 nil。这也就是为什么会一个判断,当 handler 为空的时候使用 DefaultServeMux。其实关于默认的 handler 为 DefaultServeMux 这个事情,在 ListenAndServe 这个代码的注释中就已经说明。 之后就是调用 handler.ServeHTTP 方法,ServeHTTP 方法主要是一个请求分发的作用。源码中调用了 Handler 方法。这个方法主要的作用就是查找请求对应的 Handler 是那个。 Go // ServeHTTP dispatches the request to the handler whose // pattern most closely matches the request URL. func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) { if r.RequestURI == "*" { if r.ProtoAtLeast(1, 1) { w.Header().Set("Connection", "close") } w.WriteHeader(StatusBadRequest) return } h, _ := mux.Handler(r) h.ServeHTTP(w, r) } 先对方法为 CONNECT 的请求做了处理。通过 redirecToPathSlash 方法,这个方法主要是要判断给定的路径是否要加 /,而这个方法中加锁后调用了 shouldRedirectRLocked 这个方法。通过查找 ServeMux 的 m 这个属性中是否存在相关的路径。这个 m 在上文中介绍过,是一个 map 结构。这也就是为什么会加锁,这里的 map 是并发不安全的。查询的方式也很简单,就是在之前的 map 中查询是否存在,如果存在就返回 false,如果没有找到会进行一些特殊的处理,在路径上加上后缀 / 进行查找,同时为了防止路径为 //,所以在返回的时候又进行了一次判断。 在上述的方法结束后,会返回一个 bool 值,来确定是否需要添加末尾的 /。从而返回相关的 url。通过新的 url 生成一个 RedirectHandler 的结构体。如果没有找到通过 通过 handler 这个方法。 handler 这个方法通过 match 这个方法进行查找。 Go func (mux *ServeMux) match(path string) (h Handler, pattern string) { v, ok := mux.m[path] if ok { return v.h, v.pattern } for _, e := range mux.es { if strings.HasPrefix(path, e.pattern) { return e.h, e.pattern } } return nil, "" } 这个方法比较简单,现在 m 中进行查找,如果没有找到,从 es 中查找。之前我们说过 es 是保存了后缀有 / 的 handler。如果都无法查找到就返回 nil。 这里的 handler 就是在一开始注册的 HandleFunc(pingHandler) 。 最后在 ServeMux.ServeHTTP 这个方法中调用 handler 相关的 ServeHTTP。这样就调用成功了。 总结 通过两个方法基本可以做到路由的注册方式和路由的查询方式,并且对请求来临的时候相关的处理过程。这些方法对之后研究其他框架源码或者工作方式更加清晰。
说起来已经很久很久都没有写博客了。从毕业到工作,这段时间感觉很忙很忙,失去了自己思考的时间,每天感觉像流水线上的机器,不停的写代码写代码,自己本身却没有多大的提升。 新手不要使用终端 vim。 入门建议 之前断断续续的写过一些 vim 的相关文章,和断断续续的使用 vim。 之前一直想着把 vim 当作 IDE 使用。但是繁琐的配置和一些不敬如人意的地方,导致在使用过程中会逐步的放弃。 而最近两个月,我开始使用了 vim。使用 vim 来说,就像打字盲打一样,主要靠肌肉记忆,而不是思考。 不是把 vim 当作 IDE,而是把 IDE 当 vim。 目前,大多数的 IDE 都会有 vim 的相关 keymap 设置,而我目前使用的工具主要是 Vscode 和 Goland。 这两款工具都对 vim 有良好的支持。 Vim 的模式 针对 vim 上手困难,首先的一个困难我觉得来自于它的模式,说实话,从我接触电脑来说,无模式感觉是一种正常的情况。打开 word,可以直接进行编辑,打开 IDE,可以直接进行编码,这种情况对与我来说那就是理所当然,就应该是这样。 而这些对于 vim 来看,有显示的那么反直觉,打开后无从下手,但是当你学会编辑,需要移动的时候,不停的在 normal 和 insert 两个模式换来换去。 这些对于刚刚入手 vim 来说,都将成为放弃的理由。 命令 当你逐渐了解了 vim 的模式之后,剩下的就是 vim 的命令,或者说是快捷键。 首先就是移动,通过 h,j,k,l 四个键来进行光标移动,而不是我们习惯的方向键(虽然方向键也可以移动),熟练使用这四个键进行光标移动绝对是入门 vim 的重要点,刚刚开始的时候是在强迫自己使用这四个键来进行移动,下意识的练习才能使自己不断的掌握 vim 的基本操作。 除了光标的移动,那么剩下的最重要的功能就是编辑了。在编辑过程中,单单就插入来说就有多个快捷键,例如 i,I,a,A,o,O 等,都可以从 normal 模式转化为 insert 模式,但是每个命令又有细微的差别,例如 a 可以理解为 append ,那么就是在光标都追加单词,而 i 可以记忆为 insert ,表示为光标前添加。 为了熟练的掌握这些命令,在刚刚开始使用的时候,嘴里或者心里都会默念一些 “口诀” 。 例如我修改引号内代码,就念 change inner " 手上对应的 c,i,⇧ '。 但是还有一个问题,你需要知道这些快捷键,你才能在心中默念出相关“口诀”,vim的相关命令比较多,而且同一键在不同的情况下代表的含义是不一样的,比如上述提到的 i,可以是 insert,还可以是 inner。 这些操作都不需要任何的插件,也不需要任何的设置,对于 vim 来说,这些操作都是一致的。 配置 这里我贴出我的 vim 配置。分别是 vscode 和 goland 的。 Vscode json { "vim.easymotion": true, "vim.incsearch": true, "vim.useSystemClipboard": true, "vim.useCtrlKeys": true, "vim.hlsearch": true, "vim.insertModeKeyBindings": [ { "before": [ "j", "j" ], "after": [ "<Esc>" ] } ], "vim.normalModeKeyBindingsNonRecursive": [ { "before": [ "<leader>", "d" ], "after": [ "d", "d" ] }, { "before": [ "<C-n>" ], "commands": [ ":nohl" ] }, { "before": [ "K" ], "commands": [ "lineBreakInsert" ], "silent": true }, { "before": [ "leader", "=" ], "commands": [ { "command": "editor.action.formatDocument" } ] }, { "before": [ "leader", "g" ], "commands": [ { "command": "go.test.generate.function" } ] } ], "vim.leader": "<space>", "vim.handleKeys": { "<C-a>": false, "<C-f>": false }, "editor.lineNumbers": "relative" } Goland (IDEA) text "" Source your .vimrc "source ~/.vimrc let mapleader=" " "" -- Suggested options -- " Show a few lines of context around the cursor. Note that this makes the " text scroll if you mouse-click near the start or end of the window. set scrolloff=5 set so=5 "set ideajoin "set idearefactormode=keep "set relativenumber set nu set showmode set relativenumber set clipboard+=unnamed set clipboard+=ideaput set virtualedit=onemore "set cursorline set keep-english-in-normal " Do incremental searching. set incsearch " Don't use Ex mode, use Q for formatting. map Q gq set surround set multiple-cursors set commentary set argtextobj " set easymotion set textobj-entire set ReplaceWithRegister set exchange "" -- Map IDE actions to IdeaVim -- https://jb.gg/abva4t "" Map \r to the Reformat Code action "" Map <leader>d to start debug "map <leader>d <Action>(Debug) "" Map \b to toggle the breakpoint on the current line "map \b <Action>(ToggleLineBreakpoint) " map <leader>f <Plug>(easymotion-s) " map <leader>e <Plug>(easymotion-f) nmap <leader>d <Action>(Debug) " Debug 当前方法 map <leader>r <Action>(RenameElement) " 重命名变量 noremap <leader>c <Action>(Stop) noremap <leader>z <Action>(ToggleDistractionFreeMode) " 禅模式 map <leader>s <Action>(SelectInProjectView) map <leader>a <Action>(Annotate) map <leader>h <Action>(Vcs.ShowTabbedFileHistory) map <S-Space> <Action>(GotoNextError) map <leader>b <Action>(ToggleLineBreakpoint) " 打断点 map <leader>o <Action>(FileStructurePopup) " Editor nmap <leader>= <Action>(ReformatCode) " Find more examples here: https://jb.gg/share-ideavimrc " 全局查找 noremap g/ <ESC>:action FindInPath<CR> noremap / <ESC>:action Find<CR> noremap <leader>g <ESC>:action Generate<CR> set ideajoin