local cache = require("oil.cache") local columns = require("oil.columns") local config = require("oil.config") local fs = require("oil.fs") local oil = require("oil") local parser = require("oil.mutator.parser") local util = require("oil.util") local view = require("oil.view") local M = {} ---@return "wayland"|"x11"|nil local function get_linux_session_type() local xdg_session_type = vim.env.XDG_SESSION_TYPE if not xdg_session_type then return end xdg_session_type = xdg_session_type:lower() if xdg_session_type:find("x11") then return "x11" elseif xdg_session_type:find("wayland") then return "wayland" else return nil end end ---@return boolean local function is_linux_desktop_gnome() local cur_desktop = vim.env.XDG_CURRENT_DESKTOP local session_desktop = vim.env.XDG_SESSION_DESKTOP local idx = session_desktop and session_desktop:lower():find("gnome") or cur_desktop and cur_desktop:lower():find("gnome") return idx ~= nil or cur_desktop == "X-Cinnamon" or cur_desktop == "XFCE" end ---@param winid integer ---@param entry oil.InternalEntry ---@param column_defs oil.ColumnSpec[] ---@param adapter oil.Adapter ---@param bufnr integer local function write_pasted(winid, entry, column_defs, adapter, bufnr) local col_width = {} for i in ipairs(column_defs) do col_width[i + 1] = 1 end local line_table = { view.format_entry_cols(entry, column_defs, col_width, adapter, false, bufnr) } local lines, _ = util.render_table(line_table, col_width) local pos = vim.api.nvim_win_get_cursor(winid) vim.api.nvim_buf_set_lines(bufnr, pos[1], pos[1], true, lines) end ---@param parent_url string ---@param entry oil.InternalEntry local function remove_entry_from_parent_buffer(parent_url, entry) local bufnr = vim.fn.bufadd(parent_url) assert(vim.api.nvim_buf_is_loaded(bufnr), "Expected parent buffer to be loaded during paste") local adapter = assert(util.get_adapter(bufnr)) local column_defs = columns.get_supported_columns(adapter) local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) for i, line in ipairs(lines) do local result = parser.parse_line(adapter, line, column_defs) if result and result.entry == entry then vim.api.nvim_buf_set_lines(bufnr, i - 1, i, false, {}) return end end local exported = util.export_entry(entry) vim.notify( string.format("Error: could not delete original file '%s'", exported.name), vim.log.levels.ERROR ) end ---@param paths string[] ---@param delete_original? boolean local function paste_paths(paths, delete_original) local bufnr = vim.api.nvim_get_current_buf() local scheme = "oil://" local adapter = assert(config.get_adapter_by_scheme(scheme)) local column_defs = columns.get_supported_columns(scheme) local winid = vim.api.nvim_get_current_win() local parent_urls = {} local pending_paths = {} -- Handle as many paths synchronously as possible for _, path in ipairs(paths) do -- Trim the trailing slash off directories if vim.endswith(path, "/") then path = path:sub(1, -2) end local ori_entry = cache.get_entry_by_url(scheme .. path) local parent_url = util.addslash(scheme .. vim.fs.dirname(path)) if ori_entry then write_pasted(winid, ori_entry, column_defs, adapter, bufnr) if delete_original then remove_entry_from_parent_buffer(parent_url, ori_entry) end else parent_urls[parent_url] = true table.insert(pending_paths, path) end end -- If all paths could be handled synchronously, we're done if #pending_paths == 0 then return end -- Process the remaining paths by asynchronously loading them local cursor = vim.api.nvim_win_get_cursor(winid) local complete_loading = util.cb_collect(#vim.tbl_keys(parent_urls), function(err) if err then vim.notify(string.format("Error loading parent directory: %s", err), vim.log.levels.ERROR) else -- Something in this process moves the cursor to the top of the window, so have to restore it vim.api.nvim_win_set_cursor(winid, cursor) for _, path in ipairs(pending_paths) do local ori_entry = cache.get_entry_by_url(scheme .. path) if ori_entry then write_pasted(winid, ori_entry, column_defs, adapter, bufnr) if delete_original then local parent_url = util.addslash(scheme .. vim.fs.dirname(path)) remove_entry_from_parent_buffer(parent_url, ori_entry) end else vim.notify( string.format("The pasted file '%s' could not be found", path), vim.log.levels.ERROR ) end end end end) for parent_url, _ in pairs(parent_urls) do local new_bufnr = vim.api.nvim_create_buf(false, false) vim.api.nvim_buf_set_name(new_bufnr, parent_url) oil.load_oil_buffer(new_bufnr) util.run_after_load(new_bufnr, complete_loading) end end ---@return integer start ---@return integer end local function range_from_selection() -- [bufnum, lnum, col, off]; both row and column 1-indexed local start = vim.fn.getpos("v") local end_ = vim.fn.getpos(".") local start_row = start[2] local end_row = end_[2] if start_row > end_row then start_row, end_row = end_row, start_row end return start_row, end_row end M.copy_to_system_clipboard = function() local dir = oil.get_current_dir() if not dir then vim.notify("System clipboard only works for local files", vim.log.levels.ERROR) return end local entries = {} local mode = vim.api.nvim_get_mode().mode if mode == "v" or mode == "V" then if fs.is_mac then vim.notify( "Copying multiple paths to clipboard is not supported on mac", vim.log.levels.ERROR ) return end local start_row, end_row = range_from_selection() for i = start_row, end_row do table.insert(entries, oil.get_entry_on_line(0, i)) end -- leave visual mode vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("", true, false, true), "n", true) else table.insert(entries, oil.get_cursor_entry()) end -- This removes holes in the list-like table entries = vim.tbl_values(entries) if #entries == 0 then vim.notify("Could not find local file under cursor", vim.log.levels.WARN) return end local paths = {} for _, entry in ipairs(entries) do table.insert(paths, dir .. entry.name) end local cmd = {} local stdin if fs.is_mac then cmd = { "osascript", "-e", "on run args", "-e", "set the clipboard to POSIX file (first item of args)", "-e", "end run", paths[1], } elseif fs.is_linux then local xdg_session_type = get_linux_session_type() if xdg_session_type == "x11" then vim.list_extend(cmd, { "xclip", "-i", "-selection", "clipboard" }) elseif xdg_session_type == "wayland" then table.insert(cmd, "wl-copy") else vim.notify("System clipboard not supported, check $XDG_SESSION_TYPE", vim.log.levels.ERROR) return end local urls = {} for _, path in ipairs(paths) do table.insert(urls, "file://" .. path) end if is_linux_desktop_gnome() then stdin = string.format("copy\n%s\0", table.concat(urls, "\n")) vim.list_extend(cmd, { "-t", "x-special/gnome-copied-files" }) else stdin = table.concat(urls, "\n") .. "\n" vim.list_extend(cmd, { "-t", "text/uri-list" }) end else vim.notify("System clipboard not supported on Windows", vim.log.levels.ERROR) return end if vim.fn.executable(cmd[1]) == 0 then vim.notify(string.format("Could not find executable '%s'", cmd[1]), vim.log.levels.ERROR) return end local stderr = "" local jid = vim.fn.jobstart(cmd, { stderr_buffered = true, on_stderr = function(_, data) stderr = table.concat(data, "\n") end, on_exit = function(j, exit_code) if exit_code ~= 0 then vim.notify( string.format("Error copying '%s' to system clipboard\n%s", vim.inspect(paths), stderr), vim.log.levels.ERROR ) else if #paths == 1 then vim.notify(string.format("Copied '%s' to system clipboard", paths[1])) else vim.notify(string.format("Copied %d files to system clipboard", #paths)) end end end, }) assert(jid > 0, "Failed to start job") if stdin then vim.api.nvim_chan_send(jid, stdin) vim.fn.chanclose(jid, "stdin") end end ---@param lines string[] ---@return string[] local function handle_paste_output_mac(lines) local ret = {} for _, line in ipairs(lines) do if not line:match("^%s*$") then table.insert(ret, line) end end return ret end ---@param lines string[] ---@return string[] local function handle_paste_output_linux(lines) local ret = {} for _, line in ipairs(lines) do local path = line:match("^file://(.+)$") if path then table.insert(ret, util.url_unescape(path)) end end return ret end ---@param delete_original? boolean Delete the source file after pasting M.paste_from_system_clipboard = function(delete_original) local dir = oil.get_current_dir() if not dir then return end local cmd = {} local handle_paste_output if fs.is_mac then cmd = { "osascript", "-e", "on run", "-e", "POSIX path of (the clipboard as «class furl»)", "-e", "end run", } handle_paste_output = handle_paste_output_mac elseif fs.is_linux then local xdg_session_type = get_linux_session_type() if xdg_session_type == "x11" then vim.list_extend(cmd, { "xclip", "-o", "-selection", "clipboard" }) elseif xdg_session_type == "wayland" then table.insert(cmd, "wl-paste") else vim.notify("System clipboard not supported, check $XDG_SESSION_TYPE", vim.log.levels.ERROR) return end if is_linux_desktop_gnome() then vim.list_extend(cmd, { "-t", "x-special/gnome-copied-files" }) else vim.list_extend(cmd, { "-t", "text/uri-list" }) end handle_paste_output = handle_paste_output_linux else vim.notify("System clipboard not supported on Windows", vim.log.levels.ERROR) return end local paths local stderr = "" if vim.fn.executable(cmd[1]) == 0 then vim.notify(string.format("Could not find executable '%s'", cmd[1]), vim.log.levels.ERROR) return end local jid = vim.fn.jobstart(cmd, { stdout_buffered = true, stderr_buffered = true, on_stdout = function(j, data) local lines = vim.split(table.concat(data, "\n"), "\r?\n") paths = handle_paste_output(lines) end, on_stderr = function(_, data) stderr = table.concat(data, "\n") end, on_exit = function(j, exit_code) if exit_code ~= 0 or not paths then vim.notify( string.format("Error pasting from system clipboard: %s", stderr), vim.log.levels.ERROR ) elseif #paths == 0 then vim.notify("No valid files found in system clipboard", vim.log.levels.WARN) else paste_paths(paths, delete_original) end end, }) assert(jid > 0, "Failed to start job") end return M