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

Skip to content

Latest commit

 

History

History
1529 lines (1335 loc) · 55 KB

starter.lua

File metadata and controls

1529 lines (1335 loc) · 55 KB
---@param exclude_sections table|nil Array of section names (values of `section`
--- element of item) for which index won't be added. Default: `{}`.
---
---@return function Content hook.
MiniStarter.gen_hook.indexing = function(grouping, exclude_sections)
grouping = grouping or 'all'
exclude_sections = exclude_sections or {}
local per_section = grouping == 'section'
return function(content, _)
local cur_section, n_section, n_item = nil, 0, 0
local coords = MiniStarter.content_coords(content, 'item')
for _, c in ipairs(coords) do
local unit = content[c.line][c.unit]
local item = unit.item
if not vim.tbl_contains(exclude_sections, item.section) then
n_item = n_item + 1
if cur_section ~= item.section then
cur_section = item.section
-- Cycle through lower case letters
n_section = math.fmod(n_section, 26) + 1
n_item = per_section and 1 or n_item
end
local section_index = per_section and string.char(96 + n_section) or ''
unit.string = ('%s%s. %s'):format(section_index, n_item, unit.string)
end
end
return content
end
end
--- Hook generator for aligning content
---
--- Output is a content hook which independently aligns content horizontally
--- and vertically. Window width and height are taken from first window in current
--- tabpage displaying the starter buffer.
---
--- Basically, this computes left and top pads for |MiniStarter.gen_hook.padding|
--- such that output lines would appear aligned in certain way.
---
---@param horizontal string|nil One of "left", "center", "right". Default: "left".
---@param vertical string|nil One of "top", "center", "bottom". Default: "top".
---
---@return function Content hook.
MiniStarter.gen_hook.aligning = function(horizontal, vertical)
horizontal = horizontal or 'left'
vertical = vertical or 'top'
local horiz_coef = ({ left = 0, center = 0.5, right = 1.0 })[horizontal]
local vert_coef = ({ top = 0, center = 0.5, bottom = 1.0 })[vertical]
return function(content, buf_id)
local win_id = vim.fn.bufwinid(buf_id)
if win_id < 0 then return end
local line_strings = MiniStarter.content_to_lines(content)
-- Align horizontally
-- Don't use `string.len()` to account for multibyte characters
local lines_width = vim.tbl_map(function(l) return vim.fn.strdisplaywidth(l) end, line_strings)
local min_right_space = vim.api.nvim_win_get_width(win_id) - math.max(unpack(lines_width))
local left_pad = math.max(math.floor(horiz_coef * min_right_space), 0)
-- Align vertically
local bottom_space = vim.api.nvim_win_get_height(win_id) - #line_strings
local top_pad = math.max(math.floor(vert_coef * bottom_space), 0)
return MiniStarter.gen_hook.padding(left_pad, top_pad)(content)
end
end
-- Work with content ----------------------------------------------------------
--- Get content of Starter buffer
---
--- Generally, buffer content is a table in the form of "2d array" (or rather
--- "2d list" because number of elements can differ):
--- - Each element represents content line: an array with content units to be
--- displayed in one buffer line.
--- - Each content unit is a table with at least the following elements:
--- - "type" - string with type of content. Something like "item",
--- "section", "header", "footer", "empty", etc.
--- - "string" - which string should be displayed. May be an empty string.
--- - "hl" - which highlighting should be applied to content string. May be
--- `nil` for no highlighting.
---
--- See |MiniStarter.content_to_lines| for converting content to buffer lines
--- and |MiniStarter.content_to_items| - to list of parsed items.
---
--- Notes:
--- - Content units with type "item" also have `item` element with all
--- information about an item it represents. Those elements are used directly
--- to create an array of items used for query.
---
---@param buf_id __starter_buf_id
MiniStarter.get_content = function(buf_id)
buf_id = buf_id or vim.api.nvim_get_current_buf()
if not H.validate_starter_buf_id(buf_id, 'get_content()', 'error') then return end
return H.buffer_data[buf_id].content
end
--- Helper to iterate through content
---
--- Basically, this traverses content "2d array" (in depth-first fashion; top
--- to bottom, left to right) and returns "coordinates" of units for which
--- `predicate` is true-ish.
---
---@param content table|nil Content "2d array". Default: content of current buffer.
---@param predicate function|string|nil Predictate to filter units. If it is:
--- - Function, then it is evaluated with unit as input.
--- - String, then it checks unit to have this type (allows easy getting of
--- units with some type).
--- - `nil`, all units are kept.
---
---@return table Array of resulting units' coordinates. Each coordinate is a
--- table with <line> and <unit> keys. To retrieve actual unit from coordinate
--- `c`, use `content[c.line][c.unit]`.
MiniStarter.content_coords = function(content, predicate)
content = content or MiniStarter.get_content()
if predicate == nil then predicate = function(_) return true end end
if type(predicate) == 'string' then
local pred_type = predicate
predicate = function(unit) return unit.type == pred_type end
end
local res = {}
for l_num, line in ipairs(content) do
for u_num, unit in ipairs(line) do
if predicate(unit) then table.insert(res, { line = l_num, unit = u_num }) end
end
end
return res
end
-- stylua: ignore start
--- Convert content to buffer lines
---
--- One buffer line is made by concatenating `string` element of units within
--- same content line.
---
---@param content table|nil Content "2d array". Default: content of current buffer.
---
---@return table Array of strings for each buffer line.
MiniStarter.content_to_lines = function(content)
return vim.tbl_map(
function(content_line)
return table.concat(
-- Ensure that each content line is indeed a single buffer line
vim.tbl_map(function(x) return x.string:gsub('\n', ' ') end, content_line), ''
)
end,
content or MiniStarter.get_content()
)
end
-- stylua: ignore end
--- Convert content to items
---
--- Parse content (in depth-first fashion) and retrieve each item from `item`
--- element of content units with type "item". This also:
--- - Computes some helper information about how item will be actually
--- displayed (after |MiniStarter.content_to_lines|) and minimum number of
--- prefix characters needed for a particular item to be queried single.
--- - Modifies item's `name` element taking it from corresponing `string`
--- element of content unit. This allows modifying item's `name` at the stage
--- of content hooks (like, for example, in |MiniStarter.gen_hook.indexing|).
---
---@param content table|nil Content "2d array". Default: content of current buffer.
---
---@return table Array of items.
MiniStarter.content_to_items = function(content)
content = content or MiniStarter.get_content()
-- NOTE: this havily utilizes 'modify by reference' nature of Lua tables
local items = {}
for l_num, line in ipairs(content) do
-- Track 0-based starting column of current unit (using byte length)
local start_col = 0
for _, unit in ipairs(line) do
-- Cursor position is (1, 0)-based
local cursorpos = { l_num, start_col }
if unit.type == 'item' then
local item = unit.item
-- Take item's name from content string
item.name = unit.string:gsub('\n', ' ')
item._line = l_num - 1
item._start_col = start_col
item._end_col = start_col + unit.string:len()
-- Don't overwrite possible cursor position from item's bullet
item._cursorpos = item._cursorpos or cursorpos
table.insert(items, item)
end
-- Prefer placing cursor at start of item's bullet
if unit.type == 'item_bullet' and unit._place_cursor then
-- Item bullet uses 'private' `_item` element instead of `item`
unit._item._cursorpos = cursorpos
end
start_col = start_col + unit.string:len()
end
end
-- Compute length of unique prefix for every item's name (ignoring case)
local strings = vim.tbl_map(function(x) return x.name:lower() end, items)
local nprefix = H.unique_nprefix(strings)
for i, n in ipairs(nprefix) do
items[i]._nprefix = n
end
return items
end
-- Other exported functions ---------------------------------------------------
--- Evaluate current item
---
--- Note that it resets current query before evaluation, as it is rarely needed
--- any more.
---
---@param buf_id __starter_buf_id
MiniStarter.eval_current_item = function(buf_id)
buf_id = buf_id or vim.api.nvim_get_current_buf()
if not H.validate_starter_buf_id(buf_id, 'eval_current_item()') then return end
-- Reset query before evaluation without query echo (avoids hit-enter-prompt)
H.make_query(vim.api.nvim_get_current_buf(), '', false)
local data = H.buffer_data[buf_id]
H.eval_fun_or_string(data.items[data.current_item_id].action, true)
end
--- Update current item
---
--- This makes next (with respect to `direction`) active item to be current.
---
---@param direction string One of "next" or "previous".
---@param buf_id __starter_buf_id
MiniStarter.update_current_item = function(direction, buf_id)
buf_id = buf_id or vim.api.nvim_get_current_buf()
if not H.validate_starter_buf_id(buf_id, 'update_current_item()') then return end
local data = H.buffer_data[buf_id]
-- Advance current item
local prev_current = data.current_item_id
data.current_item_id = H.next_active_item_id(buf_id, data.current_item_id, direction)
if data.current_item_id == prev_current then return end
-- Update cursor position
H.position_cursor_on_current_item(buf_id)
-- Highlight current item
vim.api.nvim_buf_clear_namespace(buf_id, H.ns.current_item, 0, -1)
H.add_hl_current_item(buf_id 8A81 )
end
--- Add character to current query
---
--- - Update current query by appending `char` to its end (only if it results
--- into at least one active item) or delete latest character if `char` is `nil`.
--- - Recompute status of items: "active" if its name starts with new query,
--- "inactive" otherwise.
--- - Update highlighting: whole strings for "inactive" items, current query
--- for "active" items.
---
---@param char string|nil Single character to be added to query. If `nil`, deletes
--- latest character from query.
---@param buf_id __starter_buf_id
MiniStarter.add_to_query = function(char, buf_id)
buf_id = buf_id or vim.api.nvim_get_current_buf()
if not H.validate_starter_buf_id(buf_id, 'add_to_query()') then return end
local data = H.buffer_data[buf_id]
local new_query
if char == nil then
new_query = data.query:sub(0, data.query:len() - 1)
else
new_query = ('%s%s'):format(data.query, char)
end
H.make_query(buf_id, new_query)
end
--- Set current query
---
---@param query string|nil Query to be set (only if it results into at least one
--- active item). Default: `nil` for setting query to empty string, which
--- essentially resets query.
---@param buf_id __starter_buf_id
MiniStarter.set_query = function(query, buf_id)
query = query or ''
if type(query) ~= 'string' then error('`query` should be either `nil` or string.') end
buf_id = buf_id or vim.api.nvim_get_current_buf()
if not H.validate_starter_buf_id(buf_id, 'add_to_query()') then return end
H.make_query(buf_id, query)
end
--- Act on |CursorMoved| by repositioning cursor in fixed place.
MiniStarter.on_cursormoved = function(buf_id)
buf_id = buf_id or vim.api.nvim_get_current_buf()
if not H.validate_starter_buf_id(buf_id, 'on_cursormoved()') then return end
H.position_cursor_on_current_item(buf_id)
end
-- Helper data ================================================================
-- Module default config
H.default_config = MiniStarter.config
-- Default config values
H.default_items = {
function()
if _G.MiniSessions == nil then return {} end
return MiniStarter.sections.sessions(5, true)()
end,
MiniStarter.sections.recent_files(5, false, false),
MiniStarter.sections.builtin_actions(),
}
H.default_header = function()
local hour = tonumber(vim.fn.strftime('%H'))
-- [04:00, 12:00) - morning, [12:00, 20:00) - day, [20:00, 04:00) - evening
local part_id = math.floor((hour + 4) / 8) + 1
local day_part = ({ 'evening', 'morning', 'afternoon', 'evening' })[part_id]
local username = vim.loop.os_get_passwd()['username'] or 'USERNAME'
return ('Good %s, %s'):format(day_part, username)
end
H.default_footer = [[
Type query to filter items
<BS> deletes latest character from query
<Esc> resets current query
<Down/Up>, <C-n/p>, <M-j/k> move current item
<CR> executes action of current item
<C-c> closes this buffer]]
H.default_content_hooks = { MiniStarter.gen_hook.adding_bullet(), MiniStarter.gen_hook.aligning('center', 'center') }
-- Storage for all Starter buffers. Fields - buffer number. Values - table:
-- - <content> - buffer content (2d array of units)
-- - <current_item_id> - identifier of current item
-- - <footer> - table of strings
0