mini.nvim 的 mini.visits 模块:持久化文件系统访问追踪与复用实战指南
mini.nvim 的 mini.visits 模块:持久化文件系统访问追踪与复用实战指南
本文围绕 mini.nvim 库中的 mini.visits 模块(readmes/mini-visits.md),系统讲解其如何按项目目录持久化追踪文件与目录的访问记录,以及如何通过排序、标签(label)、vim.ui.select() 选择与历史迭代等能力复用这些访问数据。读完本文,你将掌握该模块的完整配置项、索引数据结构、核心 API 用法,并能在自己的 Neovim 配置中搭建"最近文件/常用文件/分组文件"等实战工作流。
模块定位:追踪并复用文件系统访问
mini.visits 是 mini.nvim 库(README.md)中的一个独立 Lua 模块,核心能力可以概括为两句话:持久化地按项目目录追踪文件系统访问(包括文件和目录),以及提供一组通用函数让这些访问数据可以被复用(列出来、选择、迭代、打标签、存自定义数据)。它不依赖任何 UI 框架,选路径走 Neovim 内置的 vim.ui.select(),因此可以轻松接入你自己的选择器或 picker。
模块的主要特性(见 readmes/mini-visits.md 的 Features 一节):
- 按项目目录持久化追踪:同时记录文件和目录的访问,访问索引(visit index)以人类可读、可手工编辑的形式存储。
- 写入时自动规范化:每次写入磁盘前都会对索引做规范化,保证其中只保留相关信息,具体规则可以自定义(见
:h MiniVisits.normalize())。 - 内置标签能力:可以持久化地为某个路径添加/移除标签(如 "core"、"tmp"),便于后续按标签分组复用。
- 复用访问数据的导出函数:
- 用自定义过滤与排序列出已访问的路径/标签(默认使用 "robust frecency" 排序),可直接作为 picker 的数据源;
- 用
vim.ui.select()选择已访问的路径/标签; - 按方向("forward"、"backward"、"first"、"last")在访问路径间迭代移动。
- 手动更新索引的
*_index()系列函数:允许持久化追踪任何用户自定义信息。
两条重要设计说明
阅读模块文档时需要先记住两条"约定",它们贯穿了后续所有功能:
- 数据只存在于两处:会话内的 Lua 变量(用于快速操作)和
config.store.path指定的磁盘文件(用于持久化)。会话内的修改只有到 Neovim 退出(或主动调用写盘函数)时才会落盘。 - 不自动感知路径重命名/移动:因为不存在通用的检测手段。若文件/目录被改名或移动,通常需要手动干预索引(在下次写盘、通常是关闭当前会话之前完成),否则下次规范化会把旧路径视为已删除并从索引中移除。模块为此提供了
MiniVisits.rename_in_index()辅助函数;如果改名/移动是通过 mini.files 完成的,索引会被自动更新(该逻辑在 lua/mini/visits.lua 中通过监听MiniFilesActionRename/MiniFilesActionMove用户事件实现)。
工作原理:两步式的访问追踪机制
mini.visits 的访问追踪遵循"防抖 + 落定"的思想,分两步进行(对应 :h MiniVisits-overview,实现见 lua/mini/visits.lua 的 H.autoregister_visit):
第一步:事件触发计时器。 每次触发 config.track.event(默认是 BufEnter)时,模块会(重新)启动一个计时器,延迟 config.track.delay 毫秒(默认 1000ms)后再真正登记访问。不立即登记的目的是允许用户分多步导航到目标缓冲区,例如连续使用 :bnext / :bprevious 切换时,中间经过的缓冲区不会被计入。
第二步:延迟结束且无新事件时登记访问。 当延迟时间过去且期间没有再触发任何追踪事件(意味着用户已经"落定"在某个缓冲区上)时,会调用 MiniVisits.register_visit(),但必须同时满足以下条件:
- 模块未被禁用(见下文"禁用机制");
- 当前缓冲区是普通缓冲区(
buftype为空)且名字非空(缓冲区名即访问路径); - 访问路径与最近一次追踪的路径不同。这一条允许用户临时进入非普通缓冲区(如 help、terminal 等)而不虚增访问次数。
每次访问登记到**当前工作目录(cwd)**下,效果是让该路径的 count 加一、latest 更新为当前时间。
计时器与自动化细节
从源码看,模块在 H.timers.track 上使用 vim.loop.new_timer()(lua/mini/visits.lua),并在 H.create_autocommands() 中统一注册三类自动命令(lua/mini/visits.lua):
- 追踪事件自动命令:仅当
config.track.event非空字符串时注册; VimLeavePre自动写盘:仅当config.store.autowrite为真时执行;- mini.files 重命名/移动联动:监听用户事件自动调用
rename_in_index()并set_index()。
手动关闭自动追踪
如果不想自动追踪,可以给 config.track.event 传空字符串 '',此后完全由用户自行调用 MiniVisits.register_visit() 来登记访问。
访问索引(Visit Index)规范
索引是理解整个模块数据流的核心。它是一张两层嵌套的表(见 :h MiniVisits-index-specification,文档示例在 doc/mini-visits.txt):
- 第一层 key 是项目目录路径(cwd),即在该目录下登记过访问的所有数据都挂在对应的 cwd 之下;
- 第二层 key 是实际访问路径,其 value 是包含访问数据的表,必须满足:
| 字段 | 类型 | 含义 |
|---|---|---|
count |
number(必填) | 该路径在特定 cwd 下被访问的次数 |
latest |
number(必填) | 该路径在特定 cwd 下最近一次访问的时间戳(默认由 os.time() 计算,精确到秒) |
labels |
table(可选) | 以字符串标签为 key、true 为 value 的表,表示该路径在此 cwd 下拥有的标签 |
示例索引数据:
{
['/home/user/project_1'] = {
['/home/user/project_1/file'] = { count = 3, latest = 1699796000 },
['/home/user/project_1/subdir'] = {
count = 10, latest = 1699797000, labels = { core = true },
},
},
['/home/user/project_2'] = {
['/home/user/project_1/file'] = {
count = 0, latest = 0, labels = { other = true },
},
['/home/user/project_2/README'] = { count = 1, latest = 1699798000 },
},
}
注意三条规则:
- 所有路径均为绝对路径;
- 访问路径不一定属于对应 cwd 的子树;
count和latest在理论上可以是任意数值(整数、小数、负数均可),内置追踪会产生正整数count和来自os.time()的latest。
存储方式
落盘时,访问索引是一个返回索引表的 Lua 代码文件(存储实现见 lua/mini/visits.lua 的 write_index() 与 L926-L933 的 read_index())。写入时模块会先做规范化,再确保父目录存在,最后用 vim.inspect 序列化并以 return ... 开头写入;读取时直接通过 dofile() 执行该文件。因此它完全可以手工编辑——只要编辑结果是合法的 Lua 代码即可。
需要特别提醒:该存储方式不支持两个以上并行的 Neovim 进程共享同一个索引路径。如果多个 Neovim 实例同时使用同一存储路径,最后写入的那个会保留自己的访问历史,其余实例的历史会丢失。
规范化(Normalization)
为保证索引中大部分是相关数据,索引在每次 write_index() 时会自动规范化,也可以随时调用 MiniVisits.normalize_index() 手动规范化。规范化行为由 config.store.normalize 配置,默认使用 MiniVisits.gen_normalize.default(),其步骤(实现见 lua/mini/visits.lua)为:
- 剪枝(Prune):移除过时的访问——若
count低于剪枝阈值则从该 cwd 中移除该访问条目(在其它 cwd 下仍可保留);若第一层(cwd)或第二层(path)key 对应的路径在磁盘上已不存在,则移除整个关联值。注意:带标签的访问不会被自动剪枝(即使count = 0)。 - 衰减(Decay):让访问"更过时"。当某个 cwd 下所有
count之和超过衰减阈值时,将所有count乘以同一个系数,使新的总数等于一个较小的目标值(只保留两位小数,所以总和未必精确)。衰减与剪枝配合,能自动淘汰长期未访问、不再相关的路径。 - 再次剪枝:确保衰减之后没有低于阈值的过时路径。
gen_normalize.default() 的参数与默认值:decay_threshold = 1000、decay_target = 800、prune_threshold = 0.5、prune_paths = true。相关剪枝/衰减的辅助实现在 lua/mini/visits.lua。
安装与基础配置
安装方式
该插件既可以作为 mini.nvim 库的一部分安装(官方推荐),也可以作为独立 Git 仓库安装。有两个分支可选:
main(默认,推荐):最新的开发版本。自上一个稳定版以来的改动都应视为处于 beta 测试阶段(即已通过 alpha 测试、基本稳定)。stable:仅在正式发布时更新,代码经过了main分支的公开 beta 测试。
以下是几种常见安装方式的代码片段(任选其一,不要重复安装):
方式一:vim.pack(Neovim 0.12 及以上,推荐)
-- 独立插件,main 分支
vim.pack.add({ 'https://github.com/nvim-mini/mini.visits' })
-- 独立插件,stable 分支
vim.pack.add({
{ src = 'https://github.com/nvim-mini/mini.visits', version = 'stable' },
})
方式二:mini.deps(Neovim 0.12 之前)
-- main 分支
add('nvim-mini/mini.visits')
-- stable 分支
add({ source = 'nvim-mini/mini.visits', checkout = 'stable' })
方式三:lazy.nvim
-- main 分支
{ 'nvim-mini/mini.visits', version = false },
-- stable 分支
{ 'nvim-mini/mini.visits', version = '*' },
重要:安装后不要忘记调用 require('mini.visits').setup() 来启用功能。
Windows 用户注意:若遇到文件路径过长导致的报错(如 error: unable to create file <some file name>: Filename too long),可尝试:执行 git config --system core.longpaths true 后重新安装;或者把插件安装到路径更短的位置。
默认配置
setup() 接受一个 config 表,不传则使用以下默认配置(见 readmes/mini-visits.md 的 Default config 一节,源码中对应 lua/mini/visits.lua):
require('mini.visits').setup({
-- 访问索引如何转换为路径列表
list = {
-- 决定包含哪些路径的谓词(默认全部包含)
filter = nil,
-- 基于访问数据对路径排序(默认使用 robust frecency)
sort = nil,
},
-- 是否关闭非错误类反馈信息的显示
silent = false,
-- 访问索引如何存储
store = {
-- 是否在 Neovim 关闭前写入所有访问
autowrite = true,
-- 用于确保写入的索引保持相关性的函数
normalize = nil,
-- 存储访问索引的路径
path = vim.fn.stdpath('data') .. '/mini-visits-index',
},
-- 访问追踪如何进行
track = {
-- 在此事件上启动访问登记计时器
-- 传空字符串('')则不做自动追踪
event = 'BufEnter',
-- 事件发生到登记访问之间的防抖延迟(毫秒)
delay = 1000,
},
})
配置参数逐项解析
下面结合 doc/mini-visits.txt 与源码逐项说明各配置的作用。
config.list:索引 → 路径列表
list 定义默认情况下把索引转换为路径列表的方式,影响 list_paths()、select_path()、iterate_paths() 等函数。
list.filter:一个可调用对象,接收一条 path data 并返回true表示该路径应出现在列表中。默认是MiniVisits.gen_filter.default()(恒返回true,即不过滤)。list.sort:一个可调用对象,接收 path data 数组并返回排序后的数组(至少是包含<path>字段的表)。默认是MiniVisits.gen_sort.default()。注意:sort不仅可以用来排序,也可以用来过滤,甚至返回与输入无关的路径。
Path data 的结构:每条路径数据是一个表,至少包含以下字段:path(string,访问的绝对路径)、count(number,访问次数)、latest(number,最近访问时间戳)、labels(table|nil,标签表,字符串 key 配 true value)。如果用户在索引条目中自定义了其它字段,也会一并携带。
模块内置了两个过滤器:
MiniVisits.gen_filter.default():恒返回true,不做过滤;MiniVisits.gen_filter.this_session():只保留本次会话期间访问过的路径(判断依据是latest >= session_start_time,会话开始时间记录在 lua/mini/visits.lua 的缓存中)。
config.silent
布尔值,控制是否显示非错误类反馈(如添加/移除标签时的提示信息)。默认 false(显示)。模块内部通过 H.echo() 统一输出,见 lua/mini/visits.lua。
config.store:持久化
store.autowrite:布尔值,是否在VimLeavePre事件时把访问数据写入磁盘。默认true。store.normalize:可调用对象,接收访问索引并返回"规范化后"的索引,用于保证索引最新且只含相关数据(例如决定过时/无关的访问如何被"遗忘")。默认是MiniVisits.gen_normalize.default()。store.path:索引写入的路径(位于$XDG_DATA_HOME下的mini-visits-index文件)。设为空字符串可禁用一切写盘(包括VimLeavePre上的自动写入)。
config.track:追踪
track.event:触发追踪的 Neovim 事件名。默认BufEnter;设为空字符串可禁用自动追踪。track.delay:事件触发到访问自动登记之间的延迟(毫秒)。默认 1000,目的是让用户在缓冲区之间快速导航时不被记录中间状态。
缓冲区局部配置与禁用机制
- 缓冲区局部配置:可在
vim.b.minivisits_config中提供与MiniVisits.config同构的表,覆盖该缓冲区内的运行时配置(模块在H.get_config()中通过vim.tbl_deep_extend合并,见 lua/mini/visits.lua)。 - 禁用:设置
vim.g.minivisits_disable(全局)或vim.b.minivisits_disable(针对缓冲区)为true即可禁用。由于使用场景繁多,具体的禁用规则(例如"仅在特定文件类型下禁用")由用户自行编写(判定逻辑在 lua/mini/visits.lua)。
核心 API 使用指南
setup() 会在全局创建 MiniVisits 表,既可用于 Lua 脚本,也可通过 :lua MiniVisits.* 手动调用。以下按功能分类介绍(完整签名见 doc/mini-visits.txt)。
登记与维护访问
MiniVisits.register_visit(path, cwd):登记一次访问。步骤为:确保 path-cwd 条目存在 →count加 1 →latest设为当前时间。path默认取当前缓冲区的路径(须为普通缓冲区,否则报错);cwd默认取当前工作目录。MiniVisits.add_path(path, cwd):确保 path-cwd 条目存在(已存在则不动,不存在则以count = 0, latest = 0创建)。path传空字符串表示"该 cwd 下所有已访问路径";cwd传空字符串表示"所有 cwd"。MiniVisits.remove_path(path, cwd):从索引中删除一个或多个条目。注意它只影响会话内变量,需要调用MiniVisits.write_index()才会持久化。MiniVisits.add_label(label, path, cwd)/MiniVisits.remove_label(label, path, cwd):为路径添加/移除标签。label传nil时会通过vim.ui.input()询问用户,并自动从现有标签中提供补全(add_label补全当前 cwd 下所有标签,remove_label补全目标路径上的标签);移除最后一个标签时会同时移除labels字段。
列出与选择
MiniVisits.list_paths(cwd, opts):把某个 cwd 的索引转换成有序的已访问路径数组,支持自定义filter与sort。返回路径字符串数组。使用示例(来源 doc/mini-visits.txt):
-- 按从新到旧排序
local sort_recent = MiniVisits.gen_sort.default({ recency_weight = 1 })
MiniVisits.list_paths(nil, { sort = sort_recent })
-- 跨所有 cwd、按访问次数从多到少排序
local sort_frequent = MiniVisits.gen_sort.default({ recency_weight = 0 })
MiniVisits.list_paths('', { sort = sort_frequent })
-- 过滤掉隐藏文件
local is_not_hidden = function(path_data)
return not vim.startswith(vim.fn.fnamemodify(path_data.path, ':t'), '.')
end
MiniVisits.list_paths(nil, { filter = is_not_hidden })
MiniVisits.list_labels(path, cwd, opts):把指定 path-cwd 组合的索引转换成标签数组,按标签出现频率从高到低排序。filter支持自定义(sort不参与标签列表,但select_label()会把opts继续转发给后续的select_path())。MiniVisits.select_path(cwd, opts):用vim.ui.select()展示list_paths()的结果,选中后打开对应文件。展示项会相对 cwd 缩短显示(H.short_path,见 lua/mini/visits.lua)。MiniVisits.select_label(path, cwd, opts):先用vim.ui.select()选择标签,再调用select_path()从带该标签的路径子集中选择。- 若使用了 mini.pick,可考虑
MiniExtra.pickers.visit_paths()与MiniExtra.pickers.visit_labels()(实现在 lua/mini/extra.lua)。
沿历史迭代
-
MiniVisits.iterate_paths(direction, cwd, opts):先通过list_paths()计算目标路径的有序数组,再根据当前缓冲区路径确定当前索引,最后按方向迭代若干步。direction取值为:"first":从索引 0 向前,单次迭代落到第一个路径;"backward":从当前索引向后;"forward":从当前索引向前;"last":从末尾之后的位置向后,单次迭代落到最后一个路径。
opts支持:filter、sort(转发给list_paths())、n_times(步数,默认v:count1,即支持count前缀)、wrap(是否在列表边缘回绕,默认false)。若当前缓冲区的路径不在列表中,则前向迭代从第一个元素开始、后向迭代从最后一个元素开始。该函数主要设计为映射(mapping)使用。例如假设底层数组为
{ "file1", "file2", "file3" }:MiniVisits("first")聚焦到file1;MiniVisits("backward", { n_times = 2 })从file3落到file1;MiniVisits("forward", { n_times = 10 })从file1落到file3;MiniVisits("last", { n_times = 4, wrap = true })落到file3。一个值得注意的实现细节:该函数导航本身不会被追踪(见 lua/mini/visits.lua),实现方式是在打开目标文件前临时把
vim.g.minivisits_disable置为true、完成后恢复。这样连续迭代不会污染底层路径列表。
*_index() 系列:手动操作索引
MiniVisits.get_index():返回当前活动索引的深拷贝。MiniVisits.set_index(index):用给定表替换当前活动索引。MiniVisits.reset_index():把当前索引重置为磁盘上read_index()的结果(读取失败则不做任何事)。MiniVisits.normalize_index(index):对输入索引应用config.store.normalize,返回规范化结果。MiniVisits.read_index(store_path):从磁盘读取索引(默认读config.store.path);路径为空字符串或文件不可读时返回nil。MiniVisits.write_index(store_path, index):规范化 → 确保路径可写(自动创建父目录)→ 写入磁盘。store_path为空字符串时不写任何内容。MiniVisits.rename_in_index(path_from, path_to, index):返回重命名后的索引对象(不会修改当前索引,需配合set_index()使用)。它同时处理文件与目录路径,要求使用完整路径,目录路径不要以/结尾。示例:
local new_index = MiniVisits.rename_in_index(path_from, path_to)
MiniVisits.set_index(new_index)
这类函数的意义在于:索引中可以存放任何用户自定义信息(函数和元表之外的"可存储"数据都会被写盘),因此可以用它搭建任意形态的工作流,而不仅限于内置的追踪与标签。
排序算法:robust frecency 与 z 排序
config.list.sort 默认采用模块自研的 "robust frecency"(鲁棒频率-新近度混合)排序,由 MiniVisits.gen_sort.default(opts) 生成(实现见 lua/mini/visits.lua)。它基于"排名(rank)"操作:以某个参考值为依据给每个条目分配 1(最好)到条目数(最差)之间的名次;并列时采用"平均名次"处理——参考值相同的元素共享它们名次的平均值。这样总名次之和只取决于路径数量。
算法要点:
- 按频率(
count)从高到低排名; - 按新近度(
latest)从新到旧排名; - 加权合并两个名次:
score = (1 - w) * rank_frequency + w * rank_recency,其中w是recency_weight(默认 0.5),w越小,新近度对结果的影响越小。
不同 recency_weight 的效果:
- 默认 0.5:结合频率与新近度的 "robust frecency"。称其"鲁棒"是因为实际数值不会直接影响结果,只有排序次序起作用——例如某个文件
count = 100而其它文件只有count = 5,只要它不是最近访问的,就不会对结果产生压倒性影响。 1:从新到旧的纯新近度排序。0:从多到少的纯频率排序。
另外还提供 MiniVisits.gen_sort.z():复刻 z(rupa/z)的排序公式(见 lua/mini/visits.lua),适合偏好传统 z 风格排序的用户。
实战工作流示例
模块提供的是灵活框架,具体工作流由用户自行组织。以下示例来自 doc/mini-visits.txt 的 MiniVisits-examples 一节,可自由组合。
工作流一:切换不同排序与作用域
默认排序允许灵活调整更看重"新近度"还是"频率"。下面的映射组合了三种排序(纯新近、混合 frecency、纯频率)与两种作用域(全部访问 / 仅当前 cwd):
local make_select_path = function(select_global, recency_weight)
local visits = require('mini.visits')
local sort = visits.gen_sort.default({ recency_weight = recency_weight })
local select_opts = { sort = sort }
return function()
local cwd = select_global and '' or vim.fn.getcwd()
visits.select_path(cwd, select_opts)
end
end
local map = function(lhs, desc, ...)
vim.keymap.set('n', lhs, make_select_path(...), { desc = desc })
end
-- 按需调整 LHS 与描述
map('<Leader>vr', 'Select recent (all)', true, 1)
map('<Leader>vR', 'Select recent (cwd)', false, 1)
map('<Leader>vy', 'Select frecent (all)', true, 0.5)
map('<Leader>vY', 'Select frecent (cwd)', false, 0.5)
map('<Leader>vf', 'Select frequent (all)', true, 0)
map('<Leader>vF', 'Select frequent (cwd)', false, 0)
工作流二:手动标签
标签是创建"相关路径组"的有力工具。常规流程是:用 MiniVisits.add_label() 添加标签(会提示输入标签名)→ 用 MiniVisits.remove_label() 移除标签(同样提示)→ 需要使用时调用 MiniVisits.select_label(),它会先选标签、再通过 select_path() 选择该标签下的路径。映射示例:
local map_vis = function(keys, call, desc)
local rhs = '<Cmd>lua MiniVisits.' .. call .. '<CR>'
vim.keymap.set('n', '<Leader>' .. keys, rhs, { desc = desc })
end
map_vis('vv', 'add_label()', 'Add label')
map_vis('vV', 'remove_label()', 'Remove label')
map_vis('vl', 'select_label("", "")', 'Select label (all)')
map_vis('vL', 'select_label()', 'Select label (cwd)')
工作流三:固定标签
每个项目通常都有一小撮核心文件。可以创建"固定标签"(如 "core"),配合"全 cwd / 当前 cwd"两种作用域快速增删与选取,并基于新近度在核心文件之间迭代:
-- 添加与选择
local map_vis = function(keys, call, desc)
local rhs = '<Cmd>lua MiniVisits.' .. call .. '<CR>'
vim.keymap.set('n', '<Leader>' .. keys, rhs, { desc = desc })
end
map_vis('vv', 'add_label("core")', 'Add to core')
map_vis('vV', 'remove_label("core")', 'Remove from core')
map_vis('vc', 'select_path("", { filter = "core" })', 'Select core (all)')
map_vis('vC', 'select_path(nil, { filter = "core" })', 'Select core (cwd)')
-- 基于新近度迭代
local sort_latest = MiniVisits.gen_sort.default({ recency_weight = 1 })
local map_iterate_core = function(lhs, direction, desc)
local opts = { filter = 'core', sort = sort_latest, wrap = true }
local rhs = function()
MiniVisits.iterate_paths(direction, vim.fn.getcwd(), opts)
end
vim.keymap.set('n', lhs, rhs, { desc = desc })
end
map_iterate_core('[{', 'last', 'Core label (earliest)')
map_iterate_core('[[', 'forward', 'Core label (earlier)')
map_iterate_core(']]', 'backward', 'Core label (later)')
map_iterate_core(']}', 'first', 'Core label (latest)')
工作流四:自动化标签
使用版本控制系统(如 Git)时,通常已有现成的分组标识——分支名。下面示例把"当前分支名"作为标签自动添加/移除:
local map_branch = function(keys, action, desc)
local rhs = function()
local branch = vim.fn.system('git rev-parse --abbrev-ref HEAD')
if vim.v.shell_error ~= 0 then return nil end
branch = vim.trim(branch)
require('mini.visits')action
end
vim.keymap.set('n', '<Leader>' .. keys, rhs, { desc = desc })
end
map_branch('vb', 'add_label', 'Add branch label')
map_branch('vB', 'remove_label', 'Remove branch label')
与 mini.pick / mini.extra 的集成
如果同时安装了 mini.pick,官方推荐直接用 MiniExtra.pickers.visit_paths() 与 MiniExtra.pickers.visit_labels() 获得更完整的 picker 体验(见 lua/mini/extra.lua)。例如 :Pick visit_paths cwd='' recency_weight=1 filter='core' 可以按指定参数筛选全部访问中的核心文件,MiniExtra.pickers.visit_labels() 则先选标签再进入对应路径列表。该模块在依赖检测上做了保护:若 mini.visits 未加载,pick 函数会明确报错提示(visit_paths / visit_labels 需要 mini.visits)。
与同类插件的设计对比
mini.visits 文档明确列出了两个常被拿来对比的插件(对比详情见 doc/mini-visits.txt):
与 telescope-frecency.nvim 相比:
- telescope-frecency 存储真实的访问时间戳数组,而本模块只记录总访问次数与最近访问时间戳——这是有意的设计取舍,在"数据存储量与 frecency 排序复杂度"之间选择了更轻量的一端;
- telescope-frecency 默认每个会话只追踪一次缓冲区,而本模块在每次有意义的缓冲区进入时都会追踪,使会话内的排序更贴近实际;
- telescope-frecency 实现的是 Firefox 地址栏的原始 frecency 算法,而本模块使用自研的 "robust frecency" 方案;
- telescope-frecency 主要面向 telescope.nvim 使用,而本模块提供通用的列表函数和基于
vim.ui.select()的选择函数; - telescope-frecency 不允许使用自定义数据(如标签),而本模块支持。
与 harpoon 相比:
- harpoon 的概念更接近"有序栈"式的路径管理,本模块实现的是更常见的、不暗示顺序的标签体系,且可以按任务/偏好自动化;
- harpoon 的标记是路径内的位置(position),本模块标记的是路径本身;
- harpoon 在每次有意义变更后都写盘,本模块更保守,只在 Neovim 关闭或用户主动要求时读取/写入;
- harpoon 支持标记终端,本模块只面向路径;
- harpoon 有专门的管理 UI,本模块刻意不做 UI,只提供添加/移除标签的函数;
- harpoon 不提供任意已访问路径的追踪与复用能力,本模块可以。
禁用机制与注意事项汇总
最后汇总几个易踩的坑:
- 关闭自动追踪:
config.track.event = ''后需要自己调用MiniVisits.register_visit(),否则索引不会更新。 - 禁用模块:
vim.g.minivisits_disable = true(全局)或vim.b.minivisits_disable = true(缓冲区)。 - 路径改名/移动:用
rename_in_index()手动处理,且应在下次写盘前完成;用 mini.files 改名/移动则无需操心。 - 多实例冲突:同一存储路径不要被多个并行 Neovim 进程共用。
- 索引文件可手工编辑:它本质是
return <table>的 Lua 文件,编辑时保证语法合法即可(read_index()会通过dofile()执行)。 - 测试佐证:模块行为在 tests/test_visits.lua 中有大量用例覆盖(例如通过 mock
vim.ui.select/vim.ui.input验证选择与标签流程、通过设置XDG_DATA_HOME隔离索引文件、校验索引条目合并逻辑等),可作为理解内部行为的补充资料。
更多细节请查阅仓库内的 readmes/mini-visits.md、帮助文档 doc/mini-visits.txt(包含
:h MiniVisits-overview、:h MiniVisits-index-specification、:h MiniVisits-examples等标签)以及完整实现 lua/mini/visits.lua。