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

Skip to content

Latest commit

 

History

History
3146 lines (2717 loc) · 121 KB

files.lua

File metadata and controls

3146 lines (2717 loc) · 121 KB
--- local ui_open = function() vim.ui.open(MiniFiles.get_fs_entry().path) end
---
--- vim.api.nvim_create_autocmd('User', {
--- pattern = 'MiniFilesBufferCreate',
--- callback = function(args)
--- local b = args.data.buf_id
--- vim.keymap.set('n', 'g~', set_cwd, { buffer = b, desc = 'Set cwd' })
--- vim.keymap.set('n', 'gX', ui_open, { buffer = b, desc = 'OS open' })
--- vim.keymap.set('n', 'gy', yank_path, { buffer = b, desc = 'Yank path' })
--- end,
--- })
--- <
--- # Set custom bookmarks ~
---
--- Use |MiniFiles.set_bookmark()| inside `MiniFilesExplorerOpen` event: >lua
---
--- local set_mark = function(id, path, desc)
--- MiniFiles.set_bookmark(id, path, { desc = desc })
--- end
--- vim.api.nvim_create_autocmd('User', {
--- pattern = 'MiniFilesExplorerOpen',
--- callback = function()
--- set_mark('c', vim.fn.stdpath('config'), 'Config') -- path
--- set_mark('w', vim.fn.getcwd, 'Working directory') -- callable
--- set_mark('~', '~', 'Home directory')
--- end,
--- })
--- <
---@tag MiniFiles-examples
---@diagnostic disable:luadoc-miss-type-name
---@alias __minifiles_fs_entry_data_fields - <fs_type> `(string)` - one of "file" or "directory".
--- - <name> `(string)` - basename of an entry (including extension).
--- - <path> `(string)` - full path of an entry.
---@diagnostic disable:undefined-field
---@diagnostic disable:discard-returns
---@diagnostic disable:unused-local
---@diagnostic disable:cast-local-type
-- Module definition ==========================================================
local MiniFiles = {}
local H = {}
--- Module setup
---
---@param config table|nil Module config table. See |MiniFiles.config|.
---
---@usage >lua
--- require('mini.files').setup() -- use default config
--- -- OR
--- require('mini.files').setup({}) -- replace {} with your config table
--- <
MiniFiles.setup = function(config)
-- Export module
_G.MiniFiles = MiniFiles
-- Setup config
config = H.setup_config(config)
-- Apply config
H.apply_config(config)
-- Define behavior
H.create_autocommands(config)
-- Create default highlighting
H.create_default_hl()
end
--stylua: ignore
--- Defaults ~
---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section)
---@text # Content ~
---
--- `content.filter` is a predicate which takes file system entry data as input
--- and returns `true`-ish value if it should be shown.
--- Uses |MiniFiles.default_filter()| by default.
---
--- A file system entry data is a table with the following fields:
--- __minifiles_fs_entry_data_fields
---
--- `content.highlight` describes how file system entry name should be highlighted.
--- Takes file system entry data as input and returns a highlight group name.
--- Uses |MiniFiles.default_highlight()| by default.
---
--- `content.prefix` describes what text (prefix) to show to the left of file
--- system entry name (if any) and how to highlight it. It also takes file
--- system entry data as input and returns tuple of text and highlight group
--- name to be used to highlight prefix. See |MiniFiles-examples| for common
--- examples of how to use it.
--- Note: due to how lines are parsed to detect user edits for file system
--- manipulation, output of `content.prefix` should not contain `/` character.
--- Uses |MiniFiles.default_prefix()| by default.
---
--- `content.sort` describes in which order directory entries should be shown
--- in directory buffer. Takes as input and returns as output an array of file
--- system entry data. Note: technically, it can be used to filter and modify
--- its elements as well.
--- Uses |MiniFiles.default_sort()| by default.
---
--- # Mappings ~
---
--- `mappings` table can be used to customize buffer-local mappings created in each
--- directory buffer for built-in actions. Entry name corresponds to the function
--- name of the action, value - right hand side of the mapping. Supply empty
--- string to not create a particular mapping.
---
--- Default mappings are mostly designed for consistent navigation experience.
--- Here are some alternatives: >lua
---
--- -- Close explorer after opening file with `l`
--- mappings = {
--- go_in = 'L',
--- go_in_plus = 'l',
--- }
---
--- -- Don't use `h`/`l` for easier cursor navigation during text edit
--- mappings = {
--- go_in = 'L',
--- go_in_plus = '',
--- go_out = 'H',
--- go_out_plus = '',
--- }
--- <
--- # Options ~
---
--- `options.use_as_default_explorer` is a boolean indicating whether this module
--- is used as a default file explorer to edit directory (instead of built-in).
--- Note: to work with directory in |arglist|, do not lazy load this module.
---
--- `options.permanent_delete` is a boolean indicating whether to perform
--- permanent delete or move into special trash directory.
--- This is a module-specific variant of "remove to trash".
--- Target directory is `mini.files/trash` inside standard path of Neovim data
--- directory (execute `:echo stdpath('data')` to see its path in your case).
---
--- `options.lsp_timeout` is a number that defines a timeout for synchronous
--- LSP integration requests (see |MiniFiles-manipulation|).
--- Set to 0 to disable LSP integration.
---
--- # Windows ~
---
--- `windows.max_number` is a maximum number of windows allowed to be open
--- simultaneously. For example, use value 1 to always show single window.
--- There is no constraint by default.
---
--- `windows.preview` is a boolean indicating whether to show preview of
--- file/directory under cursor. Notes:
--- - It is always shown, even if current line is for not yet existing path.
--- - File preview is highlighted if its size is small enough (less than 1K
--- bytes per line or 1M bytes in total).
---
--- `windows.width_focus`, `windows.width_nofocus` and `windows.width_preview`
--- are number of columns used as `width` for the corresponding window type.
MiniFiles.config = {
-- Customization of shown content
content = {
-- Predicate for which file system entries to show
filter = nil,
-- Highlight group to use for a file system entry
highlight = nil,
-- Prefix text and highlight to show to the left of file system entry
prefix = nil,
-- Order in which to show file system entries
sort = nil,
},
-- Module mappings created only inside explorer.
-- Use `''` (empty string) to not create one.
mappings = {
close = 'q',
go_in = 'l',
go_in_plus = 'L',
go_out = 'h',
go_out_plus = 'H',
mark_goto = "'",
mark_set = 'm',
reset = '<BS>',
reveal_cwd = '@',
show_help = 'g?',
synchronize = '=',
trim_left = '<',
trim_right = '>',
},
-- General options
options = {
-- Whether to delete permanently or move into module-specific trash
permanent_delete = true,
-- Whether to use for editing directories
use_as_default_explorer = true,
-- Timeout for synchronous LSP integration requests
lsp_timeout = 1000,
},
-- Customization of explorer windows
windows = {
-- Maximum number of windows to show side by side
max_number = math.huge,
-- Whether to show preview of file/directory under cursor
preview = false,
-- Width of focused window
width_focus = 50,
-- Width of non-focused window
width_nofocus = 15,
-- Width of preview window
width_preview = 25,
},
}
--minidoc_afterlines_end
--- Open file explorer
---
--- Common ways to use this function: >lua
---
--- -- Open current working directory in a last used state
--- MiniFiles.open()
---
--- -- Fresh explorer in current working directory
--- MiniFiles.open(nil, false)
---
--- -- Open directory of current file (in last used state) focused on the file
--- MiniFiles.open(vim.api.nvim_buf_get_name(0))
---
--- -- Fresh explorer in directory of current file
--- MiniFiles.open(vim.api.nvim_buf_get_name(0), false)
---
--- -- Open last used `path` (per tabpage)
--- -- Current working directory for the first time
--- MiniFiles.open(MiniFiles.get_latest_path())
--- <
---@param path string|nil A valid file system path used as anchor.
--- If it is a path to directory, used directly.
--- If it is a path to file, its parent directory is used as anchor while
--- explorer will focus on the supplied file.
--- Default: path of |current-directory|.
---@param use_latest boolean|nil Whether to load explorer state from history
--- (based on the supplied anchor path). Default: `true`.
---@param opts table|nil Table of options overriding |MiniFiles.config| and
--- `vim.b.minifiles_config` for this particular explorer session.
MiniFiles.open = function(path, use_latest, opts)
-- Validate path: allow only valid file system path
path = H.fs_full_path(path or vim.fn.getcwd())
local fs_type = H.fs_get_type(path)
if fs_type == nil then H.error('`path` is not a valid path ("' .. path .. '")') end
-- - Allow file path to use its parent while focusing on file
local entry_name
if fs_type == 'file' then
path, entry_name = H.fs_get_parent(path), H.fs_get_basename(path)
end
-- Validate rest of the arguments
if use_latest == nil then use_latest = true end
-- Properly close possibly opened in the tabpage explorer
local did_close = MiniFiles.close()
if did_close == false then return end
-- Get explorer to open
local explorer
if use_latest then explorer = H.explorer_path_history[path] end
explorer = explorer or H.explorer_new(path)
-- Update explorer data. Don't use current explorer's data to allow more
-- interactive config change by modifying global/local configs.
explorer.opts = H.normalize_opts(nil, opts)
explorer.target_window = vim.api.nvim_get_current_win()
-- Possibly focus on file entry
explorer = H.explorer_focus_on_entry(explorer, path, entry_name)
-- Refresh and register as opened
H.explorer_refresh(explorer)
-- Register latest used path
H.latest_paths[vim.api.nvim_get_current_tabpage()] = path
-- Track lost focus
H.explorer_track_lost_focus()
-- Adjust some `vim.ui` methods to not track lost focus when they are active,
-- since most of their implementations use separate floating windows
H.explorer_adjust_vim_ui()
-- Trigger appropriate event
H.trigger_event('MiniFilesExplorerOpen')
end
--- Refresh explorer
---
--- Notes:
--- - If in `opts` at least one of `content` entry is not `nil`, all directory
--- buffers are forced to update.
---
---@param opts table|nil Table of options overriding local options of active
--- explorer session.
MiniFiles.refresh = function(opts)
local explorer = H.explorer_get()
if explorer == nil then return end
-- Decide whether buffers should be forcefully updated
local content_opts = (opts or {}).content or {}
local force_update = #vim.tbl_keys(content_opts) > 0
-- Confirm refresh if there is modified buffer
if force_update then force_update = H.explorer_ignore_pending_fs_actions(explorer, 'Update buffers') end
-- Respect explorer local options supplied inside its `open()` call but give
-- current `opts` higher precedence
explorer.opts = H.normalize_opts(explorer.opts, opts)
H.explorer_refresh(explorer, { force_update = force_update })
end
--- Synchronize explorer
---
--- - Parse user edits in directory buffers.
--- - Convert edits to file system actions and apply them after confirmation.
--- Choosing "No" skips application while "Cancel" stops synchronization.
--- - Update all directory buffers with the most relevant file system information.
--- Can be used without user edits to account for external file system changes.
---
---@return boolean Whether synchronization was done.
MiniFiles.synchronize = function()
local explorer = H.explorer_get()
if explorer == nil then return end
-- Parse and apply file system operations
local fs_actions = H.explorer_compute_fs_actions(explorer)
if fs_actions ~= nil then
local msg = table.concat(H.fs_actions_to_lines(fs_actions), '\n')
local confirm_res = vim.fn.confirm(msg, '&Yes\n&No\n&Cancel', 1, 'Question')
if confirm_res == 3 then return false end
if confirm_res == 1 then H.fs_actions_apply(fs_actions, explorer.opts.options.lsp_timeout) end
end
H.explorer_refresh(explorer, { force_update = true })
return true
end
--- Reset explorer
---
--- - Show single window focused on anchor directory (which was used as first
--- argument for |MiniFiles.open()|).
--- - Reset all tracked directory cursors to point at first entry.
MiniFiles.reset = function()
local explorer = H.explorer_get()
if explorer == nil then return end
-- Reset branch
explorer.branch = { explorer.anchor }
explorer.depth_focus = 1
-- Reset views
for _, view in pairs(explorer.views) do
view.cursor = { 1, 0 }
end
-- Skip update cursors, as they are already set
H.explorer_refresh(explorer, { skip_update_cursor = true })
end
--- Close explorer
---
---@return boolean|nil Whether closing was done or `nil` if there was nothing to close.
MiniFiles.close = function()
-- Stop possible tracking lost focus
pcall(vim.uv.timer_stop, H.timers.focus)
-- Act if there is explorer to close (even invisible after improper quit)
local explorer = H.explorer_get(nil, true)
if explorer == nil then return nil end
-- Confirm close if there is modified buffer
if not H.explorer_ignore_pending_fs_actions(explorer, 'Close') then return false end
-- Unadjust `vim.ui` methods
vim.ui.select = H.vim_ui.select or vim.ui.select
vim.ui.input = H.vim_ui.input or vim.ui.input
H.vim_ui = { select_active = false, input_active = false }
-- Trigger appropriate event
H.trigger_event('MiniFilesExplorerClose')
-- Focus on target window
explorer = H.explorer_ensure_target_window(explorer)
-- - Use `pcall()` because window might still be invalid
pcall(vim.api.nvim_set_current_win, explorer.target_window)
-- Update currently shown cursors
explorer = H.explorer_update_cursors(explorer)
-- Close shown explorer windows
for i, win_id in pairs(explorer.windows) do
H.window_close(win_id)
explorer.windows[i] = nil
end
-- Close possibly visible help window
for _, win_id in ipairs(vim.api.nvim_tabpage_list_wins(0)) do
local buf_id = vim.api.nvim_win_get_buf(win_id)
if vim.bo[buf_id].filetype == 'minifiles-help' then vim.api.nvim_win_close(win_id, true) end
end
-- Invalidate views
for path, view in pairs(explorer.views) do
explorer.views[path] = H.view_invalidate_buffer(H.view_encode_cursor(view))
end
-- Update histories and unmark as opened
H.explorer_path_history[explorer.anchor] = explorer
H.opened_explorers[explorer.tabpage_id] = nil
-- Return `true` indicating success in closing
return true
end
--- Go in entry under cursor
---
--- Depends on entry under cursor:
--- - If directory, focus on it in the window to the right.
--- - If file, open it in the window which was current during |MiniFiles.open()|.
--- Explorer is not closed after that.
---
---@param opts table|nil Options. Possible fields:
--- - <close_on_file> `(boolean)` - whether to close explorer after going
--- inside a file. Powers the `go_in_plus` mapping.
--- Default: `false`.
MiniFiles.go_in = function(opts)
local explorer = H.explorer_get()
if explorer == nil then return end
opts = vim.tbl_deep_extend('force', { close_on_file = false }, opts or {})
local should_close = opts.close_on_file
if should_close then
local fs_entry = MiniFiles.get_fs_entry()
should_close = fs_entry ~= nil and fs_entry.fs_type == 'file'
end
local cur_line = vim.fn.line('.')
explorer = H.explorer_go_in_range(explorer, vim.api.nvim_get_current_buf(), cur_line, cur_line)
H.explorer_refresh(explorer)
if should_close then MiniFiles.close() end
end
--- Go out to parent directory
---
--- - Focus on window to the left showing parent of current directory.
MiniFiles.go_out = function()
local explorer = H.explorer_get()
if explorer == nil then return end
if explorer.depth_focus == 1 then
explorer = H.explorer_open_root_parent(explorer)
else
explorer.depth_focus = explorer.depth_focus - 1
end
H.explorer_refresh(explorer)
end
--- Trim left part of branch
---
--- - Remove all branch paths to the left of currently focused one. This also
--- results into current window becoming the most left one.
MiniFiles.trim_left = function()
0