8000 mini.nvim/lua/mini/visits.lua at main · nickvonkaenel/mini.nvim · GitHub
[go: up one dir, main page]

Skip to content

Latest commit

 

History

History
1565 lines (1406 loc) · 60.5 KB

visits.lua

File metadata and controls

1565 lines (1406 loc) · 60.5 KB
--- *mini.visits* Track and reuse file system visits
---
--- MIT License Copyright (c) 2023 Evgeni Chasnovski
--- Features:
---
--- - Persistently track file system visits (both files and directories)
--- per project directory. Store visit index is human readable and editable.
---
--- - Visit index is normalized on every write to contain relevant information.
--- Exact details can be customized. See |MiniVisits.normalize_index()|.
---
--- - Built-in ability to persistently add labels to path for later use.
--- See |MiniVisits.add_label()| and |MiniVisits.remove_label()|.
---
--- - Exported functions to reuse visit data:
--- - List visited paths/labels with custom filter and sort (uses "robust
--- frecency" by default). Can be used as source for pickers.
--- See |MiniVisits.list_paths()| and |MiniVisits.list_labels()|.
--- See |MiniVisits.gen_filter| and |MiniVisits.gen_sort|.
---
--- - Select visited paths/labels using |vim.ui.select()|.
--- See |MiniVisits.select_path()| and |MiniVisits.select_label()|.
---
--- - Iterate through visit paths in target direction ("forward", "backward",
--- "first", "last"). See |MiniVisits.iterate_paths()|.
---
--- - Exported functions to manually update visit index allowing persistent
--- track of any user information. See `*_index()` functions.
---
--- Notes:
--- - All data is stored _only_ in in-session Lua variable (for quick operation)
--- and at `config.store.path` on disk (for persistent usage).
--- - Most of functions affect an in-session data which gets written to disk only
--- before Neovim is closing or when users asks to.
--- - It doesn't account for paths being renamed or moved (because there is no
--- general way to detect that). Usually a manual intervention to the visit
--- index is required after the change but _before_ the next writing to disk
--- (usually before closing current session) because it will treat previous
--- path as deleted and remove it from index.
--- There is a |MiniVisits.rename_in_index()| helper for that.
--- If rename/move is done with |mini.files|, index is autoupdated.
---
--- Sources with more details:
--- - |MiniVisits-overview|
--- - |MiniVisits-index-specification|
--- - |MiniVisits-examples|
---
--- # Setup ~
---
--- This module needs a setup with `require('mini.visits').setup({})` (replace
--- `{}` with your `config` table). It will create global Lua table `MiniVisits`
--- which you can use for scripting or manually (with `:lua MiniVisits.*`).
---
--- See |MiniVisits.config| for `config` structure and default values.
---
--- You can override runtime config settings locally to buffer inside
--- `vim.b.minivisits_config` which should have same structure as
--- `MiniVisits.config`. See |mini.nvim-buffer-local-config| for more details.
---
--- # Comparisons ~
---
--- - [nvim-telescope/telescope-frecency.nvim](https://github.com/nvim-telescope/telescope-frecency.nvim):
--- - It stores array of actual visit timestamps, while this module tracks
--- only total number and latest timestamp of visits. This is by design
--- as a different trade-off between how much data is being used/stored
--- and complexity of underlying "frecency" sorting.
--- - By default tracks a buffer only once per session, while this module
--- tracks on every meaningful buffer enter. This leads to a more relevant
--- in-session sorting.
--- - Implements an original frecency algorithm of Firefox's address bar,
--- while this module uses own "robust frecency" approach.
--- - Mostly designed to work with 'nvim-telescope/telescope.nvim', while
--- this module provides general function to list paths and select
--- with |vim.ui.select()|.
--- - Does not allow use of custom data (like labels), while this module does.
---
--- - [ThePrimeagen/harpoon](https://github.com/ThePrimeagen/harpoon):
--- - Has slightly different concept than general labeling, which more
--- resembles adding paths to an ordered stack. This module implements
--- a more common labeling which does not imply order with ability to
--- make it automated depending on the task and/or preference.
--- - Implements marks as positions in a path, while this module labels paths.
--- - Writes data on disk after every meaning change, while this module is
--- more conservative and read only when Neovim closes or when asked to.
--- - Has support for labeling terminals, while this modules is oriented
--- only towards paths.
--- - Has dedicated UI to manage marks, while this module does not by design.
--- There are functions for adding and removing label from the path.
--- - Does not provide functionality to track and reuse any visited path,
--- while this module does.
---
--- # Disabling ~
---
--- To disable automated tracking, set `vim.g.minivisits_disable` (globally) or
--- `vim.b.minivisits_disable` (for a buffer) to `true`. Considering high
--- number of different scenarios and customization intentions, writing exact
--- rules for disabling module's functionality is left to user. See
--- |mini.nvim-disabling-recipes| for common recipes.
---@tag MiniVisits
--- # Tracking visits ~
---
--- File system visits (both directory and files) tracking is done in two steps:
--- - On every dedicated event (`config.track.event`, |BufEnter| by default) timer
--- is (re)started to actually register visit after certain amount of time
--- (`config.track.delay` milliseconds, 1000 by default). It is not registered
--- immediately to allow navigation to target buffer in several steps
--- (for example, with series of |:bnext| / |:bprevious|).
---
--- - When delay time passes without any dedicated events being triggered
--- (meaning user is "settled" on certain buffer), |MiniVisits.register_visit()|
--- is called if all of the following conditions are met:
--- - Module is not disabled (see "Disabling" section in |mini.visits|).
--- - Buffer is normal with non-empty name (used as visit path).
--- - Visit path does not equal to the latest tracked one. This is to allow
--- temporary enter of non-normal buffers (like help, terminal, etc.)
--- without artificial increase of visit count.
---
--- Visit is autoregistered for |current-directory| and leads to increase of count
--- and latest time of visit. See |MiniVisits-index-specification| for more details.
---
--- Notes:
--- - All data is stored _only_ in in-session Lua variable (for quick operation)
--- and at `config.store.path` on disk (for persistent usage). It is automatically
--- written to disk before every Neovim exit (if `config.store.autowrite` is set).
---
--- - Tracking can be disabled by supplying empty string as `track.event`.
--- Then it is up to the user to properly call |MiniVisits.register_visit()|.
---
--- # Reusing visits ~
---
--- Visit data can be reused in at least these ways:
---
--- - Get a list of visited paths (see |MiniVisits.list_paths()|) and use it
--- to visualize/pick/navigate visit history.
---
--- - Select one of the visited paths to open it (see |MiniVisits.select_path()|).
---
--- - Move along visit history (see |MiniVisits.iterate_paths()|).
---
--- - Utilize labels. Any visit can be added one or more labels (like "core",
--- "tmp", etc.). They are bound to the visit (path registered for certain
--- directory) and are stored persistently.
--- Labels can be used to manually create groups of files and/or directories
--- that have particular interest to the user.
--- There is no one right way to use them, though. See |MiniVisits-examples|
--- for some inspiration.
---
--- - Utilizing custom data. Visit index can be manipulated manually using
--- `_index()` set of functions. All "storable" (i.e. not functions or
--- metatables) user data inside index is then stored on disk, so it can be
--- used to create any kind of workflow user wants.
---
--- See |MiniVisits-examples| for some actual configuration and workflow examples.
---@tag MiniVisits-overview
--- # Structure ~
---
--- Visit index is a table containing actual data in two level deep nested tables.
---
--- First level keys are paths of project directory (a.k.a "cwd") for which
--- visits are registered.
---
--- Second level keys are actual visit paths. Their values are tables with visit
--- data which should follow these requirements:
--- - Field <count> should be present and be a number. It represents the number
--- of times this path was visited under particular cwd.
--- - Field <latest> should be present and be a number. It represents the time
--- of latest path visit under particular cwd.
--- By default computed with |os.time()| (up to a second).
--- - Field <labels> might not be present. If present, it should be a table
--- with string labels as keys and `true` as values. It represents labels of
--- the path under particular cwd.
---
--- Notes:
--- - All paths are absolute.
--- - Visit path should not necessarily be a part of corresponding cwd.
--- - Both `count` and `latest` can be any number: whole, fractional, negative, etc.
---
--- Example of an index data: >lua
---
--- {
--- ['/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 },
--- },
--- }
--- <
--- # Storage ~
---
--- When stored on disk, visit index is a file containing Lua code returning
--- visit index table. It can be edited by hand as long as it contains a valid
--- Lua code (to be executed with |dofile()|).
---
--- Notes:
--- - Storage is implemented in such a way that it doesn't really support more
--- than one parallel Neovim processes. Meaning that if there are two or more
--- simultaneous Neovim processes with same visit index storage path, the last
--- one writing to it will preserve its visit history while others - won't.
---
--- # Normalization ~
---
--- To ensure that visit index contains mostly relevant data, it gets normalized:
--- automatically inside |MiniVisits.write_index()| or
--- via |MiniVisits.normalize_index()|.
---
--- What normalization actually does can be configured in `config.store.normalize`.
---
--- See |MiniVisits.gen_normalize.default()| for default normalization approach.
---@tag MiniVisits-index-specification
--- This module provides a flexible framework for working with file system visits.
--- Exact choice of how to organize workflow is left to the user.
--- Here are some examples for inspiration which can be combined together.
---
--- # Use different sorting ~
---
--- Default sorting in |MiniVisits.gen_sort.default()| allows flexible adjustment
--- of which feature to prefer more: recency or frequency. Here is an example of
--- how to make set of keymaps for three types of sorting combined with two types
--- of scopes (all visits and only for current cwd): >lua
---
--- 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
---
--- -- Adjust LHS and description to your liking
--- 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)
--- <
--- Note: If using |mini.pick|, consider |MiniExtra.pickers.visit_paths()|.
---
--- # Use manual labels ~
---
--- Labels is a powerful tool to create groups of associated paths.
--- Usual workflow consists of:
--- - Add label with |MiniVisits.add_label()| (prompts for actual label).
--- - Remove label with |MiniVisits.remove_label()| (prompts for actual label).
--- - When need to use labeled groups, call |MiniVisits.select_label()| which
--- will then call |MiniVisits.select_path()| to select path among those
--- having selected label.
--- Note: If using |mini.pick|, consider |MiniExtra.pickers.visit_labels()|.
---
--- To make this workflow smoother, here is an example of keymaps: >lua
---
--- 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)')
--- <
--- # Use fixed labels ~
---
--- During work on every project there is usually a handful of files where core
--- activity is concentrated. This can be made easier by creating mappings
--- which add/remove special fixed label (for example, "core") and select paths
--- with that label for both all and current cwd. Example: >lua
---
--- -- Create and select
--- 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)')
---
--- -- Iterate based on recency
--- 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)')
--- <
--- # Use automated labels ~
---
--- When using version control system (such as Git), usually there is already
--- an identifier that groups files you are working with - branch name.
--- Here is an example of keymaps to add/remove label equal to branch name: >lua
---
--- 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](branch)
--- 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')
--- <
---@tag MiniVisits-examples
---@alias __visits_path string|nil Visit path. Can be empty string to mean "all visited
--- paths for `cwd`". Default: path of current buffer if normal, error otherwise.
---@alias __visits_cwd string|nil Visit cwd (project directory). Can be empty string to mean
--- "all visited cwd". Default: |current-directory|.
---@alias __visits_filter - <filter> `(function)` - predicate to filter paths. For more information
--- about how it is used, see |MiniVisits.config.list|.
--- Default: value of `config.list.filter` with |MiniVisits.gen_filter.default()|
--- as its default.
---@alias __visits_sort - <sort> `(function)` - path data sorter. For more information about how
--- it is used, see |MiniVisits.config.list|.
--- Default: value of `config.list.sort` or |MiniVisits.gen_sort.default()|
--- as its default.
---@diagnostic disable:undefined-field
---@diagnostic disable:discard-returns
---@diagnostic disable:unused-local
---@diagnostic disable:cast-local-type
---@diagnostic disable:undefined-doc-name
---@diagnostic disable:luadoc-miss-type-name
-- Module definition ==========================================================
local MiniVisits = {}
local H = {}
--- Module setup
---
---@param config table|nil Module config table. See |MiniVisits.config|.
---
---@usage >lua
--- require('mini.visits').setup() -- use default config
--- -- OR
--- require('mini.visits').setup({}) -- replace {} with your config table
--- <
MiniVisits.setup = function(config)
-- Export module
_G.MiniVisits = MiniVisits
-- Setup config
config = H.setup_config(config)
-- Apply config
H.apply_config(config)
-- Define behavior
H.create_autocommands(config)
end
--- Defaults ~
---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section)
---@text # List ~
--- *MiniVisits.config.list*
---
--- `config.list` defines how visit index is converted to a path list by default.
---
--- `list.filter` is a callable which should take a path data and return `true` if
--- this path should be present in the list.
--- Default: output of |MiniVisits.gen_filter.default()|.
---
--- Path data is a table with at least these fields:
--- - <path> `(string)` - absolute path of visit.
--- - <count> `(number)` - number of visits.
--- - <latest> `(number)` - timestamp of latest visit.
--- - <labels> `(table|nil)` - table of labels (has string keys with `true` values).
---
--- Notes:
--- - Both `count` and `latest` (in theory) can be any number. But built-in tracking
--- results into positive integer `count` and `latest` coming from |os.time()|.
--- - There can be other entries if they are set by user as index entry.
---
--- `list.sort` is a callable which should take an array of path data and return
--- a sorted array of path data (or at least tables each containing <path> field).
--- Default: output of |MiniVisits.gen_sort.default()|.
--- Single path data entry is a table with a same structure as for `list.filter`.
---
--- Note, that `list.sort` can be used both to filter, sort, or even return paths
--- unrelated to the input.
---
--- # Silent ~
---
--- `config.silent` is a boolean controlling whether to show non-error feedback
--- (like adding/removing labels, etc.). Default: `false`.
---
--- # Store ~
---
--- `config.store` defines how visit index is stored on disk to enable persistent
--- data across several sessions.
---
--- `store.autowrite` is a boolean controlling whether to write visit data to
--- disk on |VimLeavePre| event. Default: `true`.
---
--- `store.normalize` is a callable which should take visit index
--- (see |MiniVisits-index-specification|) as input and return "normalized" visit
--- index as output. This is used to ensure that visit index is up to date and
--- contains only relevant data. For example, it controls how old and
--- irrelevant visits are "forgotten", and more.
--- Default: output of |MiniVisits.gen_normalize.default()|.
---
--- `store.path` is a path to which visit index is written. See "Storage" section
--- of |MiniVisits-index-specification| for more details.
--- Note: set to empty string to disable any writing with not explicitly set
--- path (including the one on |VimLeavePre|).
--- Default: "mini-visits-index" file inside |$XDG_DATA_HOME|.
---
--- # Track ~
---
--- `config.track` defines how visits are tracked (index entry is autoupdated).
--- See "Tracking visits" section in |MiniVisits-overview| for more details.
---
--- `track.event` is a proper Neovim |{event}| on which track get triggered.
--- Note: set to empty string to disable automated tracking.
--- Default: |BufEnter|.
---
--- `track.delay` is a delay in milliseconds after event is triggered and visit
--- is autoregistered.
--- Default: 1000 (to allow navigation between buffers without tracking
--- intermediate ones).
MiniVisits.config = {
-- How visit index is converted to list of paths
list = {
-- Predicate for which paths to include (all by default)
filter = nil,
-- Sort paths based on the visit data (robust frecency by default)
sort = nil,
},
-- Whether to disable showing non-error feedback
silent = false,
-- How visit index is stored
store = {
-- Whether to write all visits before Neovim is closed
autowrite = true,
-- Function to ensure that written index is relevant
normalize = nil,
-- Path to store visit index
path = vim.fn.stdpath('data') .. '/mini-visits-index',
},
-- How visit tracking is done
track = {
< 10BC8 /div>
-- Start visit register timer at this event
-- Supply empty string (`''`) to not do this automatically
event = 'BufEnter',
-- Debounce delay after event to register a visit
delay = 1000,
},
}
--minidoc_afterlines_end
--- Register visit
---
--- Steps:
--- - Ensure that there is an entry for path-cwd pair.
--- - Add 1 to visit `count`.
--- - Set `latest` visit time to equal current time.
---
---@param path string|nil Visit path. Default: path of current buffer if normal,
--- error otherwise.
---@param cwd string|nil Visit cwd (project directory). Default: |current-directory|.
MiniVisits.register_visit = function(path, cwd)
path = H.validate_path(path)
cwd = H.validate_cwd(cwd)
if path == '' or cwd == '' then H.error('Both `path` and `cwd` should not be empty.') end
H.ensure_index_entry(path, cwd)
local path_tbl = H.index[cwd][path]
path_tbl.count = path_tbl.count + 1
path_tbl.latest = os.time()
end
--- Add path to index
---
--- Ensures that there is a (one or more) entry for path-cwd pair. If entry is
--- already present, does nothing. If not - creates it with both `count` and
--- `latest` set to 0.
---
---@param path __visits_path
---@param cwd __visits_cwd
MiniVisits.add_path = function(path, cwd)
path = H.validate_path(path)
cwd = H.validate_cwd(cwd)
local path_cwd_pairs = H.resolve_path_cwd(path, cwd)
for _, pair in ipairs(path_cwd_pairs) do
H.ensure_index_entry(pair.path, pair.cwd)
end
end
--- Add label to path
---
--- Steps:
--- - Ensure that there is an entry for path-cwd pair.
--- - Add label to the entry.
---
---@param label string|nil Label string. Default: `nil` to ask from user.
---@param path __visits_path
---@param cwd __visits_cwd
MiniVisits.add_label = function(label, path, cwd)
path = H.validate_path(path)
cwd = H.validate_cwd(cwd)
if label == nil then
-- Suggest all labels from cwd in completion
label = H.get_label_from_user('Enter label to add', MiniVisits.list_labels('', cwd))
if label == nil then return end
end
label = H.validate_string(label, 'label')
-- Add label to all target path-cwd pairs
local path_cwd_pairs = H.resolve_path_cwd(path, cwd)
for _, pair in ipairs(path_cwd_pairs) do
H.ensure_index_entry(pair.path, pair.cwd)
local path_tbl = H.index[pair.cwd][pair.path]
local labels = path_tbl.labels or {}
labels[label] = true
path_tbl.labels = labels
end
H.echo(string.format('Added %s label.', vim.inspect(label)))
end
--- Remove path
---
--- Deletes a (one or more) entry for path-cwd pair from an index. If entry is
--- already absent, does nothing.
---
--- Notes:
--- - Affects only in-session Lua variable. Call |MiniVisits.write_index()| to
--- make it persistent.
---
---@param path __visits_path
---@param cwd __visits_cwd
MiniVisits.remove_path = function(path, cwd)
path = H.validate_path(path)
cwd = H.validate_cwd(cwd)
-- Remove all target visits
H.ensure_read_index()
local path_cwd_pairs = H.resolve_path_cwd(path, cwd)
for _, pair in ipairs(path_cwd_pairs) do
local cwd_tbl = H.index[pair.cwd]
if type(cwd_tbl) == 'table' then cwd_tbl[pair.path] = nil end
end
for dir, _ in pairs(H.index) do
if vim.tbl_count(H.index[dir]) == 0 then H.index[dir] = nil end
end
end
--- Remove label from path
---
--- Steps:
--- - Remove label from (one or more) index entry.
--- - If it was last label in an entry, remove `labels` key.
---
---@param label string|nil Label string. Default: `nil` to ask from user.
---@param path __visits_path
---@param cwd __visits_cwd
MiniVisits.remove_label = function(label, path, cwd)
path = H.validate_path(path)
cwd = H.validate_cwd(cwd)
if label == nil then
-- Suggest only labels from target path-cwd pairs
label = H.get_label_from_user('Enter label to remove', MiniVisits.list_labels(path, cwd))
if label == nil then return end
end
label = H.validate_string(label, 'label')
-- Remove label from all target path-cwd pairs (ignoring not present ones and
-- collapsing `labels` if removed last label)
H.ensure_read_index()
local path_cwd_pairs = H.resolve_path_cwd(path, cwd)
for _, pair in ipairs(path_cwd_pairs) do
local path_tbl = (H.index[pair.cwd] or {})[pair.path]
if type(path_tbl) == 'table' and type(path_tbl.labels) == 'table' then
path_tbl.labels[label] = nil
if vim.tbl_count(path_tbl.labels) == 0 then path_tbl.labels = nil end
end
end
H.echo(string.format('Removed %s label.', vim.inspect(label)))
end
--- List visit paths
---
--- Convert visit index for certain cwd into an ordered list of visited paths.
--- Supports custom filtering and sorting.
---
--- Examples: >lua
---
--- -- Get paths sorted from most to least recent
--- local sort_recent = MiniVisits.gen_sort.default({ recency_weight = 1 })
--- MiniVisits.list_paths(nil, { sort = sort_recent })
---
--- -- Get paths from all cwd sorted from most to least frequent
--- local sort_frequent = MiniVisits.gen_sort.default({ recency_weight = 0 })
--- MiniVisits.list_paths('', { sort = sort_frequent })
---
--- -- Get paths not including hidden
--- 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 })
--- <
---@param cwd __visits_cwd
---@param opts table|nil Options. Possible fields:
--- __visits_filter
--- __visits_sort
---
---@return table Array of visited paths.
MiniVisits.list_paths = function(cwd, opts)
cwd = H.validate_cwd(cwd)
opts = vim.tbl_deep_extend('force', H.get_config().list, opts or {})
local filter = H.validate_filter(opts.filter)
local sort = H.validate_sort(opts.sort)
local path_data_arr = H.make_path_array('', cwd)
local res_arr = sort(vim.tbl_filter(filter, path_data_arr))
return vim.tbl_map(function(x) return x.path end, res_arr)
end
--- List visit labels
---
--- Convert visit index for certain path-cwd pair into an ordered list of labels.
--- Supports custom filtering for paths. Result is ordered from most to least
--- frequent label.
---
--- Examples: >lua
---
--- -- Get labels for current path-cwd pair
--- MiniVisits.list_labels()
---
--- -- Get labels for current path across all cwd
--- MiniVisits.list_labels(nil, '')
---
--- -- Get all available labels excluding ones from hidden files
--- local is_not_hidden = function(path_data)
--- return not vim.startswith(vim.fn.fnamemodify(path_data.path, ':t'), '.')
--- end
--- MiniVisits.list_labels('', '', { filter = is_not_hidden })
--- <
---@param path __visits_path
---@param cwd __visits_cwd
---@param opts table|nil Options. Possible fields:
--- __visits_filter
--- __visits_sort
---
---@return table Array of available labels.
MiniVisits.list_labels = function(path, cwd, opts)
path = H.validate_path(path)
cwd = H.validate_cwd(cwd)
opts = vim.tbl_deep_extend('force', { filter = H.get_config().list.filter }, opts or {})
local filter = H.validate_filter(opts.filter)
local path_data_arr = H.make_path_array(path, cwd)
local res_arr = vim.tbl_filter(filter, path_data_arr)
-- Count labels
local label_counts = {}
for _, path_data in ipairs(res_arr) do
for label, _ in pairs(path_data.labels or {}) do
label_counts[label] = (label_counts[label] or 0) + 1
end
end
-- Sort from most to least common
local label_arr = {}
for label, count in pairs(label_counts) do
table.insert(label_arr, { count, label })
end
table.sort(label_arr, function(a, b) return a[1] > b[1] or (a[1] == b[1] and a[2] < b[2]) end)
return vim.tbl_map(function(x) return x[2] end, label_arr)
end
--- Select visit path
---
--- Uses |vim.ui.select()| with an output of |MiniVisits.list_paths()| and
--- calls |:edit| on the chosen item.
---
--- Note: if you have |mini.pick|, consider using |MiniExtra.pickers.visit_labels()|
--- and |MiniExtra.pickers.visit_paths()|.
---
--- Examples:
---
--- - Select from all visited paths: `MiniVisits.select_path('')`
---
--- - Select from paths under current directory sorted from most to least recent: >lua
---
--- local sort_recent = MiniVisits.gen_sort.default({ recency_weight = 1 })
--- MiniVisits.select_path(nil, { sort = sort_recent })
--- <
---@param cwd string|nil Forwarded to |MiniVisits.list_paths()|.
---@param opts table|nil Forwarded to |MiniVisits.list_paths()|.
MiniVisits.select_path = function(cwd, opts)
local paths = MiniVisits.list_paths(cwd, opts)
local cwd_to_short = cwd == '' and vim.fn.getcwd() or cwd
local items = vim.tbl_map(function(path) return { path = path, text = H.short_path(path, cwd_to_short) } end, paths)
local select_opts = { prompt = 'Visited paths', format_item = function(item) return item.text end }
local on_choice = function(item) H.edit((item or {}).path) end
vim.ui.select(items, select_opts, on_choice)
end
--- Select visit label
---
--- Uses |vim.ui.select()| with an output of |MiniVisits.list_labels()| and
--- calls |MiniVisits.select_path()| to get target paths with selected label.
---
--- Note: if you have |mini.pick|, consider using |MiniExtra.pickers.visit_labels()|.
---
--- Examples:
---
--- - Select from labels of current path: `MiniVisits.select_label()`
---
--- - Select from all visited labels: `MiniVisits.select_label('', '')`
---
--- - Select from current project labels and sort paths (after choosing) from most
--- to least recent: >lua
---
--- local sort_recent = MiniVisits.gen_sort.default({ recency_weight = 1 })
--- MiniVisits.select_label('', nil, { sort = sort_recent })
--- <
---@param path string|nil Forwarded to |MiniVisits.list_labels()|.
---@param cwd string|nil Forwarded to |MiniVisits.list_labels()|.
---@param opts table|nil Forwarded to both |MiniVisits.list_labels()|
--- and |MiniVisits.select_path()| (after choosing a label).
MiniVisits.select_label = function(path, cwd, opts)
local items = MiniVisits.list_labels(path, cwd, opts)
opts = opts or {}
local on_choice = function(label)
if label == nil then return end
-- Select among subset of paths with chosen label
local filter_cur = (opts or {}).filter or MiniVisits.gen_filter.default()
local new_opts = vim.deepcopy(opts)
new_opts.filter = function(path_data)
return filter_cur(path_data) and type(path_data.labels) == 'table' and path_data.labels[label]
end
MiniVisits.select_path(cwd, new_opts)
end
vim.ui.select(items, { prompt = 'Visited labels' }, on_choice)
end
--- Iterate visit paths
---
--- Steps:
--- - Compute a sorted array of target paths using |MiniVisits.list_paths()|.
--- - Identify the current index inside the array based on path of current buffer.
--- - Iterate through the array certain amount of times in a dedicated direction:
--- - For "first" direction - forward starting from index 0 (so that single
--- first iteration leads to first path).
--- - For "backward" direction - backward starting from current index.
--- - For "forward" direction - forward starting from current index.
--- - For "last" direction - backward starting from index after the last one
--- (so that single first iteration leads to the last path).
---
--- Notes:
--- - Mostly designed to be used as a mapping. See `MiniVisits-examples`.
--- - If path from current buffer is not in the output of `MiniVisits.list_paths()`,
--- starting index is inferred such that first iteration lands on first item
--- (if iterating forward) or last item (if iterating backward).
--- - Navigation with this function is not tracked (see |MiniVisits-overview|).
--- This is done to allow consecutive application without affecting
--- underlying list of paths.
---
--- Examples assuming underlying array of files `{ "file1", "file2", "file3" }`:
---
--- - `MiniVisits("first")` results into focusing on "file1".
--- - `MiniVisits("backward", { n_times = 2 })` from "file3" results into "file1".
--- - `MiniVisits("forward", { n_times = 10 })` from "file1" results into "file3".
--- - `MiniVisits("last", { n_times = 4, wrap = true })` results into "file3".
---
---@param direction string One of "first", "backward", "forward", "last".
---@param cwd string|nil Forwarded to |MiniVisits.list_paths()|.
---@param opts table|nil Options. Possible fields:
--- - <filter> `(function)` - forwarded to |MiniVisits.list_paths()|.
--- - <sort> `(function)` - forwarded to |MiniVisits.list_paths()|.
--- - <n_times> `(number)` - number of steps to go in certain direction.
--- Default: |v:count1|.
--- - <wrap> `(boolean)` - whether to wrap around list edges. Default: `false`.
MiniVisits.iterate_paths = function(direction, cwd, opts)
if not (direction == 'first' or direction == 'backward' or direction == 'forward' or direction == 'last') then
H.error('`direction` should be one of "first", "backward", "forward", "last".')
end
local is_move_forward = (direction == 'first' or direction == 'forward')
local default_opts = { filter = nil, sort = nil, n_times = vim.v.count1, wrap = false }
opts = vim.tbl_deep_extend('force', default_opts, opts or {})
local all_paths = MiniVisits.list_paths(cwd, { filter = opts.filter, sort = opts.sort })
local n_tot = #all_paths
if n_tot == 0 then return end
-- Compute current index
local cur_ind
if direction == 'first' then cur_ind = 0 end
if direction == 'last' then cur_ind = n_tot + 1 end
if direction == 'backward' or direction == 'forward' then
local cur_path = H.buf_get_path(vim.api.nvim_get_current_buf())
for i, path in ipairs(all_paths) do
if path == cur_path then
cur_ind = i
break
end
end
end
-- - If not on path from the list, make going forward start from the
-- beginning and backward - from end
if cur_ind == nil then cur_ind = is_move_forward and 0 or (n_tot + 1) end
-- Compute target index ensuring that it is inside `[1, #all_paths]`
local res_ind = cur_ind + opts.n_times * (is_move_forward and 1 or -1)
res_ind = opts.wrap and ((res_ind - 1) % n_tot + 1) or math.min(math.max(res_ind, 1), n_tot)
-- Open path with no visit track (for default `track.event`)
-- Use `vim.g` instead of `vim.b` to not register in **next** buffer
local cache_disabled = vim.g.minivisits_disable
vim.g.minivisits_disable = true
H.edit(all_paths[res_ind])
vim.g.minivisits_disable = cache_disabled
end
--- Get active visit index
---
---@return table Copy of currently active visit index table.
MiniVisits.get_index = function()
H.ensure_read_index()
return vim.deepcopy(H.index)
end
--- Set active visit index
---
---@param index table Visit index table.
MiniVisits.set_index = function(index)
H.validate_index(index, '`index`')
H.index = vim.deepcopy(index)
H.cache.needs_index_read = false
end
--- Reset active visit index
---
--- Set currently active visit index to the output of |MiniVisits.read_index()|.
--- Does nothing if reading the index failed.
MiniVisits.reset_index = function()
local ok, stored_index = pcall(MiniVisits.read_index)
if not ok or stored_index == nil then return end
MiniVisits.set_index(stored_index)
end
--- Normalize visit index
---
--- Applies `config.store.normalize` (|MiniVisits.gen_normalize.default()| by default)
--- to the input index object and returns the output (if it fits in the definition
--- of index object; see |MiniVisits-index-specification|).
---
---@param index table|nil Index object. Default: copy of the current index.
---
---@return table Normalized index object.
MiniVisits.normalize_index = function(index)
index = index or MiniVisits.get_index()
H.validate_index(index, '`index`')
local config = H.get_config()
local normalize = config.store.normalize
if not vim.is_callable(normalize) then normalize = MiniVisits.gen_normalize.default() end
local new_index = normalize(vim.deepcopy(index))
H.validate_index(new_index, '`index` after normalization')
return new_index
end
--- Read visit index from disk
---
---@param store_path string|nil Path on the disk containing visit index data.
--- Default: `config.store.path`.
--- Notes:
--- - Can return `nil` if path is empty string or file is not readable.
--- - File is sourced with |dofile()| as a regular Lua file.
---
---@return table|nil Output of the file source.
MiniVisits.read_index = function(store_path)
store_path = store_path or H.get_config().store.path
if store_path == '' then return nil end
H.validate_string(store_path, 'store_path')
if vim.fn.filereadable(store_path) == 0 then return nil end
return dofile(store_path)
end
--- Write visit index to disk
---
--- Steps:
--- - Normalize index with |MiniVisits.normalize_index()|.
--- - Ensure path is valid (all parent directories are created, etc.).
--- - Write index object to the path so that it is readable
--- with |MiniVisits.read_index()|.
---
---@param store_path string|nil Path on the disk where to write visit index data.
--- Default: `config.store.path`. Note: if empty string, nothing is written.
---@param index table|nil Index object to write to disk.
--- Default: current session index.
MiniVisits.write_index = function(store_path, index)
store_path = store_path or H.get_config().store.path
H.validate_string(store_path, 'store_path')
if store_path == '' then return end
index = index or MiniVisits.get_index()
H.validate_index(index, '`index`')
-- Normalize index
index = MiniVisits.normalize_index(index)
-- Ensure writable path
store_path = vim.fn.fnamemodify(store_path, ':p')
local path_dir = vim.fn.fnamemodify(store_path, ':h')
vim.fn.mkdir(path_dir, 'p')
-- Write
local lines = vim.split(vim.inspect(index), '\n')
lines[1] = 'return ' .. lines[1]
vim.fn.writefile(lines, store_path)
end
--- Rename path in index
---
--- A helper to react for a path rename/move in order to preserve its visit data.
--- It works both for file and directory paths.
---
--- Notes:
--- - It does not update current index, but returns a modified index object.
--- Use |MiniVisits.set_index()| to make it current.
--- - Use only full paths.
--- - Do not append `/` to directory paths. Use same format as for files.
---
--- Assuming `path_from` and `path_to` are variables containing full paths
--- before and after rename/move, here is an example to update current index: >lua
---
--- local new_index = MiniVisits.rename_in_index(path_from, path_to)
--- MiniVisits.set_index(new_index)
--- <
---@param path_from string Full path to be renamed.
---@param path_to string Full path to be replaced with.
---@param index table|nil Index object inside which to perform renaming.
--- Default: current session index.
---
---@return table Index object with renamed path.
MiniVisits.rename_in_index = function(path_from, path_to, index)
path_from = H.validate_string(path_from, 'path_from')
path_to = H.validate_string(path_to, 'path_to')
index = index or MiniVisits.get_index()
H.validate_index(index, '`index`')
local path_from_pattern = vim.pesc(path_from)
local pattern_from_full = string.format('^%s$', path_from_pattern)
local pattern_from_parent_dir = string.format('^(%s)/', path_from_pattern)
local path_to_parent_dir = path_to .. '/'
local replace = function(x)
if string.find(x, pattern_from_full) ~= nil then return path_to end
return string.gsub(x, pattern_from_parent_dir, path_to_parent_dir)
0