canola.nvim/lua/oil/clipboard.lua
Barrett Ruth 8dd67f91e8
refactor: revert module namespace from canola back to oil
Problem: the canola rename creates unnecessary friction for users
migrating from stevearc/oil.nvim — every `require('oil')` call and
config reference must change.

Solution: revert all module paths, URL schemes, autocmd groups,
highlight groups, and filetype names back to `oil`. The repo stays
`canola.nvim` for identity; the code is a drop-in replacement.
2026-03-10 22:48:16 -04:00

370 lines
11 KiB
Lua

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('<Esc>', 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