local config = require('oil.config') local constants = require('oil.constants') local M = {} local FIELD_ID = constants.FIELD_ID local FIELD_NAME = constants.FIELD_NAME local FIELD_TYPE = constants.FIELD_TYPE local FIELD_META = constants.FIELD_META ---@alias oil.IconProvider fun(type: string, name: string, conf: table?, ft: string?): (icon: string, hl: string) ---@param url string ---@return nil|string ---@return nil|string M.parse_url = function(url) return url:match('^(.*://)(.*)$') end ---Escapes a filename for use in :edit ---@param filename string ---@return string M.escape_filename = function(filename) local ret = vim.fn.fnameescape(filename) return ret end local _url_escape_to_char = { ['20'] = ' ', ['22'] = '“', ['23'] = '#', ['24'] = '$', ['25'] = '%', ['26'] = '&', ['27'] = '‘', ['2B'] = '+', ['2C'] = ',', ['2F'] = '/', ['3A'] = ':', ['3B'] = ';', ['3C'] = '<', ['3D'] = '=', ['3E'] = '>', ['3F'] = '?', ['40'] = '@', ['5B'] = '[', ['5C'] = '\\', ['5D'] = ']', ['5E'] = '^', ['60'] = '`', ['7B'] = '{', ['7C'] = '|', ['7D'] = '}', ['7E'] = '~', } local _char_to_url_escape = {} for k, v in pairs(_url_escape_to_char) do _char_to_url_escape[v] = '%' .. k end -- TODO this uri escape handling is very incomplete ---@param string string ---@return string M.url_escape = function(string) return (string:gsub('.', _char_to_url_escape)) end ---@param string string ---@return string M.url_unescape = function(string) return ( string:gsub('%%([0-9A-Fa-f][0-9A-Fa-f])', function(seq) return _url_escape_to_char[seq:upper()] or ('%' .. seq) end) ) end ---@param bufnr integer ---@param silent? boolean ---@return nil|oil.Adapter M.get_adapter = function(bufnr, silent) local bufname = vim.api.nvim_buf_get_name(bufnr) local adapter = config.get_adapter_by_scheme(bufname) if not adapter and not silent then vim.notify_once( string.format("[oil] could not find adapter for buffer '%s://'", bufname), vim.log.levels.ERROR ) end return adapter end ---@param text string ---@param width integer|nil ---@param align oil.ColumnAlign ---@return string padded_text ---@return integer left_padding M.pad_align = function(text, width, align) if not width then return text, 0 end local text_width = vim.api.nvim_strwidth(text) local total_pad = width - text_width if total_pad <= 0 then return text, 0 end if align == 'right' then return string.rep(' ', total_pad) .. text, total_pad elseif align == 'center' then local left_pad = math.floor(total_pad / 2) local right_pad = total_pad - left_pad return string.rep(' ', left_pad) .. text .. string.rep(' ', right_pad), left_pad else return text .. string.rep(' ', total_pad), 0 end end ---@generic T : any ---@param tbl T[] ---@param start_idx? number ---@param end_idx? number ---@return T[] M.tbl_slice = function(tbl, start_idx, end_idx) local ret = {} if not start_idx then start_idx = 1 end if not end_idx then end_idx = #tbl end for i = start_idx, end_idx do table.insert(ret, tbl[i]) end return ret end ---@param entry oil.InternalEntry ---@return oil.Entry M.export_entry = function(entry) return { name = entry[FIELD_NAME], type = entry[FIELD_TYPE], id = entry[FIELD_ID], meta = entry[FIELD_META], } end ---@param src_bufnr integer|string Buffer number or name ---@param dest_buf_name string ---@return boolean True if the buffer was replaced instead of renamed M.rename_buffer = function(src_bufnr, dest_buf_name) if type(src_bufnr) == 'string' then src_bufnr = vim.fn.bufadd(src_bufnr) if not vim.api.nvim_buf_is_loaded(src_bufnr) then vim.api.nvim_buf_delete(src_bufnr, {}) return false end end local bufname = vim.api.nvim_buf_get_name(src_bufnr) -- If this buffer is not literally a file on disk, then we can use the simple -- rename logic. The only reason we can't use nvim_buf_set_name on files is because vim will -- think that the new buffer conflicts with the file next time it tries to save. if not vim.loop.fs_stat(dest_buf_name) then ---@diagnostic disable-next-line: param-type-mismatch local altbuf = vim.fn.bufnr('#') -- This will fail if the dest buf name already exists local ok = pcall(vim.api.nvim_buf_set_name, src_bufnr, dest_buf_name) if ok then -- Renaming the buffer creates a new buffer with the old name. -- Find it and try to delete it, but don't if the buffer is in a context -- where Neovim doesn't allow buffer modifications. pcall(vim.api.nvim_buf_delete, vim.fn.bufadd(bufname), {}) if altbuf and vim.api.nvim_buf_is_valid(altbuf) then vim.fn.setreg('#', altbuf) end return false end end local is_modified = vim.bo[src_bufnr].modified local dest_bufnr = vim.fn.bufadd(dest_buf_name) pcall(vim.fn.bufload, dest_bufnr) if vim.bo[src_bufnr].buflisted then vim.bo[dest_bufnr].buflisted = true end -- If the src_bufnr was marked as modified by the previous operation, we should undo that vim.bo[src_bufnr].modified = is_modified -- If we're renaming a buffer that we're about to enter, this may be called before the buffer is -- actually in the window. We need to wait to enter the buffer and _then_ replace it. vim.schedule(function() -- Find any windows with the old buffer and replace them for _, winid in ipairs(vim.api.nvim_list_wins()) do if vim.api.nvim_win_is_valid(winid) then if vim.api.nvim_win_get_buf(winid) == src_bufnr then vim.api.nvim_win_set_buf(winid, dest_bufnr) end end end if vim.api.nvim_buf_is_valid(src_bufnr) then if vim.bo[src_bufnr].modified then local src_lines = vim.api.nvim_buf_get_lines(src_bufnr, 0, -1, true) vim.api.nvim_buf_set_lines(dest_bufnr, 0, -1, true, src_lines) end -- Try to delete, but don't if the buffer has changes pcall(vim.api.nvim_buf_delete, src_bufnr, {}) end -- Renaming a buffer won't load the undo file, so we need to do that manually if vim.bo[dest_bufnr].undofile then vim.api.nvim_buf_call(dest_bufnr, function() vim.cmd.rundo({ args = { vim.fn.undofile(dest_buf_name) }, magic = { file = false, bar = false }, mods = { emsg_silent = true, }, }) end) end end) return true end ---@param count integer ---@param cb fun(err: nil|string) M.cb_collect = function(count, cb) return function(err) if err then -- selene: allow(mismatched_arg_count) cb(err) cb = function() end else count = count - 1 if count == 0 then cb() end end end end ---@param url string ---@return string[] local function get_possible_buffer_names_from_url(url) local fs = require('oil.fs') local scheme, path = M.parse_url(url) if config.adapters[scheme] == 'files' then assert(path) return { fs.posix_to_os_path(path) } end return { url } end ---@param entry_type oil.EntryType ---@param src_url string ---@param dest_url string M.update_moved_buffers = function(entry_type, src_url, dest_url) local src_buf_names = get_possible_buffer_names_from_url(src_url) local dest_buf_name = get_possible_buffer_names_from_url(dest_url)[1] if entry_type ~= 'directory' then for _, src_buf_name in ipairs(src_buf_names) do M.rename_buffer(src_buf_name, dest_buf_name) end else M.rename_buffer(M.addslash(src_url), M.addslash(dest_url)) -- If entry type is directory, we need to rename this buffer, and then update buffers that are -- inside of this directory for _, bufnr in ipairs(vim.api.nvim_list_bufs()) do local bufname = vim.api.nvim_buf_get_name(bufnr) if vim.startswith(bufname, src_url) then -- Handle oil directory buffers vim.api.nvim_buf_set_name(bufnr, dest_url .. bufname:sub(src_url:len() + 1)) elseif bufname ~= '' and vim.bo[bufnr].buftype == '' then -- Handle regular buffers local scheme = M.parse_url(bufname) -- If the buffer is a local file, make sure we're using the absolute path if not scheme then bufname = vim.fn.fnamemodify(bufname, ':p') end for _, src_buf_name in ipairs(src_buf_names) do if vim.startswith(bufname, src_buf_name) then M.rename_buffer(bufnr, dest_buf_name .. bufname:sub(src_buf_name:len() + 1)) break end end end end end end ---@param name_or_config string|table ---@return string ---@return table|nil M.split_config = function(name_or_config) if type(name_or_config) == 'string' then return name_or_config, nil else if not name_or_config[1] and name_or_config['1'] then -- This was likely loaded from json, so the first element got coerced to a string key name_or_config[1] = name_or_config['1'] name_or_config['1'] = nil end return name_or_config[1], name_or_config end end ---@alias oil.ColumnAlign "left"|"center"|"right" ---@param lines oil.TextChunk[][] ---@param col_width integer[] ---@param col_align? oil.ColumnAlign[] ---@return string[] ---@return any[][] List of highlights {group, lnum, col_start, col_end} M.render_table = function(lines, col_width, col_align) col_align = col_align or {} local str_lines = {} local highlights = {} for _, cols in ipairs(lines) do local col = 0 local pieces = {} for i, chunk in ipairs(cols) do local text, hl if type(chunk) == 'table' then text = chunk[1] hl = chunk[2] else text = chunk end local unpadded_len = text:len() local padding text, padding = M.pad_align(text, col_width[i], col_align[i] or 'left') table.insert(pieces, text) if hl then if type(hl) == 'table' then -- hl has the form { [1]: hl_name, [2]: col_start, [3]: col_end }[] -- Notice that col_start and col_end are relative position inside -- that col, so we need to add the offset to them for _, sub_hl in ipairs(hl) do table.insert(highlights, { sub_hl[1], #str_lines, col + padding + sub_hl[2], col + padding + sub_hl[3], }) end else table.insert(highlights, { hl, #str_lines, col + padding, col + padding + unpadded_len }) end end col = col + text:len() + 1 end table.insert(str_lines, table.concat(pieces, ' ')) end return str_lines, highlights end ---@param bufnr integer ---@param highlights any[][] List of highlights {group, lnum, col_start, col_end} M.set_highlights = function(bufnr, highlights) local ns = vim.api.nvim_create_namespace('Oil') vim.api.nvim_buf_clear_namespace(bufnr, ns, 0, -1) for _, hl in ipairs(highlights) do local group, line, col_start, col_end = unpack(hl) vim.api.nvim_buf_set_extmark(bufnr, ns, line, col_start, { end_col = col_end, hl_group = group, strict = false, }) end end ---@param path string ---@param os_slash? boolean use os filesystem slash instead of posix slash ---@return string M.addslash = function(path, os_slash) local slash = '/' if os_slash and require('oil.fs').is_windows then slash = '\\' end local endslash = path:match(slash .. '$') if not endslash then return path .. slash else return path end end ---@param winid nil|integer ---@return boolean M.is_floating_win = function(winid) return vim.api.nvim_win_get_config(winid or 0).relative ~= '' end ---Recalculate the window title for the current buffer ---@param winid nil|integer ---@return string M.get_title = function(winid) if config.float.get_win_title ~= nil then return config.float.get_win_title(winid or 0) end local src_buf = vim.api.nvim_win_get_buf(winid or 0) local title = vim.api.nvim_buf_get_name(src_buf) local scheme, path = M.parse_url(title) if config.adapters[scheme] == 'files' then assert(path) local fs = require('oil.fs') title = vim.fn.fnamemodify(fs.posix_to_os_path(path), ':~') end return title end local winid_map = {} M.add_title_to_win = function(winid, opts) opts = opts or {} opts.align = opts.align or 'left' if not vim.api.nvim_win_is_valid(winid) then return end -- HACK to force the parent window to position itself -- See https://github.com/neovim/neovim/issues/13403 vim.cmd.redraw() local title = M.get_title(winid) local width = math.min(vim.api.nvim_win_get_width(winid) - 4, 2 + vim.api.nvim_strwidth(title)) local title_winid = winid_map[winid] local bufnr if title_winid and vim.api.nvim_win_is_valid(title_winid) then vim.api.nvim_win_set_width(title_winid, width) bufnr = vim.api.nvim_win_get_buf(title_winid) else bufnr = vim.api.nvim_create_buf(false, true) local col = 1 if opts.align == 'center' then col = math.floor((vim.api.nvim_win_get_width(winid) - width) / 2) elseif opts.align == 'right' then col = vim.api.nvim_win_get_width(winid) - 1 - width elseif opts.align ~= 'left' then vim.notify( string.format("Unknown oil window title alignment: '%s'", opts.align), vim.log.levels.ERROR ) end title_winid = vim.api.nvim_open_win(bufnr, false, { relative = 'win', win = winid, width = width, height = 1, row = -1, col = col, focusable = false, zindex = 151, style = 'minimal', noautocmd = true, }) winid_map[winid] = title_winid vim.api.nvim_set_option_value( 'winblend', vim.wo[winid].winblend, { scope = 'local', win = title_winid } ) vim.bo[bufnr].bufhidden = 'wipe' local update_autocmd = vim.api.nvim_create_autocmd('BufWinEnter', { desc = 'Update oil floating window title when buffer changes', pattern = '*', callback = function(params) local winbuf = params.buf if vim.api.nvim_win_get_buf(winid) ~= winbuf then return end local new_title = M.get_title(winid) local new_width = math.min(vim.api.nvim_win_get_width(winid) - 4, 2 + vim.api.nvim_strwidth(new_title)) vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, { ' ' .. new_title .. ' ' }) vim.bo[bufnr].modified = false vim.api.nvim_win_set_width(title_winid, new_width) local new_col = 1 if opts.align == 'center' then new_col = math.floor((vim.api.nvim_win_get_width(winid) - new_width) / 2) elseif opts.align == 'right' then new_col = vim.api.nvim_win_get_width(winid) - 1 - new_width end vim.api.nvim_win_set_config(title_winid, { relative = 'win', win = winid, row = -1, col = new_col, width = new_width, height = 1, }) end, }) vim.api.nvim_create_autocmd('WinClosed', { desc = 'Close oil floating window title when floating window closes', pattern = tostring(winid), callback = function() if title_winid and vim.api.nvim_win_is_valid(title_winid) then vim.api.nvim_win_close(title_winid, true) end winid_map[winid] = nil vim.api.nvim_del_autocmd(update_autocmd) end, once = true, nested = true, }) end vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, { ' ' .. title .. ' ' }) vim.bo[bufnr].modified = false vim.api.nvim_set_option_value( 'winhighlight', 'Normal:FloatTitle,NormalFloat:FloatTitle', { scope = 'local', win = title_winid } ) end ---@param action oil.Action ---@return oil.Adapter ---@return nil|oil.CrossAdapterAction M.get_adapter_for_action = function(action) local adapter = assert(config.get_adapter_by_scheme(action.url or action.src_url)) if action.dest_url then local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url)) if adapter ~= dest_adapter then if adapter.supported_cross_adapter_actions and adapter.supported_cross_adapter_actions[dest_adapter.name] then return adapter, adapter.supported_cross_adapter_actions[dest_adapter.name] elseif dest_adapter.supported_cross_adapter_actions and dest_adapter.supported_cross_adapter_actions[adapter.name] then return dest_adapter, dest_adapter.supported_cross_adapter_actions[adapter.name] else error( string.format( 'Cannot copy files from %s -> %s; no cross-adapter transfer method found', action.src_url, action.dest_url ) ) end end end return adapter end ---@param str string ---@param align "left"|"right"|"center" ---@param width integer ---@return string ---@return integer M.h_align = function(str, align, width) if align == 'center' then local padding = math.floor((width - vim.api.nvim_strwidth(str)) / 2) return string.rep(' ', padding) .. str, padding elseif align == 'right' then local padding = width - vim.api.nvim_strwidth(str) return string.rep(' ', padding) .. str, padding else return str, 0 end end ---@param bufnr integer ---@param text string|string[] ---@param opts nil|table --- h_align nil|"left"|"right"|"center" --- v_align nil|"top"|"bottom"|"center" --- actions nil|string[] --- winid nil|integer M.render_text = function(bufnr, text, opts) opts = vim.tbl_deep_extend('keep', opts or {}, { h_align = 'center', v_align = 'center', }) ---@cast opts -nil if not vim.api.nvim_buf_is_valid(bufnr) then return end if type(text) == 'string' then text = { text } end local height = 40 local width = 30 -- If no winid passed in, find the first win that displays this buffer if not opts.winid then for _, winid in ipairs(vim.api.nvim_list_wins()) do if vim.api.nvim_win_is_valid(winid) and vim.api.nvim_win_get_buf(winid) == bufnr then opts.winid = winid break end end end if opts.winid then height = vim.api.nvim_win_get_height(opts.winid) width = vim.api.nvim_win_get_width(opts.winid) end local lines = {} -- Add vertical spacing for vertical alignment if opts.v_align == 'center' then for _ = 1, (height / 2) - (#text / 2) do table.insert(lines, '') end elseif opts.v_align == 'bottom' then local num_lines = height if opts.actions then num_lines = num_lines - 2 end while #lines + #text < num_lines do table.insert(lines, '') end end -- Add the lines of text for _, line in ipairs(text) do line = M.h_align(line, opts.h_align, width) table.insert(lines, line) end -- Render the actions (if any) at the bottom local highlights = {} if opts.actions then while #lines < height - 1 do table.insert(lines, '') end local last_line, padding = M.h_align(table.concat(opts.actions, ' '), 'center', width) local col = padding for _, action in ipairs(opts.actions) do table.insert(highlights, { 'Special', #lines, col, col + 3 }) col = padding + action:len() + 4 end table.insert(lines, last_line) end vim.bo[bufnr].modifiable = true pcall(vim.api.nvim_buf_set_lines, bufnr, 0, -1, false, lines) vim.bo[bufnr].modifiable = false vim.bo[bufnr].modified = false M.set_highlights(bufnr, highlights) end ---Run a function in the context of a full-editor window ---@param bufnr nil|integer ---@param callback fun() M.run_in_fullscreen_win = function(bufnr, callback) if not bufnr then bufnr = vim.api.nvim_create_buf(false, true) vim.bo[bufnr].bufhidden = 'wipe' end local winid = vim.api.nvim_open_win(bufnr, false, { relative = 'editor', width = vim.o.columns, height = vim.o.lines, row = 0, col = 0, noautocmd = true, }) local winnr = vim.api.nvim_win_get_number(winid) vim.cmd.wincmd({ count = winnr, args = { 'w' }, mods = { noautocmd = true } }) callback() vim.cmd.close({ count = winnr, mods = { noautocmd = true, emsg_silent = true } }) end ---@param bufnr integer ---@return boolean M.is_oil_bufnr = function(bufnr) local filetype = vim.bo[bufnr].filetype if filetype == 'oil' then return true elseif filetype ~= '' then -- If the filetype is set and is NOT "oil", then it's not an oil buffer return false end local scheme = M.parse_url(vim.api.nvim_buf_get_name(bufnr)) return config.adapters[scheme] or config.adapter_aliases[scheme] end ---This is a hack so we don't end up in insert mode after starting a task ---@param prev_mode string The vim mode we were in before opening a terminal M.hack_around_termopen_autocmd = function(prev_mode) -- It's common to have autocmds that enter insert mode when opening a terminal vim.defer_fn(function() local new_mode = vim.api.nvim_get_mode().mode if new_mode ~= prev_mode then if string.find(new_mode, 'i') == 1 then vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes('', true, true, true), 'n', false) if string.find(prev_mode, 'v') == 1 or string.find(prev_mode, 'V') == 1 then vim.cmd.normal({ bang = true, args = { 'gv' } }) end end end end, 10) end ---@param opts? {include_not_owned?: boolean} ---@return nil|integer M.get_preview_win = function(opts) opts = opts or {} for _, winid in ipairs(vim.api.nvim_tabpage_list_wins(0)) do if vim.api.nvim_win_is_valid(winid) and vim.wo[winid].previewwindow and (opts.include_not_owned or vim.w[winid]['oil_preview']) then return winid end end end ---@return fun() restore Function that restores the cursor M.hide_cursor = function() vim.api.nvim_set_hl(0, 'OilPreviewCursor', { nocombine = true, blend = 100 }) local original_guicursor = vim.go.guicursor vim.go.guicursor = 'a:OilPreviewCursor/OilPreviewCursor' return function() -- HACK: see https://github.com/neovim/neovim/issues/21018 vim.go.guicursor = 'a:' vim.cmd.redrawstatus() vim.go.guicursor = original_guicursor end end ---@param bufnr integer ---@param preferred_win nil|integer ---@return nil|integer M.buf_get_win = function(bufnr, preferred_win) if preferred_win and vim.api.nvim_win_is_valid(preferred_win) and vim.api.nvim_win_get_buf(preferred_win) == bufnr then return preferred_win end for _, winid in ipairs(vim.api.nvim_tabpage_list_wins(0)) do if vim.api.nvim_win_is_valid(winid) and vim.api.nvim_win_get_buf(winid) == bufnr then return winid end end for _, winid in ipairs(vim.api.nvim_list_wins()) do if vim.api.nvim_win_is_valid(winid) and vim.api.nvim_win_get_buf(winid) == bufnr then return winid end end return nil end ---@param adapter oil.Adapter ---@param url string ---@param opts {columns?: string[], no_cache?: boolean} ---@param callback fun(err: nil|string, entries: nil|oil.InternalEntry[]) M.adapter_list_all = function(adapter, url, opts, callback) local cache = require('oil.cache') if not opts.no_cache then local entries = cache.list_url(url) if not vim.tbl_isempty(entries) then return callback(nil, vim.tbl_values(entries)) end end local ret = {} adapter.list(url, opts.columns or {}, function(err, entries, fetch_more) if err then callback(err) return end if entries then vim.list_extend(ret, entries) end if fetch_more then vim.defer_fn(fetch_more, 4) else callback(nil, ret) end end) end ---Send files from the current oil directory to quickfix ---based on the provided options. ---@param opts {target?: "qflist"|"loclist", action?: "r"|"a", only_matching_search?: boolean} M.send_to_quickfix = function(opts) if type(opts) ~= 'table' then opts = {} end local oil = require('oil') local dir = oil.get_current_dir() if type(dir) ~= 'string' then return end local range = M.get_visual_range() if not range then range = { start_lnum = 1, end_lnum = vim.fn.line('$') } end local match_all = not opts.only_matching_search local qf_entries = {} for i = range.start_lnum, range.end_lnum do local entry = oil.get_entry_on_line(0, i) if entry and entry.type == 'file' and (match_all or M.is_matching(entry)) then local qf_entry = { filename = dir .. entry.name, lnum = 1, col = 1, text = entry.name, } table.insert(qf_entries, qf_entry) end end if #qf_entries == 0 then vim.notify('[oil] No entries found to send to quickfix', vim.log.levels.WARN) return end vim.api.nvim_exec_autocmds('QuickFixCmdPre', {}) local qf_title = 'oil files' local action = opts.action == 'a' and 'a' or 'r' if opts.target == 'loclist' then vim.fn.setloclist(0, {}, action, { title = qf_title, items = qf_entries }) vim.cmd.lopen() else vim.fn.setqflist({}, action, { title = qf_title, items = qf_entries }) vim.cmd.copen() end vim.api.nvim_exec_autocmds('QuickFixCmdPost', {}) end ---@return boolean M.is_visual_mode = function() local mode = vim.api.nvim_get_mode().mode return mode:match('^[vV]') ~= nil end ---Get the current visual selection range. If not in visual mode, return nil. ---@return {start_lnum: integer, end_lnum: integer}? M.get_visual_range = function() if not M.is_visual_mode() then return end -- This is the best way to get the visual selection at the moment -- https://github.com/neovim/neovim/pull/13896 local _, start_lnum, _, _ = unpack(vim.fn.getpos('v')) local _, end_lnum, _, _, _ = unpack(vim.fn.getcurpos()) if start_lnum > end_lnum then start_lnum, end_lnum = end_lnum, start_lnum end return { start_lnum = start_lnum, end_lnum = end_lnum } end ---@param entry oil.Entry ---@return boolean M.is_matching = function(entry) -- if search highlightig is not enabled, all files are considered to match local search_highlighting_is_off = (vim.v.hlsearch == 0) if search_highlighting_is_off then return true end local pattern = vim.fn.getreg('/') local position_of_match = vim.fn.match(entry.name, pattern) return position_of_match ~= -1 end ---@param bufnr integer ---@param callback fun() M.run_after_load = function(bufnr, callback) if bufnr == 0 then bufnr = vim.api.nvim_get_current_buf() end if vim.b[bufnr].oil_ready then callback() else vim.api.nvim_create_autocmd('User', { pattern = 'OilEnter', callback = function(args) if args.data.buf == bufnr then vim.api.nvim_buf_call(bufnr, callback) return true end end, }) end end ---@param entry oil.Entry ---@return boolean M.is_directory = function(entry) local is_directory = entry.type == 'directory' or ( entry.type == 'link' and entry.meta and entry.meta.link_stat and entry.meta.link_stat.type == 'directory' ) return is_directory == true end ---Get the :edit path for an entry ---@param bufnr integer The oil buffer that contains the entry ---@param entry oil.Entry ---@param callback fun(normalized_url: string) M.get_edit_path = function(bufnr, entry, callback) local pathutil = require('oil.pathutil') local bufname = vim.api.nvim_buf_get_name(bufnr) local scheme, dir = M.parse_url(bufname) local adapter = M.get_adapter(bufnr, true) assert(scheme and dir and adapter) local url = scheme .. dir .. entry.name if M.is_directory(entry) then url = url .. '/' end if entry.name == '..' then callback(scheme .. pathutil.parent(dir)) elseif adapter.get_entry_path then adapter.get_entry_path(url, entry, callback) else adapter.normalize_url(url, callback) end end --- Check for an icon provider and return a common icon provider API ---@return (oil.IconProvider)? M.get_icon_provider = function() -- prefer mini.icons local _, mini_icons = pcall(require, 'mini.icons') -- selene: allow(global_usage) ---@diagnostic disable-next-line: undefined-field if _G.MiniIcons then return function(type, name, conf, ft) if ft then return mini_icons.get('filetype', ft) end return mini_icons.get(type == 'directory' and 'directory' or 'file', name) end end local has_nonicons, nonicons = pcall(require, 'nonicons') if has_nonicons and nonicons.get_icon then local has_devicons, devicons = pcall(require, 'nvim-web-devicons') if not has_devicons then devicons = nil end return function(type, name, conf, ft) if type == 'directory' then local icon = nonicons.get('file-directory-fill') return icon or (conf and conf.directory or ''), 'OilDirIcon' end local hl = devicons and select(2, devicons.get_icon(name)) or 'OilFileIcon' if ft then local ft_icon = nonicons.get_icon_by_filetype(ft) if ft_icon then local ft_hl = devicons and select(2, devicons.get_icon_by_filetype(ft)) return ft_icon, ft_hl or hl end end local icon = nonicons.get_icon(name) if icon then return icon, hl end local fallback = nonicons.get('file') return fallback or (conf and conf.default_file or ''), hl end end local has_devicons, devicons = pcall(require, 'nvim-web-devicons') if not has_devicons then return end return function(type, name, conf, ft) if type == 'directory' then return conf and conf.directory or '', 'OilDirIcon' else if ft then local ft_icon, ft_hl = devicons.get_icon_by_filetype(ft) if ft_icon and ft_icon ~= '' then return ft_icon, ft_hl end end local icon, hl = devicons.get_icon(name) hl = hl or 'OilFileIcon' icon = icon or (conf and conf.default_file or '') return icon, hl end end end ---Read a buffer into a scratch buffer and apply syntactic highlighting when possible ---@param path string The path to the file to read ---@param preview_method oil.PreviewMethod ---@return nil|integer M.read_file_to_scratch_buffer = function(path, preview_method) local bufnr = vim.api.nvim_create_buf(false, true) if bufnr == 0 then return end vim.bo[bufnr].bufhidden = 'wipe' vim.bo[bufnr].buftype = 'nofile' local has_lines, read_res if preview_method == 'fast_scratch' then has_lines, read_res = pcall(vim.fn.readfile, path, '', vim.o.lines) else has_lines, read_res = pcall(vim.fn.readfile, path) end local lines = has_lines and vim.split(table.concat(read_res, '\n'), '\n') or {} local ok = pcall(vim.api.nvim_buf_set_lines, bufnr, 0, -1, false, lines) if not ok then return end local ft = vim.filetype.match({ filename = path, buf = bufnr }) if ft and ft ~= '' and vim.treesitter.language.get_lang then local lang = vim.treesitter.language.get_lang(ft) -- selene: allow(empty_if) if not pcall(vim.treesitter.start, bufnr, lang) then vim.bo[bufnr].syntax = ft else end end -- Replace the scratch buffer with a real buffer if we enter it vim.api.nvim_create_autocmd('BufEnter', { desc = 'oil.nvim replace scratch buffer with real buffer', buffer = bufnr, callback = function() local winid = vim.api.nvim_get_current_win() -- Have to schedule this so all the FileType, etc autocmds will fire vim.schedule(function() if vim.api.nvim_get_current_win() == winid then vim.cmd.edit({ args = { path } }) -- If we're still in a preview window, make sure this buffer still gets treated as a -- preview if vim.wo.previewwindow then vim.bo.bufhidden = 'wipe' vim.b.oil_preview_buffer = true end end end) end, }) return bufnr end local _regcache = {} ---Check if a file matches a BufReadCmd autocmd ---@param filename string ---@return boolean M.file_matches_bufreadcmd = function(filename) local autocmds = vim.api.nvim_get_autocmds({ event = 'BufReadCmd', }) for _, au in ipairs(autocmds) do local pat = _regcache[au.pattern] if not pat then pat = vim.fn.glob2regpat(au.pattern) _regcache[au.pattern] = pat end if vim.fn.match(filename, pat) >= 0 then return true end end return false end return M