mini.nvim 的 mini.visits 模块:持久化文件系统访问追踪与复用实战指南

原创2026-09-15 16:58:411,340 阅读
文章标签:开发工具编辑器

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() 系列函数:允许持久化追踪任何用户自定义信息。

两条重要设计说明

阅读模块文档时需要先记住两条"约定",它们贯穿了后续所有功能:

  1. 数据只存在于两处:会话内的 Lua 变量(用于快速操作)和 config.store.path 指定的磁盘文件(用于持久化)。会话内的修改只有到 Neovim 退出(或主动调用写盘函数)时才会落盘。
  2. 不自动感知路径重命名/移动:因为不存在通用的检测手段。若文件/目录被改名或移动,通常需要手动干预索引(在下次写盘、通常是关闭当前会话之前完成),否则下次规范化会把旧路径视为已删除并从索引中移除。模块为此提供了 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)为:

  1. 剪枝(Prune):移除过时的访问——若 count 低于剪枝阈值则从该 cwd 中移除该访问条目(在其它 cwd 下仍可保留);若第一层(cwd)或第二层(path)key 对应的路径在磁盘上已不存在,则移除整个关联值。注意:带标签的访问不会被自动剪枝(即使 count = 0)。
  2. 衰减(Decay):让访问"更过时"。当某个 cwd 下所有 count 之和超过衰减阈值时,将所有 count 乘以同一个系数,使新的总数等于一个较小的目标值(只保留两位小数,所以总和未必精确)。衰减与剪枝配合,能自动淘汰长期未访问、不再相关的路径。
  3. 再次剪枝:确保衰减之后没有低于阈值的过时路径。

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(最好)到条目数(最差)之间的名次;并列时采用"平均名次"处理——参考值相同的元素共享它们名次的平均值。这样总名次之和只取决于路径数量。

算法要点:

  1. 按频率(count)从高到低排名;
  2. 按新近度(latest)从新到旧排名;
  3. 加权合并两个名次: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 不提供任意已访问路径的追踪与复用能力,本模块可以。

禁用机制与注意事项汇总

最后汇总几个易踩的坑:

  1. 关闭自动追踪:config.track.event = '' 后需要自己调用 MiniVisits.register_visit(),否则索引不会更新。
  2. 禁用模块:vim.g.minivisits_disable = true(全局)或 vim.b.minivisits_disable = true(缓冲区)。
  3. 路径改名/移动:用 rename_in_index() 手动处理,且应在下次写盘前完成;用 mini.files 改名/移动则无需操心。
  4. 多实例冲突:同一存储路径不要被多个并行 Neovim 进程共用。
  5. 索引文件可手工编辑:它本质是 return <table> 的 Lua 文件,编辑时保证语法合法即可(read_index() 会通过 dofile() 执行)。
  6. 测试佐证:模块行为在 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。

登录后查看全文
mini.nvim