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

Skip to content

Latest commit

 

History

History
2091 lines (1838 loc) · 81.1 KB

extra.lua

File metadata and controls

2091 lines (1838 loc) · 81.1 KB
--- *mini.extra* Extra 'mini.nvim' functionality
--- *MiniExtra*
---
--- MIT License Copyright (c) 2023 Evgeni Chasnovski
---
--- ==============================================================================
---
--- Extra useful functionality which is not essential enough for other 'mini.nvim'
--- modules to include directly.
---
--- Features:
---
--- - Various pickers for 'mini.pick':
--- - Built-in diagnostic (|MiniExtra.pickers.diagnostic()|).
--- - File explorer (|MiniExtra.pickers.explorer()|).
--- - Git branches/commits/files/hunks (|MiniExtra.pickers.git_hunks()|, etc.).
--- - Command/search/input history (|MiniExtra.pickers.history()|).
--- - LSP references/symbols/etc. (|MiniExtra.pickers.lsp()|).
--- - Tree-sitter nodes (|MiniExtra.pickers.treesitter()|).
--- - And much more.
--- See |MiniExtra.pickers| for more.
---
--- - Various textobject specifications for 'mini.ai'. See |MiniExtra.gen_ai_spec|.
---
--- - Various highlighters for 'mini.hipatterns'. See |MiniExtra.gen_highlighter|.
---
--- Notes:
--- - This module requires only those 'mini.nvim' modules which are needed for
--- a particular functionality: 'mini.pick' for pickers, etc.
---
--- # Setup ~
---
--- This module needs a setup with `require('mini.extra').setup({})` (replace
--- `{}` with your `config` table). It will create global Lua table `MiniExtra`
--- which you can use for scripting or manually (with `:lua MiniExtra.*`).
---
--- See |MiniExtra.config| for `config` structure and default values.
---
--- This module doesn't have runtime options, so using `vim.b.miniextra_config`
--- will have no effect here.
---
--- # Comparisons ~
---
--- - 'nvim-telescope/telescope.nvim':
--- - With |MiniExtra.pickers|, 'mini.pick' is reasonably on par when it comes
--- to built-in pickers.
---
--- - 'ibhagwan/fzf-lua':
--- - Same as 'nvim-telescope/telescope.nvim'.
---@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
---@alias __extra_ai_spec_return function Function implementing |MiniAi-textobject-specification|.
---@alias __extra_pickers_local_opts table|nil Options defining behavior of this particular picker.
---@alias __extra_pickers_opts table|nil Options forwarded to |MiniPick.start()|.
---@alias __extra_pickers_return any Output of the called picker.
---@alias __extra_pickers_git_notes Notes:
--- - Requires executable `git`.
--- - Requires target path to be part of git repository.
--- - Present for exploration and navigation purposes. Doing any Git operations
--- is suggested to be done in a dedicated Git client and is not planned.
---@alias __extra_pickers_preserve_order - <preserve_order> `(boolean)` - whether to preserve original order
--- during query. Default: `false`.
---@alias __extra_pickers_git_path - <path> `(string|nil)` - target path for Git operation (if required). Also
--- used to find Git repository inside which to construct items.
--- Default: `nil` for root of Git repository containing |current-directory|.
-- Module definition ==========================================================
local MiniExtra = {}
local H = {}
--- Module setup
---
---@param config table|nil Module config table. See |MiniExtra.config|.
---
---@usage >lua
--- require('mini.extra').setup() -- use default config
--- -- OR
--- require('mini.extra').setup({}) -- replace {} with your config table
--- <
MiniExtra.setup = function(config)
-- Export module
_G.MiniExtra = MiniExtra
-- Setup config
config = H.setup_config(config)
-- Apply config
H.apply_config(config)
end
--stylua: ignore
--- Module config
---
--- Default values:
---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section)
MiniExtra.config = {}
--minidoc_afterlines_end
--- 'mini.ai' textobject specification generators
---
--- This is a table with function elements. Call to actually get specification.
---
--- Assumed to be used as part of |MiniAi.setup()|. Example: >lua
---
--- local gen_ai_spec = require('mini.extra').gen_ai_spec
--- require('mini.ai').setup({
--- custom_textobjects = {
--- B = gen_ai_spec.buffer(),
--- D = gen_ai_spec.diagnostic(),
--- I = gen_ai_spec.indent(),
--- L = gen_ai_spec.line(),
--- N = gen_ai_spec.number(),
--- },
--- })
--- <
MiniExtra.gen_ai_spec = {}
--- Current buffer textobject
---
--- Notes:
--- - `a` textobject selects all lines in a buffer.
--- - `i` textobject selects all lines except blank lines at start and end.
---
---@return __extra_ai_spec_return
MiniExtra.gen_ai_spec.buffer = function()
return function(ai_type)
local start_line, end_line = 1, vim.fn.line('$')
if ai_type == 'i' then
-- Skip first and last blank lines for `i` textobject
local first_nonblank, last_nonblank = vim.fn.nextnonblank(start_line), vim.fn.prevnonblank(end_line)
-- Do nothing for buffer with all blanks
if first_nonblank == 0 or last_nonblank == 0 then return { from = { line = start_line, col = 1 } } end
start_line, end_line = first_nonblank, last_nonblank
end
local to_col = math.max(vim.fn.getline(end_line):len(), 1)
return { from = { line = start_line, col = 1 }, to = { line = end_line, col = to_col } }
end
end
--- Current buffer diagnostic textobject
---
--- Notes:
--- - Both `a` and `i` textobjects return |vim.diagnostic.get()| output for the
--- current buffer. It is modified to fit |MiniAi-textobject-specification|.
---
---@param severity any Which severity to use. Forwarded to |vim.diagnostic.get()|.
--- Default: `nil` to use all diagnostic entries.
---
---@return __extra_ai_spec_return
MiniExtra.gen_ai_spec.diagnostic = function(severity)
return function(ai_type)
local cur_diag = vim.diagnostic.get(0, { severity = severity })
local regions = {}
for _, diag in ipairs(cur_diag) do
local from = { line = diag.lnum + 1, col = diag.col + 1 }
local to = { line = diag.end_lnum + 1, col = diag.end_col }
if to.line == nil or to.col == nil then to = { line = diag.lnum + 1, col = diag.col + 1 } end
table.insert(regions, { from = from, to = to })
end
return regions
end
end
--- Current buffer indent scopes textobject
---
--- Indent scope is a set of consecutive lines with the following properties:
--- - Lines above first and below last are non-blank. They are called borders.
--- - There is at least one non-blank line in a set.
--- - All non-blank lines between borders have strictly greater indent
--- (perceived leading space respecting |tabstop|) than either of borders.
---
--- Notes:
--- - `a` textobject selects scope including borders.
--- - `i` textobject selects the scope charwise.
--- - Differences with |MiniIndentscope.textobject|:
--- - This textobject always treats blank lines on top and bottom of `i`
--- textobject as part of it, while 'mini.indentscope' can configure that.
--- - This textobject can select non-covering scopes, while 'mini.indentscope'
--- can not (by design).
--- - In this textobject scope computation is done only by "casting rays" from
--- top to bottom and not in both ways as in 'mini.indentscope'.
--- This works in most common scenarios and doesn't work only if indent of
--- of the bottom border is expected to be larger than the top.
---
---@return function Function implementing |MiniAi-textobject-specification|.
--- It returns array of regions representing all indent scopes in the buffer
--- ordered increasingly by the start line.
MiniExtra.gen_ai_spec.indent = function() return H.ai_indent_spec end
--- Current line textobject
---
--- Notes:
--- - `a` textobject selects whole line.
--- - `i` textobject selects line after initial indent.
---
---@return __extra_ai_spec_return
MiniExtra.gen_ai_spec.line = function()
return function(ai_type)
local line_num = vim.fn.line('.')
local line = vim.fn.getline(line_num)
-- Ignore indentation for `i` textobject
local from_col = ai_type == 'a' and 1 or (line:match('^(%s*)'):len() + 1)
-- Don't select `\n` past the line to operate within a line
local to_col = line:len()
return { from = { line = line_num, col = from_col }, to = { line = line_num, col = to_col } }
end
end
--- Number textobject
---
--- Notes:
--- - `a` textobject selects a whole number possibly preceded with "-" and
--- possibly followed by decimal part (dot and digits).
--- - `i` textobject selects consecutive digits.
---
---@return __extra_ai_spec_return
MiniExtra.gen_ai_spec.number = function()
local digits_pattern = '%f[%d]%d+%f[%D]'
local find_a_number = function(line, init)
-- First find consecutive digits
local from, to = line:find(digits_pattern, init)
if from == nil then return nil, nil end
-- Make sure that these digits were not processed before. This can happen
-- because 'miin.ai' does next with `init = from + 1`, meaning that
-- "-12.34" was already matched, then it would try to match starting from
-- "1": we want to avoid matching that right away and avoid matching "34"
-- from this number.
if from == init and line:sub(from - 1, from - 1) == '-' then
init = to + 1
from, to = line:find(digits_pattern, init)
end
if from == nil then return nil, nil end
if line:sub(from - 2):find('^%d%.') ~= nil then
init = to + 1
from, to = line:find(digits_pattern, init)
end
if from == nil then return nil, nil end
-- Match the whole number with minus and decimal part
if line:sub(from - 1, from - 1) == '-' then from = from - 1 end
local dec_part = line:sub(to + 1):match('^%.%d+()')
if dec_part ~= nil then to = to + dec_part - 1 end
return from, to
end
return function(ai_type)
if ai_type == 'i' then return { digits_pattern } end
return { find_a_number, { '^().*()$' } }
end
end
--- 'mini.hipatterns' highlighter generators
---
--- This is a table with function elements. Call to actually get specification.
---
--- Assumed to be used as part of |MiniHipatterns.setup()|. Example: >lua
---
--- local hi_words = require('mini.extra').gen_highlighter.words
--- require('mini.hipatterns').setup({
--- highlighters = {
--- todo = hi_words({ 'TODO', 'Todo', 'todo' }, 'MiniHipatternsTodo'),
--- },
--- })
--- <
MiniExtra.gen_highlighter = {}
--- Highlight words
---
--- Notes:
--- - Words should start and end with alphanumeric symbol (latin letter or digit).
--- - Words will be highlighted only in full and not if part bigger word, i.e.
--- there should not be alphanumeric symbol before and after it.
---
---@param words table Array of words to highlight. Will be matched as is, not
--- as Lua pattern.
---@param group string|function Proper `group` field for `highlighter`.
--- See |MiniHipatterns.config|.
---@param extmark_opts any Proper `extmark_opts` field for `highlighter`.
--- See |MiniHipatterns.config|.
MiniExtra.gen_highlighter.words = function(words, group, extmark_opts)
if not H.islist(words) then H.error('`words` should be an array.') end
if not (type(group) == 'string' or vim.is_callable(group)) then H.error('`group` should be string or callable.') end
local pattern = vim.tbl_map(function(x)
if type(x) ~= 'string' then H.error('All elements of `words` should be strings.') end
return '%f[%w]()' .. vim.pesc(x) .. '()%f[%W]'
end, words)
return { pattern = pattern, group = group, extmark_opts = extmark_opts }
end
--- 'mini.pick' pickers
---
--- A table with |MiniPick| pickers (which is a hard dependency).
--- Notes:
--- - All have the same signature:
--- - <local_opts> - optional table with options local to picker.
--- - <opts> - optional table with options forwarded to |MiniPick.start()|.
--- - All of them are automatically registered in |MiniPick.registry| inside
--- both |MiniExtra.setup()| or |MiniPick.setup()| (only one is enough).
--- - All use default versions of |MiniPick-source.preview|, |MiniPick-source.choose|,
--- and |MiniPick-source.choose_marked| if not stated otherwise.
--- Shown text and |MiniPick-source.show| are targeted to the picked items.
---
--- Examples of usage:
--- - As Lua code: `MiniExtra.pickers.buf_lines()`.
--- - With |:Pick| command: `:Pick buf_lines scope='current'`
--- Note: this requires calling |MiniExtra.setup()|.
MiniExtra.pickers = {}
--- Buffer lines picker
---
--- Pick from buffer lines. Notes:
--- - Loads all target buffers which are currently unloaded.
---
---@param local_opts __extra_pickers_local_opts
--- Possible fields:
--- - <scope> `(string)` - one of "all" (normal listed buffers) or "current".
--- Default: "all".
--- __extra_pickers_preserve_order
---@param opts __extra_pickers_opts
---
---@return __extra_pickers_return
MiniExtra.pickers.buf_lines = function(local_opts, opts)
local pick = H.validate_pick('buf_lines')
local_opts = vim.tbl_deep_extend('force', { scope = 'all', preserve_order = false }, local_opts or {})
local scope = H.pick_validate_scope(local_opts, { 'all', 'current' }, 'buf_lines')
local is_scope_all = scope == 'all'
-- Define non-blocking callable `items` because getting all lines from all
-- buffers (plus loading them) may take visibly long time
local buffers = {}
if is_scope_all then
for _, buf_id in ipairs(vim.api.nvim_list_bufs()) do
if vim.bo[buf_id].buflisted and vim.bo[buf_id].buftype == '' then table.insert(buffers, buf_id) end
end
else
buffers = { vim.api.nvim_get_current_buf() }
end
local poke_picker = pick.poke_is_picker_active
local f = function()
local items = {}
for _, buf_id in ipairs(buffers) do
if not poke_picker() then return end
H.buf_ensure_loaded(buf_id)
local buf_name = H.buf_get_name(buf_id) or ''
local n_digits = math.floor(math.log10(vim.api.nvim_buf_line_count(buf_id))) + 1
local format_pattern = '%s%' .. n_digits .. 'd\0%s'
for lnum, l in ipairs(vim.api.nvim_buf_get_lines(buf_id, 0, -1, false)) do
local prefix = is_scope_all and (buf_name .. '\0') or ''
table.insert(items, { text = format_pattern:format(prefix, lnum, l), bufnr = buf_id, lnum = lnum })
end
end
pick.set_picker_items(items)
end
local items = vim.schedule_wrap(coroutine.wrap(f))
local show = H.pick_get_config().source.show
if is_scope_all and show == nil then show = H.show_with_icons end
local match_opts = { preserve_order = local_opts.preserve_order }
local match = function(stritems, inds, query) pick.default_match(stritems, inds, query, match_opts) end
local default_source = { name = string.format('Buffer lines (%s)', scope), show = show, match = match }
return H.pick_start(items, { source = default_source }, opts)
end
--- Neovim commands picker
---
--- Pick from Neovim built-in (|ex-commands|) and |user-commands|.
--- Notes:
--- - Preview shows information about the command (if available).
--- - Choosing either executes command (if reliably known that it doesn't need
--- arguments) or populates Command line with the command.
---
---@param local_opts __extra_pickers_local_opts
--- Not used at the moment.
---@param opts __extra_pickers_opts
---
---@return __extra_pickers_return
MiniExtra.pickers.commands = function(local_opts, opts)
local pick = H.validate_pick('commands')
local commands = vim.tbl_deep_extend('force', vim.api.nvim_get_commands({}), vim.api.nvim_buf_get_commands(0, {}))
local preview = function(buf_id, item)
local data = commands[item]
local lines = data == nil and { string.format('No command data for `%s` is yet available.', item) }
or vim.split(vim.inspect(data), '\n')
H.set_buflines(buf_id, lines)
end
local choose = function(item)
local data = commands[item] or {}
-- If no arguments needed, execute immediately
local keys = string.format(':%s%s', item, data.nargs == '0' and '\r' or ' ')
vim.schedule(function() vim.fn.feedkeys(keys) end)
end
local items = vim.fn.getcompletion('', 'command')
local default_opts = { source = { name = 'Commands', preview = preview, choose = choose } }
return H.pick_start(items, default_opts, opts)
end
--- Built-in diagnostic picker
---
--- Pick from |vim.diagnostic| using |vim.diagnostic.get()|.
---
---@param local_opts __extra_pickers_local_opts
--- Possible fields:
--- - <get_opts> `(table)` - options for |vim.diagnostic.get()|. Can be used
--- to limit severity or namespace. Default: `{}`.
--- - <scope> `(string)` - one of "all" (available) or "current" (buffer).
--- Default: "all".
--- - <sort_by> `(string)` - sort priority. One of "severity", "path", "none".
--- Default: "severity".
---@param opts __extra_pickers_opts
---
---@return __extra_pickers_return
MiniExtra.pickers.diagnostic = function(local_opts, opts)
local pick = H.validate_pick('diagnostic')
local_opts = vim.tbl_deep_extend('force', { get_opts = {}, scope = 'all', sort_by = 'severity' }, local_opts or {})
local scope = H.pick_validate_scope(local_opts, { 'all', 'current' }, 'diagnostic')
local sort_by = H.pick_validate_one_of('sort_by', local_opts, { 'severity', 'path', 'none' }, 'diagnostic')
local plus_one = function(x)
if x == nil then return nil end
return x + 1
end
local diag_buf_id
if scope == 'current' then diag_buf_id = vim.api.nvim_get_current_buf() end
local items = vim.deepcopy(vim.diagnostic.get(diag_buf_id, local_opts.get_opts))
-- Compute final path width
local path_width = 0
for _, item in ipairs(items) do
item.path = H.buf_get_name(item.bufnr) or ''
item.severity = item.severity or 0
path_width = math.max(path_width, vim.fn.strchars(item.path))
end
-- Sort
local compare = H.diagnostic_make_compare(sort_by)
if vim.is_callable(compare) then table.sort(items, compare) end
-- Update items
for _, item in ipairs(items) do
local severity = vim.diagnostic.severity[item.severity] or ' '
local text = item.message:gsub('\n', ' ')
item.text = string.format('%s │ %s │ %s', severity:sub(1, 1), H.ensure_text_width(item.path, path_width), text)
item.lnum, item.col, item.end_lnum, item.end_col =
plus_one(item.lnum), plus_one(item.col), plus_one(item.end_lnum), plus_one(item.end_col)
end
local hl_groups_ref = {
[vim.diagnostic.severity.ERROR] = 'DiagnosticFloatingError',
[vim.diagnostic.severity.WARN] = 'DiagnosticFloatingWarn',
[vim.diagnostic.severity.INFO] = 'DiagnosticFloatingInfo',
[vim.diagnostic.severity.HINT] = 'DiagnosticFloatingHint',
}
-- Define source
local show = function(buf_id, items_to_show, query)
pick.default_show(buf_id, items_to_show, query)
H.pick_clear_namespace(buf_id, H.ns_id.pickers)
for i, item in ipairs(items_to_show) do
H.pick_highlight_line(buf_id, i, hl_groups_ref[item.severity], 199)
end
end
local name = string.format('Diagnostic (%s)', scope)
return H.pick_start(items, { source = { name = name, choose = H.choose_with_buflisted, show = show } }, opts)
end
--- File explorer picker
---
--- Explore file system and open file.
--- Notes:
--- - Choosing a directory navigates inside it, changing picker's items and
--- current working directory.
--- - Query and preview work as usual (not only `move_next`/`move_prev` can be used).
--- - Preview works for any item.
---
--- Examples ~
---
--- - `MiniExtra.pickers.explorer()`
--- - `:Pick explorer cwd='..'` - open explorer in parent directory.
---
---@param local_opts __extra_pickers_local_opts
--- Possible fields:
--- - <cwd> `(string)` - initial directory to explore. Should be a valid
--- directory path. Default: `nil` for |current-directory|.
--- - <filter> `(function)` - callable predicate to filter items to show.
--- Will be called for every item and should return `true` if it should be
--- shown. Each item is a table with the following fields:
--- - <fs_type> `(string)` - path type. One of "directory" or "file".
--- - <path> `(string)` - item path.
--- - <text> `(string)` - shown text (path's basename).
--- - <sort> `(function)` - callable item sorter. Will be called with array
--- of items (each element with structure as described above) and should
--- return sorted array of items.
---@param opts __extra_pickers_opts
---
---@return __extra_pickers_return
MiniExtra.pickers.explorer = function(local_opts, opts)
local pick = H.validate_pick('explorer')
local_opts = vim.tbl_deep_extend('force', { cwd = nil, filter = nil, sort = nil }, local_opts or {})
local cwd = local_opts.cwd or vim.fn.getcwd()
if vim.fn.isdirectory(cwd) == 0 then H.error('`local_opts.cwd` should be valid directory path.') end
-- - Call twice "full path" to make sure that possible '..' are collapsed
cwd = H.full_path(vim.fn.fnamemodify(cwd, ':p'))
local filter = local_opts.filter or function() return true end
if not vim.is_callable(filter) then H.error('`local_opts.filter` should be callable.') end
local sort = local_opts.sort or H.explorer_default_sort
if not vim.is_callable(sort) then H.error('`local_opts.sort` should be callable.') end
-- Define source
local choose = function(item)
local path = item.path
if vim.fn.filereadable(path) == 1 then return pick.default_choose(path) end
if vim.fn.isdirectory(path) == 0 then return false end
pick.set_picker_items(H.explorer_make_items(path, filter, sort))
pick.set_picker_opts({ source = { cwd = path } })
pick.set_picker_query({})
return true
end
local show = H.pick_get_config().source.show or H.show_with_icons
local items = H.explorer_make_items(cwd, filter, sort)
local source = { items = items, name = 'File explorer', cwd = cwd, show = show, choose = choose }
opts = vim.tbl_deep_extend('force', { source = source }, opts or {})
return pick.start(opts)
end
--- Git branches picker
---
--- Pick from Git branches using `git branch`.
--- __extra_pickers_git_notes
--- - On choose opens scratch buffer with branch's history.
---
--- Examples ~
---
--- - `MiniExtra.pickers.git_branches({ scope = 'local' })` - local branches of
--- the |current-directory| parent Git repository.
--- - `:Pick git_branches path='%'` - all branches of the current file parent
--- Git repository.
---
---@param local_opts __extra_pickers_local_opts
--- Possible fields:
--- __extra_pickers_git_path
--- - <scope> `(string)` - branch scope to show. One of "all", "local", "remotes".
--- Default: "all".
---@param opts __extra_pickers_opts
---
---@return __extra_pickers_return
MiniExtra.pickers.git_branches = function(local_opts, opts)
local pick = H.validate_pick('git_branches')
H.validate_git('git_branches')
local_opts = vim.tbl_deep_extend('force', { path = nil, scope = 'all' }, local_opts or {})
local scope = H.pick_validate_scope(local_opts, { 'all', 'local', 'remotes' }, 'git_branches')
-- Compute path to repo with target path (as it might differ from current)
local path, path_type = H.git_normalize_path(local_opts.path, 'git_branches')
local repo_dir = H.git_get_repo_dir(path, path_type, 'git_branches')
-- Define source
local show_history = function(buf_id, item)
local branch = item:match('^%*?%s*(%S+)')
local cmd = { 'git', '-C', repo_dir, 'log', branch, '--format=format:%h %s' }
H.cli_show_output(buf_id, cmd)
end
local preview = show_history
local choose = H.make_show_in_target_win('git_branches', show_history)
local command = { 'git', 'branch', '-v', '--no-color', '--list' }
if scope == 'all' or scope == 'remotes' then table.insert(command, 3, '--' .. scope) end
local name = string.format('Git branches (%s)', scope)
local default_source = { name = name, cwd = repo_dir, preview = preview, choose = choose }
opts = vim.tbl_deep_extend('force', { source = default_source }, opts or {})
return pick.builtin.cli({ command = command }, opts)
end
--- Git commits picker
---
--- Pick from Git commits using `git log`.
--- __extra_pickers_git_notes
--- - On choose opens scratch buffer with commit's diff.
---
--- Examples ~
---
--- - `MiniExtra.pickers.git_commits()` - all commits from parent Git
--- repository of |current-directory|.
--- - `MiniExtra.pickers.git_commits({ path = 'subdir' })` - commits affecting
--- files from 'subdir' subdirectory.
--- - `:Pick git_commits path='%'` commits affecting current file.
---
---@param local_opts __extra_pickers_local_opts
--- Possible fields:
--- __extra_pickers_git_path
---@param opts __extra_pickers_opts
---
---@return __extra_pickers_return
MiniExtra.pickers.git_commits = function(local_opts, opts)
local pick = H.validate_pick('git_commits')
H.validate_git('git_commits')
local_opts = vim.tbl_deep_extend('force', { path = nil }, local_opts or {})
-- Compute path to repo with target path (as it might differ from current)
local path, path_type = H.git_normalize_path(local_opts.path, 'git_commits')
local repo_dir = H.git_get_repo_dir(path, path_type, 'git_commits')
if local_opts.path == nil then path = repo_dir end
-- Define source
local preview = function(buf_id, item)
if type(item) ~= 'string' then return end
-- Only define syntax highlighting to avoid costly `FileType` autocommands
vim.bo[buf_id].syntax = 'git'
H.cli_show_output(buf_id, { 'git', '-C', repo_dir, '--no-pager', 'show', item:match('^(%S+)') })
end
local choose_show = function(buf_id, item)
preview(buf_id, item)
-- Set filetype on opened buffer to trigger appropriate `FileType` event
vim.bo[buf_id].filetype = 'git'
end
local choose = H.make_show_in_target_win('git_commits', choose_show)
local command = { 'git', 'log', [[--format=format:%h %s]], '--', path }
local name = string.format('Git commits (%s)', local_opts.path == nil and 'all' or 'for path')
local default_source = { name = name, cwd = repo_dir, preview = preview, choose = choose }
opts = vim.tbl_deep_extend('force', { source = default_source }, opts or {})
return pick.builtin.cli({ command = command }, opts)
end
--- Git files picker
---
--- Pick from Git files using `git ls-files`.
--- __extra_pickers_git_notes
---
--- Examples ~
---
--- - `MiniExtra.pickers.git_files({ scope = 'ignored' })` - ignored files from
--- parent Git repository of |current-directory|.
--- - `:Pick git_files path='subdir' scope='modified'` - files from 'subdir'
--- subdirectory which are ignored by Git.
---
---@param local_opts __extra_pickers_local_opts
--- Possible fields:
--- __extra_pickers_git_path
--- - <scope> `(string)` - files scope to show. One of
--- - "tracked" (`--cached` Git flag).
--- - "modified" (`--modified` Git flag).
--- - "untracked" (`--others` Git flag).
--- - "ignored" (`--ignored` Git flag).
--- - "deleted" (`--deleted` Git flag).
--- Default: "tracked".
---@param opts __extra_pickers_opts
---
---@return __extra_pickers_return
MiniExtra.pickers.git_files = function(local_opts, opts)
local pick = H.validate_pick('git_files')
H.validate_git('git_files')
local_opts = vim.tbl_deep_extend('force', { path = nil, scope = 'tracked' }, local_opts or {})
local allowed_scope = { 'tracked', 'modified', 'untracked', 'ignored', 'deleted' }
local scope = H.pick_validate_scope(local_opts, allowed_scope, 'git_files')
-- Compute path to repo with target path (as it might differ from current)
local path, path_type = H.git_normalize_path(local_opts.path, 'git_files')
H.git_get_repo_dir(path, path_type, 'git_files')
local path_dir = path_type == 'directory' and path or vim.fn.fnamemodify(path, ':h')
-- Define source
local show = H.pick_get_config().source.show or H.show_with_icons
--stylua: ignore
local command = ({
tracked = { 'git', '-C', path_dir, 'ls-files', '--cached' },
modified = { 'git', '-C', path_dir, 'ls-files', '--modified' },
untracked = { 'git', '-C', path_dir, 'ls-files', '--others' },
ignored = { 'git', '-C', path_dir, 'ls-files', '--others', '--ignored', '--exclude-standard' },
deleted = { 'git', '-C', path_dir, 'ls-files', '--deleted' },
})[local_opts.scope]
local name = string.format('Git files (%s)', local_opts.scope)
local default_source = { name = name, cwd = path_dir, show = show }
opts = vim.tbl_deep_extend('force', { source = default_source }, opts or {})
return pick.builtin.cli({ command = command }, opts)
end
--- Git hunks picker
---
--- Pick from Git hunks using `git diff`.
--- __extra_pickers_git_notes
--- - On choose navigates to hunk's first change.
---
--- Examples ~
---
--- - `MiniExtra.pickers.git_hunks({ scope = 'staged' })` - staged hunks from
--- parent Git repository of |current-directory|.
--- - `:Pick git_hunks path='%' n_context=0` - hunks from current file computed
--- with no context.
---
---@param local_opts __extra_pickers_local_opts
--- Possible fields:
--- - <n_context> `(number)` - number of context lines to show in hunk's preview.
--- Default: 3.
--- __extra_pickers_git_path
--- - <scope> `(string)` - hunks scope to show. One of "unstaged" or "staged".
--- Default: "unstaged".
---@param opts __extra_pickers_opts
---
---@return __extra_pickers_return
MiniExtra.pickers.git_hunks = function(local_opts, opts)
local pick = H.validate_pick('git_hunks')
H.validate_git('git_hunks')
local default_local_opts = { n_context = 3, path = nil, scope = 'unstaged' }
local_opts = vim.tbl_deep_extend('force', default_local_opts, local_opts or {})
if not (type(local_opts.n_context) == 'number' and local_opts.n_context >= 0) then
H.error('`n_context` option in `pickers.git_hunks` picker should be non-negative number.')
end
local n_context = math.floor(local_opts.n_context)
local scope = H.pick_validate_scope(local_opts, { 'unstaged', 'staged' }, 'git_hunks')
-- Compute path to repo with target path (as it might differ from current)
local path, path_type = H.git_normalize_path(local_opts.path, 'git_hunks')
local repo_dir = H.git_get_repo_dir(path, path_type, 'git_hunks')
if local_opts.path == nil then path = repo_dir end
-- Define source
local show = H.pick_get_config().source.show or H.show_with_icons
local preview = function(buf_id, item)
vim.bo[buf_id].syntax = 'diff'
local lines = vim.deepcopy(item.header)
vim.list_extend(lines, item.hunk)
H.set_buflines(buf_id, lines)
end
local command = { 'git', 'diff', '--patch', '--unified=' .. n_context, '--color=never', '--', path }
if scope == 'staged' then table.insert(command, 4, '--cached') end
local postprocess = function(lines) return H.git_difflines_to_hunkitems(lines, n_context) end
local name = string.format('Git hunks (%s %s)', scope, local_opts.path == nil and 'all' or 'for path')
local default_source = { name = name, cwd = repo_dir, show = show, preview = preview }
opts = vim.tbl_deep_extend('force', { source = default_source }, opts or {})
return pick.builtin.cli({ command = command, postprocess = postprocess }, opts)
end
--- Matches from 'mini.hipatterns' picker
---
--- Pick from |MiniHipatterns| matches using |MiniHipatterns.get_matches()|.
--- Notes:
--- - Requires 'mini.hipatterns'.
--- - Highlighter identifier is highlighted with its highlight group.
---
---@param local_opts __extra_pickers_local_opts
--- Possible fields:
--- - <scope> `(string)` - one of "all" (buffers with enabled 'mini.hipatterns')
--- or "current" (buffer). Default: "all".
--- - <highlighters> `(table|nil)` - highlighters for which to find matches.
--- Forwarded to |MiniHipatterns.get_matches()|. Default: `nil`.
---@param opts __extra_pickers_opts
---
---@return __extra_pickers_return
MiniExtra.pickers.hipatterns = function(local_opts, opts)
local pick = H.validate_pick('hipatterns')
local has_hipatterns, hipatterns = pcall(require, 'mini.hipatterns')
if not has_hipatterns then H.error([[`pickers.hipatterns` requires 'mini.hipatterns' which can not be found.]]) end
local_opts = vim.tbl_deep_extend('force', { highlighters = nil, scope = 'all' }, local_opts or {})
if local_opts.highlighters ~= nil and not H.islist(local_opts.highlighters) then
H.error('`local_opts.highlighters` should be an array of highlighter identifiers.')
end
local highlighters = local_opts.highlighters
local scope = H.pick_validate_scope(local_opts, { 'all', 'current' }, 'hipatterns')
-- Get items
local buffers = scope == 'all' and hipatterns.get_enabled_buffers() or { vim.api.nvim_get_current_buf() }
local items, highlighter_width = {}, 0
for _, buf_id in ipairs(buffers) do
local lines = vim.api.nvim_buf_get_lines(buf_id, 0, -1, false)
local buf_name = H.buf_get_name(buf_id)
if buf_name == '' then buf_name = 'Buffer_' .. buf_id end
for _, match in ipairs(hipatterns.get_matches(buf_id, highlighters)) do
match.highlighter = tostring(match.highlighter)
match.buf_name, match.line = buf_name, lines[match.lnum]
table.insert(items, match)
highlighter_width = math.max(highlighter_width, vim.fn.strchars(match.highlighter))
end
end
for _, item in ipairs(items) do
--stylua: ignore
item.text = string.format(
'%s │ %s│%d│%d│%s',
H.ensure_text_width(item.highlighter, highlighter_width),
item.buf_name, item.lnum, item.col, item.line
)
item.buf_name, item.line = nil, nil
end
local show = function(buf_id, items_to_show, query)
pick.default_show(buf_id, items_to_show, query)
H.pick_clear_namespace(buf_id, H.ns_id.pickers)
for i, item in ipairs(items_to_show) do
local end_col = string.len(item.highlighter)
local extmark_opts = { hl_group = item.hl_group, end_row = i - 1, end_col = end_col, priority = 1 }
vim.api.nvim_buf_set_extmark(buf_id, H.ns_id.pickers, i - 1, 0, extmark_opts)
end
end
local name = string.format('Mini.hipatterns matches (%s)', scope)
return H.pick_start(items, { source = { name = name, show = show } }, opts)
end
--- Neovim history picker
---
--- Pick from output of |:history|.
--- Notes:
--- - Has no preview.
--- - Choosing action depends on scope:
--- - For "cmd" / ":" scopes, the command is executed.
--- - For "search" / "/" / "?" scopes, search is redone.
--- - For other scopes nothing is done (but chosen item is still returned).
---
--- Examples ~
---
--- - Command history: `MiniExtra.pickers.history({ scope = ':' })`
--- - Search history: `:Pick history scope='/'`
---
---@param local_opts __extra_pickers_local_opts
--- Possible fields:
--- - <scope> `(string)` - any allowed {name} flag of |:history| command.
--- Note: word abbreviations are not allowed. Default: "all".
---@param opts __extra_pickers_opts
---
---@return __extra_pickers_return
MiniExtra.pickers.history = function(local_opts, opts)
local pick = H.validate_pick('history')
local_opts = vim.tbl_deep_extend('force', { scope = 'all' }, local_opts or {})
local allowed_scope = { 'all', 'cmd', 'search', 'expr', 'input', 'debug', ':', '/', '?', '=', '@', '>' }
local scope = H.pick_validate_scope(local_opts, allowed_scope, 'history')
--stylua: ignore
local type_ids = {
cmd = ':', search = '/', expr = '=', input = '@', debug = '>',
[':'] = ':', ['/'] = '/', ['='] = '=', ['@'] = '@', ['>'] = '>',
['?'] = '?',
}
-- Construct items
local items = {}
local names = scope == 'all' and { 'cmd', 'search', 'expr', 'input', 'debug' } or { scope }
for _, cur_name in ipairs(names) do
local cmd_output = vim.api.nvim_exec(':history ' .. cur_name, true)
local lines = vim.split(cmd_output, '\n')
local id = type_ids[cur_name]
-- Output of `:history` is sorted from oldest to newest
for i = #lines, 2, -1 do
local hist_entry = lines[i]:match('^.-%-?%d+%s+(.*)$')
table.insert(items, string.format('%s %s', id, hist_entry))
end
end
-- Define source
local preview = H.pick_make_no_preview('history')
local choose = function(item)
if not (type(item) == 'string' and vim.fn.mode() == 'n') then return end
local id, entry = item:match('^(.) (.*)$')
if id == ':' or id == '/' or id == '?' then
vim.schedule(function() vim.fn.feedkeys(id .. entry .. '\r', 'nx') end)
end
end
local default_source = { name = string.format('History (%s)', scope), preview = preview, choose = choose }
return H.pick_start(items, { source = default_source }, opts)
end
--- Highlight groups picker
---
--- Pick and preview highlight groups.
--- Notes:
--- - Item line is colored with same highlight group it represents.
--- - Preview shows highlight's definition (as in |:highlight| with {group-name}).
--- - Choosing places highlight definition in Command line to update and apply.
---
---@param local_opts __extra_pickers_local_opts
--- Not used at the moment.
---@param opts __extra_pickers_opts
---
---@return __extra_pickers_return
MiniExtra.pickers.hl_groups = function(local_opts, opts)
local pick = H.validate_pick('hl_groups')
-- Construct items
local group_data = vim.split(vim.api.nvim_exec('highlight', true), '\n')
local items = {}
for _, l in ipairs(group_data) do
local group = l:match('^(%S+)')
if group ~= nil then table.insert(items, group) end
end
local show = function(buf_id, items_to_show, query)
H.set_buflines(buf_id, items_to_show)
H.pick_clear_namespace(buf_id, H.ns_id.pickers)
-- Highlight line with highlight group of its item
for i = 1, #items_to_show do
H.pick_highlight_line(buf_id, i, items_to_show[i], 300)
end
end
-- Define source
local preview = function(buf_id, item)
local lines = vim.split(vim.api.nvim_exec('hi ' .. item, true), '\n')
H.set_buflines(buf_id, lines)
end
local choose = function(item)
local hl_def = vim.split(vim.api.nvim_exec('hi ' .. item, true), '\n')[1]
hl_def = hl_def:gsub('^(%S+)%s+xxx%s+', '%1 ')
vim.schedule(function() vim.fn.feedkeys(':hi ' .. hl_def, 'n') end)
end
local default_source = { name = 'Highlight groups', show = show, preview = preview, choose = choose }
return H.pick_start(items, { source = default_source }, opts)
end
--- Neovim keymaps picker
---
--- Pick and preview data about Neovim keymaps.
--- Notes:
--- - Item line contains data about keymap mode, whether it is buffer local, its
--- left hand side, and inferred description.
--- - Preview shows keymap data or callback source (if present and reachable).
--- - Choosing emulates pressing the left hand side of the keymap.
---
---@param local_opts __extra_pickers_local_opts
--- Possible fields:
--- - <mode> `(string)` - modes to show. One of "all" or appropriate mode
--- for |nvim_set_keymap()|. Default: "all".
--- - <scope> `(string)` - scope to show. One of "all", "global", "buf".
--- Default: "all".
---@param opts __extra_pickers_opts
---
---@return __extra_pickers_return
MiniExtra.pickers.keymaps = function(local_opts, opts)
local pick = H.validate_pick('keymaps')
local_opts = vim.tbl_deep_extend('force', { mode = 'all', scope = 'all' }, local_opts or {})
local mode = H.pick_validate_one_of('mode', local_opts, { 'all', 'n', 'x', 's', 'o', 'i', 'l', 'c', 't' }, 'keymaps')
local scope = H.pick_validate_scope(local_opts, { 'all', 'global', 'buf' }, 'keymaps')
-- Create items
local items = {}
local populate_modes = mode == 'all' and { 'n', 'x', 's', 'o', 'i', 'l', 'c', 't' } or { mode }
local max_lhs_width = 0
local populate_items = function(source)
for _, m in ipairs(populate_modes) do
for _, maparg in ipairs(source(m)) do
local desc = maparg.desc ~= nil and vim.inspect(maparg.desc) or maparg.rhs
local lhs = vim.fn.keytrans(maparg.lhsraw or maparg.lhs)
max_lhs_width = math.max(vim.fn.strchars(lhs), max_lhs_width)
table.insert(items, { lhs = lhs, desc = desc, maparg = maparg })
end
end
end
if scope == 'all' or scope == 'buf' then populate_items(function(m) return vim.api.nvim_buf_get_keymap(0, m) end) end
if scope == 'all' or scope == 'global' then populate_items(vim.api.nvim_get_keymap) end
for _, item in ipairs(items) do
local buf_map_indicator = item.maparg.buffer == 0 and ' ' or '@'
0