mini.pick 实战指南:用 Neovim 单窗口通用选择器轻松实现文件、grep 与自定义拾取
2026/9/16 18:51:45 网站建设 项目流程

mini.pick 实战指南:用 Neovim 单窗口通用选择器轻松实现文件、grep 与自定义拾取

【免费下载链接】mini.nvimLibrary of 45+ independent Lua modules improving Neovim experience with minimal effort项目地址: https://gitcode.com/GitHub_Trending/mi/mini.nvim

导读

mini.pick 是 mini.nvim 库中负责"拾取任意元素"的独立模块:它用一个浮窗把任意数组变成可交互的"筛选 + 排序 + 预览 + 选择"界面,并内置文件、grep、缓冲区、帮助标签、CLI 输出、恢复上次会话等常用选择器,同时提供:Pick命令与vim.ui.select()集成。读完本文,你将掌握如何启动选择器、理解默认模糊匹配规则、按需改造映射与窗口样式、用MiniPick.source协议写出自定义选择器,以及在插件中安全复用MiniPickAPI。文档与源码依据为 readmes/mini-pick.md、doc/mini-pick.txt 和 lua/mini/pick.lua。

功能总览与设计理念

mini.pick 的核心设计理念可以概括为:任意数组(array of objects)→ 交互式过滤/排序/导航/预览 → 选择一个或多个条目。围绕这一点,模块提供了一组相互独立又彼此配合的能力:

  • 单窗口通用界面:任意元素数组都可作为拾取对象;
  • 按需切换的预览(Preview)与信息(Info)视图;
  • 交互式查询匹配(过滤 + 排序),默认算法为快速非阻塞的模糊匹配,并支持精确、锚定等特殊模式;
  • 内置选择器:文件、模式匹配(固定模式或实时反馈,均支持 glob 过滤)、缓冲区、帮助标签、CLI 输出、恢复上次选择器;
  • :Pick用户命令,配合可扩展的MiniPick.registry
  • vim.ui.select()实现,可整体替换 Neovim 原生的选择交互;
  • 丰富的内置动作:移动焦点、垂直/水平滚动、切换预览/信息、标记/取消标记、细化(refine)匹配等;
  • 精简而灵活的源(source)协议:条目、名称、工作目录、匹配算法、展示方式、预览、"选择"行为均可自定义;
  • 全局、缓冲区级、单次调用三个层级均可配置动作与按键;
  • 开箱即用地支持'ignorecase''smartcase'
  • 匹配缓存机制,在重复输入查询时提升响应速度。

从源码结构看,lua/mini/pick.lua(约 3800 行)把这些能力组织为:核心状态机H.pickers.active、配置系统MiniPick.config、公共 API(MiniPick.start/stop/refresh及一系列 getter/setter)、内置选择器表MiniPick.builtin,以及默认实现(MiniPick.default_match/default_show/default_preview/default_choose/default_choose_marked)。测试则集中在 tests/test_pick.lua(含 340 处相关断言),可作为行为验证的参考。

快速上手:三种启动选择器的方式

1. 用MiniPick.start()自定义源

最底层的方式是直接调用MiniPick.start(),其opts.source定义数据来源。例如拾取当前目录下的所有条目:

MiniPick.start({ source = { items = vim.fn.readdir('.') } })

MiniPick.start({opts})返回被选中时的当前条目;若被中止则返回nil。若调用时已有激活的选择器,旧选择器会被妥善停止,新选择器将在主事件循环中"很快"启动(源码见MiniPick.start,lua/mini/pick.lua)。

2. 直接调用内置选择器

MiniPick.builtin表内置了最常用的拾取器,无需自己定义源:

MiniPick.builtin.files({ tool = 'git' })

3. 通过:Pick命令

MiniPick.setup()会创建:Pick用户命令,它从MiniPick.registry读取选择器名称作为必填参数,并把其余<f-args>展开后合并成一张配置表传给该选择器:

:Pick files tool='git' :Pick grep pattern='<cword>'

从实现看,MiniPick.setup()会把MiniPick.builtin中的全部选择器自动注册进MiniPick.registry(见 lua/mini/pick.lua)。记住:无论用哪种方式安装,都必须调用require('mini.pick').setup()才能启用模块功能(它会创建全局MiniPick表、用户命令并设置vim.ui.select)。

界面构成:单窗口三种视图

mini.pick 把传统选择器的"多窗口"压缩进一个窗口,包含三种可切换视图:

  • Main(主视图):展示当前查询的匹配结果,默认行为;
  • Preview(预览视图):预览当前条目,<Tab>切换;
  • Info(信息视图):展示选择器与状态信息,<S-Tab>切换。

窗口边框承担了两处视觉信息:

  • 左上角是当前查询提示(prompt),其中竖线字符(默认)表示光标(caret)位置;
  • 底部左侧显示选择器名称,底部右侧显示状态信息,格式为:
<当前匹配索引> | <匹配总数> | <已标记数> / <条目总数>

当选择器处于忙碌状态(例如条目尚未就绪或匹配计算进行中),窗口边框会在空闲超过config.delay.busy毫秒后切换为MiniPickBorderBusy高亮,提供明确的计算反馈。全部可定制的高亮组清单(如MiniPickBorderMiniPickMatchCurrentMiniPickMatchRangesMiniPickPreviewLine等)见 doc/mini-pick.txt,修改方式为vim.api.nvim_set_hl(0, 'MiniPickMatchCurrent', { ... })

交互生命周期:查询匹配与常用按键

选择器激活后,输入字符即开始过滤与排序(内部通过MiniPick.default_match()计算,query是已按键字符构成的数组)。默认匹配规则如下:

  • 查询以'开头 →精确匹配(不允许字符间有间隔);
  • 查询以^开头 →精确匹配且锚定开头
  • 查询以$结尾 →精确匹配且锚定结尾
  • 查询以*开头 →强制模糊匹配(忽略其他模式);
  • 其余情况 → 模糊匹配(允许间隔);
  • 排序先最小化"匹配宽度",再最小化"匹配起始位置",不额外偏好字符串中的任何位置。

常用按键动作如下(完整映射见:h MiniPick-actions):

  • <C-n>/<Down>下移,<C-p>/<Up>上移;
  • <Left>/<Right>左右移动 prompt 光标;
  • <S-Tab>切换信息窗口(内含全部可用映射的说明);
  • <Tab>切换预览;
  • <C-x>/<C-a>切换当前条目 / 全部条目的标记状态;
  • <C-Space>/<M-Space>把全部匹配或已标记条目作为新选择器的条目(refine);
  • <CR>/<M-CR>选择当前条目 / 已标记条目;
  • <Esc>/<C-c>停止选择器。

生命周期实现细节

源码揭示了两个值得注意的实现事实(见 doc/mini-pick.txt):

  • 按键处理通过专门的"按键查询"进程完成,因此常规映射在选择器内不生效,且选择器窗口在显示期间必须是当前窗口;切换窗口焦点会导致选择器(延迟一小段时间后)自动停止,非选择器相关的屏幕变化需要显式:redraw
  • 任意选择器都是非阻塞的,但会等待返回被选中的条目。例如local file = MiniPick.builtin.files()在界面显示期间仍允许执行其他操作,最后把file赋值为选中项。

默认配置逐项解析

MiniPick.config是模块的默认配置(无需复制进setup(),会自动生效):

{ -- 延迟(毫秒;至少为 1) delay = { -- 强制异步行为之间的间隔 async = 10, -- 计算开始到给出视觉反馈之间的间隔 busy = 50, }, -- 执行动作的按键。见 `:h MiniPick-actions` mappings = { caret_left = '<Left>', caret_right = '<Right>', choose = '<CR>', choose_in_split = '<C-s>', choose_in_tabpage = '<C-t>', choose_in_vsplit = '<C-v>', choose_marked = '<M-CR>', delete_char = '<BS>', delete_char_right = '<Del>', delete_left = '<C-u>', delete_word = '<C-w>', mark = '<C-x>', mark_all = '<C-a>', move_down = '<C-n>', move_start = '<C-g>', move_up = '<C-p>', paste = '<C-r>', refine = '<C-Space>', refine_marked = '<M-Space>', scroll_down = '<C-f>', scroll_left = '<C-h>', scroll_right = '<C-l>', scroll_up = '<C-b>', stop = '<Esc>', toggle_info = '<S-Tab>', toggle_preview = '<Tab>', }, -- 通用选项 options = { -- 是否从下往上展示内容 content_from_bottom = false, -- 是否缓存匹配结果(重复提示时更快,但更占内存) use_cache = false, }, -- 源定义。见 `:h MiniPick-source` source = { items = nil, name = nil, cwd = nil, match = nil, show = nil, preview = nil, choose = nil, choose_marked = nil, }, -- 窗口相关选项 window = { -- 浮窗配置(表或返回表的可调用对象) config = nil, -- prompt 中用作光标的字符串 prompt_caret = '▏', -- prompt 的前缀字符串 prompt_prefix = '> ', }, }

关键配置项含义

  • delay.async:强制异步行为(如预览中的:redrawMiniPick.poke_is_picker_active()轮询)的间隔。越小体验越顺滑,但计算开销越大;
  • delay.busy:计算开始到边框切换为MiniPickBorderBusy的间隔。越小反馈越快,但更容易产生闪烁感;
  • options.content_from_bottom:是否从底部向上展示内容(最佳匹配出现在底部),默认false
  • options.use_cache:按 prompt(拼接后的查询)缓存匹配结果,重复输入(如删除查询字符)时响应更快,代价是内存占用增加,默认false
  • config.source:作为源规格的全局兜底,例如可替换默认match实现,或让部分内置选择器不显示图标(见下文示例);
  • window.config:主窗口浮窗配置,可为表或返回表的函数,window.prompt_caret/window.prompt_prefix控制 prompt 的光标字符与前缀。

setup()还支持缓冲区级覆盖:在vim.b.minipick_config中放入与MiniPick.config同结构的表即可对当前缓冲区生效。整体优先级为:调用时的opts>vim.b.minipick_config>MiniPick.config

内置选择器详解

MiniPick.builtin.files():拾取文件

递归列出所有子目录中的文件。按rgfdgit的顺序尝试可用的 CLI 工具,三者皆无时回退到基于vim.fs.dir()的实现:

MiniPick.builtin.files() -- 自动选择可用工具 MiniPick.builtin.files({ tool = 'git' }) -- 强制使用 git

local_opts.tool取值为"rg" | "fd" | "git" | "fallback"

MiniPick.builtin.grep()/grep_live():拾取模式匹配

grep()递归搜索所有子目录中的模式匹配;grep_live()则把 prompt 直接当作模式,实时反馈匹配结果。二者均支持toolrg/gitgrep_live无工具时直接报错以保护性能)、globs(glob 数组,如{ '*.lua', 'lua/**' },仅rggit支持)与method"regex""plain")参数:

MiniPick.builtin.grep({ pattern = '<cword>', globs = { '*.lua' } }) MiniPick.builtin.grep_live()

grep_live中,<C-o>可追加 glob 限制,<C-e>可切换匹配方法;grep()未给pattern时会通过MiniInput.get()(若启用)或input()交互询问。二者都会通过向 CLI 强制传参来尊重'ignorecase''smartcase'

其余内置选择器

  • buffers():拾取缓冲区,include_current(默认true)与include_unlisted(默认false)可调;内置无缓冲区操作映射,示例中通过自定义映射实现<C-d>删除当前缓冲区(见 doc/mini-pick.txt);
  • help():拾取帮助标签,选中后直接执行:help(支持水平/垂直/标签页打开,default_split可配);
  • cli():执行命令行工具并把输出构造成条目,例如MiniPick.builtin.cli({ command = { 'echo', 'a\nb\nc' } })
  • resume():恢复最近一次选择器,得益于源中的cwd固定机制而更稳健。

提示:更多选择器(诊断、文件浏览器、Git、历史记录、LSP、Tree-sitter 等)由 mini.extra 提供,注册为MiniExtra.pickers.*,并自动出现在MiniPick.registry中(见 lua/mini/extra.lua)。

源(source)协议:构建自定义选择器的核心

源(source)是 mini.pick 的灵魂:它由itemsnamecwdmatchshowpreviewchoosechoose_marked八个字段构成,可定义在全局配置、缓冲区配置或单次调用的opts.source中(优先级递增)。

items:条目定义

可为以下任意一种:

  • 数组:元素可以是任意类型;
  • nil:选择器等待显式调用MiniPick.set_picker_items()
  • 可调用对象:启动时调用一次(cwd会被设置为当前目录)。

匹配基于条目的字符串表示(称为stritems),计算规则为:可调用对象调用一次取输出;字符串直接用;表取text字段(若存在);否则用vim.inspect()的输出。例如:

items = { 'aaa.txt', { text = 'bbb' }, function() return 'ccc' end } -- 对应 stritems: { 'aaa.txt', 'bbb', 'ccc' }

为让MiniPick.default_show()default_preview()default_choose()开箱即用地工作,文档建议了常见条目类型(doc/mini-pick.txt):

  • 路径:字符串或表的path字段,可为绝对/相对(相对source.cwd)/URI 格式,如'aaa.txt'{ path = 'aaa.txt' }
  • 缓冲区:数字/字符串缓冲区 id,或bufnr/buf_id/buf字段,如{ bufnr = 1 }
  • { path = ..., lnum = N }"<path>\0<line>"格式;
  • 位置{ path = ..., lnum = N, col = N }"<path>\0<line>\0<col>"格式;
  • 区域lnum/col/end_lnum/end_col字段(起止行列均从 1 开始,末行包含、末列不包含),命名与getqflist()类似。

match:自定义匹配算法

match(stritems, inds, query)接收全部 stritems、当前匹配索引数组与查询字符数组,应返回匹配索引数组(同步),或通过MiniPick.set_picker_match_inds()异步设置。若查询满足"忽略大小写"条件(仅设'ignorecase',或'ignorecase'/'smartcase'均设且查询全小写),stritems 与 query 会被统一转为小写,从而自动获得大小写不敏感支持。简单的精确匹配示例:

local match_exact = function(stritems, inds, query) local prompt_pattern = vim.pesc(table.concat(query)) local f = function(i) return stritems[i]:find(prompt_pattern) ~= nil end return vim.tbl_filter(f, inds) end

show:自定义展示

show(buf_id, items_to_show, query)负责把待展示条目写入缓冲区(每条一行、从第一行开始,不要依赖content_from_bottom),通常还要高亮匹配部分:

local show_prepend = function(buf_id, items_arr, query) local lines = vim.tbl_map(function(x) return 'Item: ' .. x end, items_arr) vim.api.nvim_buf_set_lines(buf_id, 0, -1, false, lines) end

previewchoose/choose_marked

  • preview(buf_id, item)把条目渲染进预览缓冲区(每次预览会新建 scratch 缓冲区);默认实现会按条目类型展示:文件/缓冲区显示开头、目录列出内容、行/位置/区域定位到目标处,其余用vim.inspect()展示;
  • choose(item)定义"选中"行为,返回nil/false停止选择器,返回其他值则继续;默认实现为:文件用bufadd()打开并定位光标、缓冲区在当前目标窗口打开、目录用:edit、其余在命令行打印vim.inspect()结果;
  • choose_marked(items_marked)处理多选,默认在条目含文件/缓冲区时打开 quickfix 列表(list_type可选"quickfix""location"),否则对第一个条目执行source.choose

非阻塞匹配与 API 辅助

对于耗时匹配,可用MiniPick.poke_is_picker_active()实现协程化非阻塞逻辑:它返回选择器是否激活,且在协程中执行时会先yield再经vim.schedule()尽快恢复,便于在计算中途感知查询更新并放弃过期结果。源码中的非阻塞精确匹配示例见 doc/mini-pick.txt。此外还提供MiniPick.get_querytick()(唯一查询标识,查询变更/选择器启停时更新)及一整套 getter/setter(get_picker_itemsset_picker_items_from_cliset_picker_match_indsset_picker_target_windowget_picker_state等),均可供自定义源与插件集成使用。

动作系统:内置动作与自定义映射

选择器激活时,mappings表把按键映射为动作。动作分为两类:

  • 内置动作:存在于默认config.mappings中,只能用不同按键覆盖;
  • 自定义动作:值为包含char(单个触发字符)与func(无参可调用函数)字段的表;函数返回值决定是否停止选择器(nil/false继续,其余如true停止)。

内置动作按语义分为:Caret(左右移动 prompt 光标)、Choose(choose/choose_in_split/choose_in_tabpage/choose_in_vsplit/choose_marked)、Delete(delete_char/delete_char_right/delete_left/delete_word)、Mark(mark/mark_all,标记跨查询保留)、Move(move_down/move_start/move_up,上下环绕;<Down>/<Up>/<Home>为硬编码替代键)、Paste(paste,支持寄存器粘贴及<C-f>/<C-w>/<C-a>/<C-l>特例)、Refine(refine/refine_marked)、Scroll(垂直/水平滚动)、Stop(stop<C-c>恒可停止)、Toggle(toggle_info/toggle_preview)。

Refine 动作值得单独说明:它把当前匹配(或已标记)条目作为全新选择器的条目、重置查询、并把source.match恢复为配置值。典型场景是连续"收窄"查询——例如要同时精确包含helloworld(顺序不限):输入'hello后按<C-Space>,再输入'world。自定义映射示例:

require('mini.pick').setup({ mappings = { execute = { char = '<C-e>', func = function() vim.cmd(vim.fn.input('Execute: ')) end, }, }, })

实战配置示例

切换按键布局

toggle_infomove_down/move_up对调(适配部分用户的肌肉记忆):

require('mini.pick').setup({ mappings = { toggle_info = '<C-k>', toggle_preview = '<C-p>', move_down = '<Tab>', move_up = '<S-Tab>', }, })

禁用内置选择器的图标

内置选择器默认通过 mini.icons(或回退 nvim-web-devicons)为路径条目显示图标。仅用default_show作为source.show即可关闭(default_showshow_icons默认值为false):

local pick = require('mini.pick') pick.setup({ source = { show = pick.default_show } })

定制窗口样式

  • 双线边框{ window = { config = { border = 'double' } } }
  • "光标提示气泡"(跟随光标弹出):
{ window = { config = { relative = 'cursor', anchor = 'NW', row = 0, col = 0, width = 40, height = 20, }, }, }
  • 屏幕居中(按 0.618 黄金比例取宽高,窗口尺寸变化时动态计算):
local win_config = function() local height = math.floor(0.618 * vim.o.lines) local width = math.floor(0.618 * vim.o.columns) return { anchor = 'NW', height = height, width = width, row = math.floor(0.5 * (vim.o.lines - height)), col = math.floor(0.5 * (vim.o.columns - width)), } end { window = { config = win_config } }

自定义 registry 选择器

MiniPick.registry:Pick命令的数据源,可自由扩展。例如注册一个"拾取注册表里其他选择器"的选择器(见 doc/mini-pick.txt):

MiniPick.registry.registry = function() local items = vim.tbl_keys(MiniPick.registry) table.sort(items) local source = { items = items, name = 'Registry', choose = function() end } local chosen_picker_name = MiniPick.start({ source = source }) if chosen_picker_name == nil then return end return MiniPick.registry[chosen_picker_name]() end

再如让:Pick files支持cwd参数:

MiniPick.registry.files = function(local_opts) local opts = { source = { cwd = local_opts.cwd } } local_opts.cwd = nil return MiniPick.builtin.files(local_opts, opts) end

vim.ui.select()的集成

MiniPick.setup()会自动把vim.ui.select替换为MiniPick.ui_select,使依赖该接口的插件(如 LSP code actions、:LspCodeAction等)直接获得 mini.pick 的交互体验。若希望保留原实现,可在setup()后手动还原:

local ui_select_orig = vim.ui.select require('mini.pick').setup() vim.ui.select = ui_select_orig

MiniPick.ui_select兼容vim.ui.select()签名并略有增强:支持opts.preview_item(返回预览行数组,或在 Neovim >= 0.12.3 时返回标准预览数据),并接受第四个参数start_opts定制MiniPick.start()调用:

vim.ui.select = function(items, opts, on_choice) local start_opts = { window = { config = { width = vim.o.columns } } } return MiniPick.ui_select(items, opts, on_choice, start_opts) end

插件作者注意事项与事件钩子

若要在其他插件中复用 mini.pick:

  • 优先使用vim.ui.select()以获得更广的用户覆盖;仅在确需同步选择或额外能力时使用MiniPick.start()
  • 使用前检查_G.MiniPick ~= nil,确保用户已显式配置该模块(这正是 lua/mini/pick.lua 中setup()导出全局表的原因)。

模块还会在常见时机触发三个User自动命令事件,便于外部集成与自定义:

  • MiniPickStart:选择器启动后;
  • MiniPickMatch:查询匹配更新或条目设置后;
  • MiniPickStop:选择器停止前。

依赖与安装

mini.pick 在缺少任何依赖时都能工作,但以下依赖可提供完整体验:

  • 启用 mini.icons 模块以在路径条目旁显示图标(可回退到 nvim-tree/nvim-web-devicons,或干脆不显示图标);
  • 安装 ripgrep(推荐,足以驱动files/grep/grep_live全部三个内置选择器);fd 仅用于files,git 则足以覆盖三者。

注意:CLI 工具只会被以获取条目所需的基础参数调用,如需自定义输出(如忽略特定目录),应使用工具自身的配置方案(如 ripgrep 配置文件、fd 的排除规则、.gitignore),而不要在 mini.pick 层做文章。

安装方式

作为 mini.nvim 库一部分安装(推荐),或作为独立仓库安装;可选main分支(默认,最新开发版,处于 beta 测试阶段)或stable分支(仅在发布时更新)。以 lazy.nvim 独立安装为例:

-- main 分支(开发版) { 'nvim-mini/mini.pick', version = false }, -- stable 分支(稳定版) { 'nvim-mini/mini.pick', version = '*' },

在 Neovim 0.12 及以上可用vim.pack.add({ 'https://github.com/nvim-mini/mini.pick' }),旧版本可用 mini.deps 的add('nvim-mini/mini.pick')安装后务必调用require('mini.pick').setup()。Windows 用户若遇路径过长报错,可启用git config --system core.longpaths true后重装,或换用更短的安装路径。

结语

mini.pick 把"从任意数组中选择一个或多个元素"这一高频交互抽象为统一、可扩展、非阻塞的选择器框架:内置选择器覆盖日常文件与搜索需求,source协议允许深度定制数据源与行为,vim.ui.select()集成让其能力辐射到整个插件生态。从 lua/mini/pick.lua 的 API 设计与 tests/test_pick.lua 的测试覆盖来看,其核心价值在于"以最小的心智负担获得可编程的拾取体验"——建议从:Pick files:Pick grep开始,再逐步深入自定义源与动作。

【免费下载链接】mini.nvimLibrary of 45+ independent Lua modules improving Neovim experience with minimal effort项目地址: https://gitcode.com/GitHub_Trending/mi/mini.nvim

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询