1459 lines
49 KiB
Lua
1459 lines
49 KiB
Lua
local M = {}
|
|
|
|
---@class (exact) canola.Entry
|
|
---@field name string
|
|
---@field type canola.EntryType
|
|
---@field id nil|integer Will be nil if it hasn't been persisted to disk yet
|
|
---@field parsed_name nil|string
|
|
---@field meta nil|table
|
|
|
|
---@alias canola.EntryType uv.aliases.fs_types
|
|
---@alias canola.HlRange { [1]: string, [2]: integer, [3]: integer } A tuple of highlight group name, col_start, col_end
|
|
---@alias canola.HlTuple { [1]: string, [2]: string } A tuple of text, highlight group
|
|
---@alias canola.HlRangeTuple { [1]: string, [2]: canola.HlRange[] } A tuple of text, internal highlights
|
|
---@alias canola.TextChunk string|canola.HlTuple|canola.HlRangeTuple
|
|
---@alias canola.CrossAdapterAction "copy"|"move"
|
|
|
|
---@class (exact) canola.Adapter
|
|
---@field name string The unique name of the adapter (this will be set automatically)
|
|
---@field list fun(path: string, column_defs: string[], cb: fun(err?: string, entries?: canola.InternalEntry[], fetch_more?: fun())) Async function to list a directory.
|
|
---@field is_modifiable fun(bufnr: integer): boolean Return true if this directory is modifiable (allows for directories with read-only permissions).
|
|
---@field get_column fun(name: string): nil|canola.ColumnDefinition If the adapter has any adapter-specific columns, return them when fetched by name.
|
|
---@field get_parent? fun(bufname: string): string Get the parent url of the given buffer
|
|
---@field normalize_url fun(url: string, callback: fun(url: string)) Before canola opens a url it will be normalized. This allows for link following, path normalizing, and converting an canola file url to the actual path of a file.
|
|
---@field get_entry_path? fun(url: string, entry: canola.Entry, callback: fun(path: string)) Similar to normalize_url, but used when selecting an entry
|
|
---@field render_action? fun(action: canola.Action): string Render a mutation action for display in the preview window. Only needed if adapter is modifiable.
|
|
---@field perform_action? fun(action: canola.Action, cb: fun(err: nil|string)) Perform a mutation action. Only needed if adapter is modifiable.
|
|
---@field read_file? fun(bufnr: integer) Used for adapters that deal with remote/virtual files. Read the contents of the file into a buffer.
|
|
---@field write_file? fun(bufnr: integer) Used for adapters that deal with remote/virtual files. Write the contents of a buffer to the destination.
|
|
---@field supported_cross_adapter_actions? table<string, canola.CrossAdapterAction> Mapping of adapter name to enum for all other adapters that can be used as a src or dest for move/copy actions.
|
|
---@field filter_action? fun(action: canola.Action): boolean When present, filter out actions as they are created
|
|
---@field filter_error? fun(action: canola.ParseError): boolean When present, filter out errors from parsing a buffer
|
|
|
|
---Get the entry on a specific line (1-indexed)
|
|
---@param bufnr integer
|
|
---@param lnum integer
|
|
---@return nil|canola.Entry
|
|
M.get_entry_on_line = function(bufnr, lnum)
|
|
local columns = require('canola.columns')
|
|
local parser = require('canola.mutator.parser')
|
|
local util = require('canola.util')
|
|
if vim.bo[bufnr].filetype ~= 'canola' then
|
|
return nil
|
|
end
|
|
local adapter = util.get_adapter(bufnr)
|
|
if not adapter then
|
|
return nil
|
|
end
|
|
|
|
local line = vim.api.nvim_buf_get_lines(bufnr, lnum - 1, lnum, true)[1]
|
|
if not line then
|
|
return nil
|
|
end
|
|
local column_defs = columns.get_supported_columns(adapter)
|
|
local result = parser.parse_line(adapter, line, column_defs)
|
|
if result then
|
|
if result.entry then
|
|
local entry = util.export_entry(result.entry)
|
|
entry.parsed_name = result.data.name
|
|
return entry
|
|
else
|
|
return {
|
|
id = result.data.id,
|
|
name = result.data.name,
|
|
type = result.data._type,
|
|
parsed_name = result.data.name,
|
|
}
|
|
end
|
|
end
|
|
-- This is a NEW entry that hasn't been saved yet
|
|
local name = vim.trim(line)
|
|
local entry_type
|
|
if vim.endswith(name, '/') then
|
|
name = name:sub(1, name:len() - 1)
|
|
entry_type = 'directory'
|
|
else
|
|
entry_type = 'file'
|
|
end
|
|
if name == '' then
|
|
return nil
|
|
else
|
|
return {
|
|
name = name,
|
|
type = entry_type,
|
|
parsed_name = name,
|
|
}
|
|
end
|
|
end
|
|
|
|
---Get the entry currently under the cursor
|
|
---@return nil|canola.Entry
|
|
M.get_cursor_entry = function()
|
|
local lnum = vim.api.nvim_win_get_cursor(0)[1]
|
|
return M.get_entry_on_line(0, lnum)
|
|
end
|
|
|
|
---Discard all changes made to canola buffers
|
|
M.discard_all_changes = function()
|
|
local view = require('canola.view')
|
|
for _, bufnr in ipairs(view.get_all_buffers()) do
|
|
if vim.bo[bufnr].modified then
|
|
view.render_buffer_async(bufnr, {}, function(err)
|
|
if err then
|
|
vim.notify(
|
|
string.format(
|
|
'Error rendering canola buffer %s: %s',
|
|
vim.api.nvim_buf_get_name(bufnr),
|
|
err
|
|
),
|
|
vim.log.levels.ERROR
|
|
)
|
|
end
|
|
end)
|
|
end
|
|
end
|
|
end
|
|
|
|
---Change the display columns for canola
|
|
---@param cols canola.ColumnSpec[]
|
|
M.set_columns = function(cols)
|
|
require('canola.view').set_columns(cols)
|
|
end
|
|
|
|
---Change the sort order for canola
|
|
---@param sort canola.SortSpec[] List of columns plus direction. See :help canola-columns to see which ones are sortable.
|
|
---@example
|
|
--- require("canola").set_sort({ { "type", "asc" }, { "size", "desc" } })
|
|
M.set_sort = function(sort)
|
|
require('canola.view').set_sort(sort)
|
|
end
|
|
|
|
---Change how canola determines if the file is hidden
|
|
---@param is_hidden_file fun(filename: string, bufnr: integer, entry: canola.Entry): boolean Return true if the file/dir should be hidden
|
|
M.set_is_hidden_file = function(is_hidden_file)
|
|
require('canola.view').set_is_hidden_file(is_hidden_file)
|
|
end
|
|
|
|
---Toggle hidden files and directories
|
|
M.toggle_hidden = function()
|
|
require('canola.view').toggle_hidden()
|
|
end
|
|
|
|
---Get the current directory
|
|
---@param bufnr? integer
|
|
---@return nil|string
|
|
M.get_current_dir = function(bufnr)
|
|
local config = require('canola.config')
|
|
local fs = require('canola.fs')
|
|
local util = require('canola.util')
|
|
local buf_name = vim.api.nvim_buf_get_name(bufnr or 0)
|
|
local scheme, path = util.parse_url(buf_name)
|
|
if config.adapters[scheme] == 'files' then
|
|
assert(path)
|
|
return fs.posix_to_os_path(path)
|
|
end
|
|
end
|
|
|
|
---Get the canola url for a given directory
|
|
---@private
|
|
---@param dir nil|string When nil, use the cwd
|
|
---@param use_canola_parent nil|boolean If in an canola buffer, return the parent (default true)
|
|
---@return string The parent url
|
|
---@return nil|string The basename (if present) of the file/dir we were just in
|
|
M.get_url_for_path = function(dir, use_canola_parent)
|
|
if use_canola_parent == nil then
|
|
use_canola_parent = true
|
|
end
|
|
local config = require('canola.config')
|
|
local fs = require('canola.fs')
|
|
local util = require('canola.util')
|
|
if vim.bo.filetype == 'netrw' and not dir then
|
|
dir = vim.b.netrw_curdir
|
|
end
|
|
if dir then
|
|
local scheme = util.parse_url(dir)
|
|
if scheme then
|
|
return dir
|
|
end
|
|
local abspath = vim.fn.fnamemodify(dir, ':p')
|
|
local path = fs.os_to_posix_path(abspath)
|
|
return config.adapter_to_scheme.files .. path
|
|
else
|
|
local bufname = vim.api.nvim_buf_get_name(0)
|
|
return M.get_buffer_parent_url(bufname, use_canola_parent)
|
|
end
|
|
end
|
|
|
|
---@private
|
|
---@param bufname string
|
|
---@param use_canola_parent boolean If in an canola buffer, return the parent
|
|
---@return string
|
|
---@return nil|string
|
|
M.get_buffer_parent_url = function(bufname, use_canola_parent)
|
|
local config = require('canola.config')
|
|
local fs = require('canola.fs')
|
|
local pathutil = require('canola.pathutil')
|
|
local util = require('canola.util')
|
|
local scheme, path = util.parse_url(bufname)
|
|
if not scheme then
|
|
local parent, basename
|
|
scheme = config.adapter_to_scheme.files
|
|
if bufname == '' then
|
|
parent = fs.os_to_posix_path(vim.fn.getcwd())
|
|
else
|
|
parent = fs.os_to_posix_path(vim.fn.fnamemodify(bufname, ':p:h'))
|
|
basename = vim.fn.fnamemodify(bufname, ':t')
|
|
end
|
|
local parent_url = util.addslash(scheme .. parent)
|
|
return parent_url, basename
|
|
else
|
|
assert(path)
|
|
if scheme == 'term://' then
|
|
---@type string
|
|
path = vim.fn.expand(path:match('^(.*)//')) ---@diagnostic disable-line: assign-type-mismatch
|
|
return config.adapter_to_scheme.files .. util.addslash(path)
|
|
end
|
|
|
|
-- This is some unknown buffer scheme
|
|
if not config.adapters[scheme] then
|
|
return vim.fn.getcwd()
|
|
end
|
|
|
|
if not use_canola_parent then
|
|
return bufname
|
|
end
|
|
local adapter = assert(config.get_adapter_by_scheme(scheme))
|
|
local parent_url
|
|
if adapter and adapter.get_parent then
|
|
local adapter_scheme = config.adapter_to_scheme[adapter.name]
|
|
parent_url = adapter.get_parent(adapter_scheme .. path)
|
|
else
|
|
local parent = pathutil.parent(path)
|
|
parent_url = scheme .. util.addslash(parent)
|
|
end
|
|
if parent_url == bufname then
|
|
return parent_url
|
|
else
|
|
return util.addslash(parent_url), pathutil.basename(path)
|
|
end
|
|
end
|
|
end
|
|
|
|
---@class (exact) canola.OpenOpts
|
|
---@field preview? canola.OpenPreviewOpts When present, open the preview window after opening canola
|
|
|
|
---Open canola browser in a floating window
|
|
---@param dir? string When nil, open the parent of the current buffer, or the cwd if current buffer is not a file
|
|
---@param opts? canola.OpenOpts
|
|
---@param cb? fun() Called after the canola buffer is ready
|
|
M.open_float = function(dir, opts, cb)
|
|
opts = opts or {}
|
|
local config = require('canola.config')
|
|
local layout = require('canola.layout')
|
|
local util = require('canola.util')
|
|
local view = require('canola.view')
|
|
|
|
local parent_url, basename = M.get_url_for_path(dir)
|
|
if basename then
|
|
view.set_last_cursor(parent_url, basename)
|
|
end
|
|
|
|
local bufnr = vim.api.nvim_create_buf(false, true)
|
|
vim.bo[bufnr].bufhidden = 'wipe'
|
|
local win_opts = layout.get_fullscreen_win_opts()
|
|
|
|
local original_winid = vim.api.nvim_get_current_win()
|
|
local winid = vim.api.nvim_open_win(bufnr, true, win_opts)
|
|
vim.w[winid].is_canola_win = true
|
|
vim.w[winid].canola_original_win = original_winid
|
|
for k, v in pairs(config.float.win_options) do
|
|
vim.api.nvim_set_option_value(k, v, { scope = 'local', win = winid })
|
|
end
|
|
local autocmds = {}
|
|
table.insert(
|
|
autocmds,
|
|
vim.api.nvim_create_autocmd('WinLeave', {
|
|
desc = 'Close floating canola window',
|
|
group = 'Canola',
|
|
callback = vim.schedule_wrap(function()
|
|
if util.is_floating_win() or vim.fn.win_gettype() == 'command' then
|
|
return
|
|
end
|
|
if vim.api.nvim_win_is_valid(winid) then
|
|
vim.api.nvim_win_close(winid, true)
|
|
end
|
|
for _, id in ipairs(autocmds) do
|
|
vim.api.nvim_del_autocmd(id)
|
|
end
|
|
autocmds = {}
|
|
end),
|
|
nested = true,
|
|
})
|
|
)
|
|
|
|
table.insert(
|
|
autocmds,
|
|
vim.api.nvim_create_autocmd('BufWinEnter', {
|
|
desc = 'Reset local canola window options when buffer changes',
|
|
pattern = '*',
|
|
callback = function(params)
|
|
local winbuf = params.buf
|
|
if not vim.api.nvim_win_is_valid(winid) or vim.api.nvim_win_get_buf(winid) ~= winbuf then
|
|
return
|
|
end
|
|
for k, v in pairs(config.float.win_options) do
|
|
vim.api.nvim_set_option_value(k, v, { scope = 'local', win = winid })
|
|
end
|
|
|
|
-- Update the floating window title
|
|
if vim.fn.has('nvim-0.9') == 1 and config.float.border ~= 'none' then
|
|
local cur_win_opts = vim.api.nvim_win_get_config(winid)
|
|
vim.api.nvim_win_set_config(winid, {
|
|
relative = 'editor',
|
|
row = cur_win_opts.row,
|
|
col = cur_win_opts.col,
|
|
width = cur_win_opts.width,
|
|
height = cur_win_opts.height,
|
|
title = util.get_title(winid),
|
|
})
|
|
end
|
|
end,
|
|
})
|
|
)
|
|
|
|
vim.cmd.edit({ args = { util.escape_filename(parent_url) }, mods = { keepalt = true } })
|
|
-- :edit will set buflisted = true, but we may not want that
|
|
if config.buf_options.buflisted ~= nil then
|
|
vim.api.nvim_set_option_value('buflisted', config.buf_options.buflisted, { buf = 0 })
|
|
end
|
|
|
|
util.run_after_load(0, function()
|
|
if opts.preview then
|
|
M.open_preview(opts.preview, cb)
|
|
elseif cb then
|
|
cb()
|
|
end
|
|
end)
|
|
|
|
if vim.fn.has('nvim-0.9') == 0 then
|
|
util.add_title_to_win(winid)
|
|
end
|
|
end
|
|
|
|
---Open canola browser in a floating window, or close it if open
|
|
---@param dir nil|string When nil, open the parent of the current buffer, or the cwd if current buffer is not a file
|
|
---@param opts? canola.OpenOpts
|
|
---@param cb? fun() Called after the canola buffer is ready
|
|
M.toggle_float = function(dir, opts, cb)
|
|
if vim.w.is_canola_win then
|
|
M.close()
|
|
if cb then
|
|
cb()
|
|
end
|
|
else
|
|
M.open_float(dir, opts, cb)
|
|
end
|
|
end
|
|
|
|
---@param canola_bufnr? integer
|
|
local function update_preview_window(canola_bufnr)
|
|
canola_bufnr = canola_bufnr or 0
|
|
local util = require('canola.util')
|
|
util.run_after_load(canola_bufnr, function()
|
|
local cursor_entry = M.get_cursor_entry()
|
|
local preview_win_id = util.get_preview_win()
|
|
if
|
|
cursor_entry
|
|
and preview_win_id
|
|
and cursor_entry.id ~= vim.w[preview_win_id].canola_entry_id
|
|
then
|
|
M.open_preview()
|
|
end
|
|
end)
|
|
end
|
|
|
|
---Open canola browser for a directory
|
|
---@param dir? string When nil, open the parent of the current buffer, or the cwd if current buffer is not a file
|
|
---@param opts? canola.OpenOpts
|
|
---@param cb? fun() Called after the canola buffer is ready
|
|
M.open = function(dir, opts, cb)
|
|
opts = opts or {}
|
|
local config = require('canola.config')
|
|
local util = require('canola.util')
|
|
local view = require('canola.view')
|
|
local parent_url, basename = M.get_url_for_path(dir)
|
|
if basename then
|
|
view.set_last_cursor(parent_url, basename)
|
|
end
|
|
vim.cmd.edit({ args = { util.escape_filename(parent_url) }, mods = { keepalt = true } })
|
|
-- :edit will set buflisted = true, but we may not want that
|
|
if config.buf_options.buflisted ~= nil then
|
|
vim.api.nvim_set_option_value('buflisted', config.buf_options.buflisted, { buf = 0 })
|
|
end
|
|
|
|
util.run_after_load(0, function()
|
|
if opts.preview then
|
|
M.open_preview(opts.preview, cb)
|
|
elseif cb then
|
|
cb()
|
|
end
|
|
end)
|
|
|
|
-- If preview window exists, update its content
|
|
update_preview_window()
|
|
end
|
|
|
|
---@class canola.CloseOpts
|
|
---@field exit_if_last_buf? boolean Exit vim if this canola buffer is the last open buffer
|
|
|
|
---Restore the buffer that was present when canola was opened
|
|
---@param opts? canola.CloseOpts
|
|
M.close = function(opts)
|
|
opts = opts or {}
|
|
local mode = vim.api.nvim_get_mode().mode
|
|
if mode:match('^[vVsS\22\19]') or mode:match('^no') then
|
|
vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes('<Esc>', true, true, true), 'n', false)
|
|
return
|
|
end
|
|
-- If we're in a floating canola window, close it and try to restore focus to the original window
|
|
if vim.w.is_canola_win then
|
|
local original_winid = vim.w.canola_original_win
|
|
vim.api.nvim_win_close(0, true)
|
|
if original_winid and vim.api.nvim_win_is_valid(original_winid) then
|
|
vim.api.nvim_set_current_win(original_winid)
|
|
end
|
|
return
|
|
end
|
|
local ok, bufnr = pcall(vim.api.nvim_win_get_var, 0, 'canola_original_buffer')
|
|
if ok and vim.api.nvim_buf_is_valid(bufnr) then
|
|
vim.api.nvim_win_set_buf(0, bufnr)
|
|
if vim.w.canola_original_view then
|
|
vim.fn.winrestview(vim.w.canola_original_view)
|
|
end
|
|
return
|
|
end
|
|
|
|
-- Deleting the buffer closes all windows with that buffer open, so navigate to a different
|
|
-- buffer first
|
|
local canolabuf = vim.api.nvim_get_current_buf()
|
|
ok = pcall(vim.cmd.bprev)
|
|
-- If `bprev` failed, there are no buffers open
|
|
if not ok then
|
|
-- either exit or create a new blank buffer
|
|
if opts.exit_if_last_buf then
|
|
vim.cmd.quit()
|
|
else
|
|
vim.cmd.enew()
|
|
end
|
|
end
|
|
vim.api.nvim_buf_delete(canolabuf, { force = true })
|
|
end
|
|
|
|
---@class canola.OpenPreviewOpts
|
|
---@field vertical? boolean Open the buffer in a vertical split
|
|
---@field horizontal? boolean Open the buffer in a horizontal split
|
|
---@field split? "aboveleft"|"belowright"|"topleft"|"botright" Split modifier
|
|
|
|
---Preview the entry under the cursor in a split
|
|
---@param opts? canola.OpenPreviewOpts
|
|
---@param callback? fun(err: nil|string) Called once the preview window has been opened
|
|
M.open_preview = function(opts, callback)
|
|
opts = opts or {}
|
|
local config = require('canola.config')
|
|
local layout = require('canola.layout')
|
|
local util = require('canola.util')
|
|
|
|
local function finish(err)
|
|
if err then
|
|
vim.notify(err, vim.log.levels.ERROR)
|
|
end
|
|
if callback then
|
|
callback(err)
|
|
end
|
|
end
|
|
|
|
if not opts.horizontal and opts.vertical == nil then
|
|
opts.vertical = true
|
|
end
|
|
if not opts.split then
|
|
if opts.horizontal then
|
|
opts.split = vim.o.splitbelow and 'belowright' or 'aboveleft'
|
|
else
|
|
opts.split = vim.o.splitright and 'belowright' or 'aboveleft'
|
|
end
|
|
end
|
|
|
|
local preview_win = util.get_preview_win({ include_not_owned = true })
|
|
local prev_win = vim.api.nvim_get_current_win()
|
|
local bufnr = vim.api.nvim_get_current_buf()
|
|
|
|
local entry = M.get_cursor_entry()
|
|
if not entry then
|
|
return finish('Could not find entry under cursor')
|
|
end
|
|
local entry_title = entry.name
|
|
if entry.type == 'directory' then
|
|
entry_title = entry_title .. '/'
|
|
end
|
|
|
|
if util.is_floating_win() then
|
|
if preview_win == nil then
|
|
local root_win_opts, preview_win_opts =
|
|
layout.split_window(0, config.float.preview_split, config.float.padding)
|
|
|
|
local win_opts_canola = {
|
|
relative = 'editor',
|
|
width = root_win_opts.width,
|
|
height = root_win_opts.height,
|
|
row = root_win_opts.row,
|
|
col = root_win_opts.col,
|
|
border = config.float.border,
|
|
zindex = 45,
|
|
}
|
|
vim.api.nvim_win_set_config(0, win_opts_canola)
|
|
local win_opts = {
|
|
relative = 'editor',
|
|
width = preview_win_opts.width,
|
|
height = preview_win_opts.height,
|
|
row = preview_win_opts.row,
|
|
col = preview_win_opts.col,
|
|
border = config.float.border,
|
|
zindex = 45,
|
|
focusable = false,
|
|
noautocmd = true,
|
|
style = 'minimal',
|
|
}
|
|
|
|
if vim.fn.has('nvim-0.9') == 1 then
|
|
win_opts.title = entry_title
|
|
end
|
|
|
|
preview_win = vim.api.nvim_open_win(bufnr, true, win_opts)
|
|
vim.api.nvim_set_option_value('previewwindow', true, { scope = 'local', win = preview_win })
|
|
vim.api.nvim_win_set_var(preview_win, 'canola_preview', true)
|
|
vim.api.nvim_set_current_win(prev_win)
|
|
elseif vim.fn.has('nvim-0.9') == 1 then
|
|
vim.api.nvim_win_set_config(preview_win, { title = entry_title })
|
|
end
|
|
end
|
|
|
|
local cmd = preview_win and 'buffer' or 'sbuffer'
|
|
local mods = {
|
|
vertical = opts.vertical,
|
|
horizontal = opts.horizontal,
|
|
split = opts.split,
|
|
}
|
|
|
|
-- HACK Switching windows takes us out of visual mode.
|
|
-- Switching with nvim_set_current_win causes the previous visual selection (as used by `gv`) to
|
|
-- not get set properly. So we have to switch windows this way instead.
|
|
local hack_set_win = function(winid)
|
|
local winnr = vim.api.nvim_win_get_number(winid)
|
|
vim.cmd.wincmd({ args = { 'w' }, count = winnr })
|
|
end
|
|
|
|
util.get_edit_path(bufnr, entry, function(normalized_url)
|
|
local mc = package.loaded['multicursor-nvim']
|
|
local has_multicursors = mc and mc.hasCursors()
|
|
local is_visual_mode = util.is_visual_mode()
|
|
if preview_win then
|
|
if is_visual_mode then
|
|
hack_set_win(preview_win)
|
|
else
|
|
vim.api.nvim_set_current_win(preview_win)
|
|
end
|
|
end
|
|
|
|
local entry_is_file = not vim.endswith(normalized_url, '/')
|
|
local filebufnr
|
|
if entry_is_file then
|
|
if config.preview_win.disable_preview(normalized_url) then
|
|
filebufnr = vim.api.nvim_create_buf(false, true)
|
|
vim.bo[filebufnr].bufhidden = 'wipe'
|
|
vim.bo[filebufnr].buftype = 'nofile'
|
|
util.render_text(filebufnr, 'Preview disabled', { winid = preview_win })
|
|
elseif
|
|
config.preview_win.preview_method ~= 'load'
|
|
and not util.file_matches_bufreadcmd(normalized_url)
|
|
then
|
|
filebufnr =
|
|
util.read_file_to_scratch_buffer(normalized_url, config.preview_win.preview_method)
|
|
end
|
|
end
|
|
|
|
if not filebufnr then
|
|
filebufnr = vim.fn.bufadd(normalized_url)
|
|
if entry_is_file and vim.fn.bufloaded(filebufnr) == 0 then
|
|
vim.bo[filebufnr].bufhidden = 'wipe'
|
|
vim.b[filebufnr].canola_preview_buffer = true
|
|
end
|
|
end
|
|
|
|
---@diagnostic disable-next-line: param-type-mismatch
|
|
local ok, err = pcall(vim.cmd, {
|
|
cmd = cmd,
|
|
args = { filebufnr },
|
|
mods = mods,
|
|
})
|
|
-- Ignore swapfile errors
|
|
if not ok and err and not err:match('^Vim:E325:') then
|
|
vim.api.nvim_echo({ { err, 'Error' } }, true, {})
|
|
end
|
|
|
|
-- If we called open_preview during an autocmd, then the edit command may not trigger the
|
|
-- BufReadCmd to load the buffer. So we need to do it manually.
|
|
if util.is_canola_bufnr(filebufnr) and not vim.b[filebufnr].canola_ready then
|
|
M.load_canola_buffer(filebufnr)
|
|
end
|
|
|
|
vim.api.nvim_set_option_value('previewwindow', true, { scope = 'local', win = 0 })
|
|
vim.api.nvim_win_set_var(0, 'canola_preview', true)
|
|
for k, v in pairs(config.preview_win.win_options) do
|
|
vim.api.nvim_set_option_value(k, v, { scope = 'local', win = preview_win })
|
|
end
|
|
vim.w.canola_entry_id = entry.id
|
|
vim.w.canola_source_win = prev_win
|
|
if has_multicursors then
|
|
hack_set_win(prev_win)
|
|
mc.restoreCursors()
|
|
elseif is_visual_mode then
|
|
hack_set_win(prev_win)
|
|
-- Restore the visual selection
|
|
vim.cmd.normal({ args = { 'gv' }, bang = true })
|
|
else
|
|
vim.api.nvim_set_current_win(prev_win)
|
|
end
|
|
finish()
|
|
end)
|
|
end
|
|
|
|
---@class (exact) canola.SelectOpts
|
|
---@field vertical? boolean Open the buffer in a vertical split
|
|
---@field horizontal? boolean Open the buffer in a horizontal split
|
|
---@field split? "aboveleft"|"belowright"|"topleft"|"botright" Split modifier
|
|
---@field tab? boolean Open the buffer in a new tab
|
|
---@field close? boolean Close the original canola buffer once selection is made
|
|
---@field handle_buffer_callback? fun(buf_id: integer) If defined, all other buffer related options here would be ignored. This callback allows you to take over the process of opening the buffer yourself.
|
|
|
|
---Select the entry under the cursor
|
|
---@param opts nil|canola.SelectOpts
|
|
---@param callback nil|fun(err: nil|string) Called once all entries have been opened
|
|
M.select = function(opts, callback)
|
|
local cache = require('canola.cache')
|
|
local config = require('canola.config')
|
|
local constants = require('canola.constants')
|
|
local util = require('canola.util')
|
|
local FIELD_META = constants.FIELD_META
|
|
opts = vim.tbl_extend('keep', opts or {}, {})
|
|
|
|
local function finish(err)
|
|
if err then
|
|
vim.notify(err, vim.log.levels.ERROR)
|
|
end
|
|
if callback then
|
|
callback(err)
|
|
end
|
|
end
|
|
if not opts.split and (opts.horizontal or opts.vertical) then
|
|
if opts.horizontal then
|
|
opts.split = vim.o.splitbelow and 'belowright' or 'aboveleft'
|
|
else
|
|
opts.split = vim.o.splitright and 'belowright' or 'aboveleft'
|
|
end
|
|
end
|
|
if opts.tab and opts.split then
|
|
return finish('Cannot use split=true when tab = true')
|
|
end
|
|
local adapter = util.get_adapter(0)
|
|
if not adapter then
|
|
return finish('Not an canola buffer')
|
|
end
|
|
|
|
local visual_range = util.get_visual_range()
|
|
|
|
---@type canola.Entry[]
|
|
local entries = {}
|
|
if visual_range then
|
|
for i = visual_range.start_lnum, visual_range.end_lnum do
|
|
local entry = M.get_entry_on_line(0, i)
|
|
if entry then
|
|
table.insert(entries, entry)
|
|
end
|
|
end
|
|
else
|
|
local entry = M.get_cursor_entry()
|
|
if entry then
|
|
table.insert(entries, entry)
|
|
end
|
|
end
|
|
if vim.tbl_isempty(entries) then
|
|
return finish('Could not find entry under cursor')
|
|
end
|
|
|
|
-- Check if any of these entries are moved from their original location
|
|
local bufname = vim.api.nvim_buf_get_name(0)
|
|
local any_moved = false
|
|
for _, entry in ipairs(entries) do
|
|
-- Ignore entries with ID 0 (typically the "../" entry)
|
|
if entry.id ~= 0 then
|
|
local is_new_entry = entry.id == nil
|
|
local is_moved_from_dir = entry.id and cache.get_parent_url(entry.id) ~= bufname
|
|
local is_renamed = entry.parsed_name ~= entry.name
|
|
local internal_entry = entry.id and cache.get_entry_by_id(entry.id)
|
|
if internal_entry then
|
|
local meta = internal_entry[FIELD_META]
|
|
if meta and meta.display_name then
|
|
is_renamed = entry.parsed_name ~= meta.display_name
|
|
end
|
|
end
|
|
if is_new_entry or is_moved_from_dir or is_renamed then
|
|
any_moved = true
|
|
break
|
|
end
|
|
end
|
|
end
|
|
if any_moved and config.prompt_save_on_select_new_entry then
|
|
local ok, choice = pcall(vim.fn.confirm, 'Save changes?', 'Yes\nNo', 1)
|
|
if not ok then
|
|
return finish()
|
|
elseif choice == 1 then
|
|
M.save()
|
|
return finish()
|
|
end
|
|
end
|
|
|
|
local prev_win = vim.api.nvim_get_current_win()
|
|
local canola_bufnr = vim.api.nvim_get_current_buf()
|
|
|
|
-- Async iter over entries so we can normalize the url before opening
|
|
local i = 1
|
|
local function open_next_entry(cb)
|
|
local entry = entries[i]
|
|
i = i + 1
|
|
if not entry then
|
|
return cb()
|
|
end
|
|
if util.is_directory(entry) then
|
|
-- If this is a new directory BUT we think we already have an entry with this name, disallow
|
|
-- entry. This prevents the case of MOVE /foo -> /bar + CREATE /foo.
|
|
-- If you enter the new /foo, it will show the contents of the old /foo.
|
|
if not entry.id and cache.list_url(bufname)[entry.name] then
|
|
return cb('Please save changes before entering new directory')
|
|
end
|
|
else
|
|
-- Close floating window before opening a file
|
|
if vim.w.is_canola_win then
|
|
M.close()
|
|
end
|
|
end
|
|
|
|
-- Normalize the url before opening to prevent needing to rename them inside the BufReadCmd
|
|
-- Renaming buffers during opening can lead to missed autocmds
|
|
util.get_edit_path(canola_bufnr, entry, function(normalized_url)
|
|
local mods = {
|
|
vertical = opts.vertical,
|
|
horizontal = opts.horizontal,
|
|
split = opts.split,
|
|
keepalt = false,
|
|
}
|
|
local filebufnr = vim.fn.bufadd(normalized_url)
|
|
local entry_is_file = not vim.endswith(normalized_url, '/')
|
|
|
|
-- The :buffer command doesn't set buflisted=true
|
|
-- So do that for normal files or for canola dirs if config set buflisted=true
|
|
if entry_is_file or config.buf_options.buflisted then
|
|
vim.bo[filebufnr].buflisted = true
|
|
end
|
|
|
|
local cmd = 'buffer'
|
|
if opts.tab then
|
|
vim.cmd.tabnew({ mods = mods })
|
|
-- Make sure the new buffer from tabnew gets cleaned up
|
|
vim.bo.bufhidden = 'wipe'
|
|
elseif opts.split then
|
|
cmd = 'sbuffer'
|
|
end
|
|
if opts.handle_buffer_callback ~= nil then
|
|
opts.handle_buffer_callback(filebufnr)
|
|
else
|
|
---@diagnostic disable-next-line: param-type-mismatch
|
|
local ok, err = pcall(vim.cmd, {
|
|
cmd = cmd,
|
|
args = { filebufnr },
|
|
mods = mods,
|
|
})
|
|
-- Ignore swapfile errors
|
|
if not ok and err and not err:match('^Vim:E325:') then
|
|
vim.api.nvim_echo({ { err, 'Error' } }, true, {})
|
|
end
|
|
end
|
|
|
|
open_next_entry(cb)
|
|
end)
|
|
end
|
|
|
|
open_next_entry(function(err)
|
|
if err then
|
|
return finish(err)
|
|
end
|
|
if
|
|
opts.close
|
|
and vim.api.nvim_win_is_valid(prev_win)
|
|
and prev_win ~= vim.api.nvim_get_current_win()
|
|
then
|
|
vim.api.nvim_win_call(prev_win, function()
|
|
M.close()
|
|
end)
|
|
end
|
|
|
|
update_preview_window()
|
|
|
|
finish()
|
|
end)
|
|
end
|
|
|
|
---@param bufnr integer
|
|
---@return boolean
|
|
local function maybe_hijack_directory_buffer(bufnr)
|
|
local config = require('canola.config')
|
|
local fs = require('canola.fs')
|
|
local util = require('canola.util')
|
|
if not config.default_file_explorer then
|
|
return false
|
|
end
|
|
local bufname = vim.api.nvim_buf_get_name(bufnr)
|
|
if bufname == '' then
|
|
return false
|
|
end
|
|
if util.parse_url(bufname) or vim.fn.isdirectory(bufname) == 0 then
|
|
return false
|
|
end
|
|
local new_name = util.addslash(
|
|
config.adapter_to_scheme.files .. fs.os_to_posix_path(vim.fn.fnamemodify(bufname, ':p'))
|
|
)
|
|
local replaced = util.rename_buffer(bufnr, new_name)
|
|
return not replaced
|
|
end
|
|
|
|
---@private
|
|
M._get_highlights = function()
|
|
return {
|
|
{
|
|
name = 'CanolaEmpty',
|
|
link = 'Comment',
|
|
desc = 'Empty column values',
|
|
},
|
|
{
|
|
name = 'CanolaHidden',
|
|
link = 'Comment',
|
|
desc = 'Hidden entry in an canola buffer',
|
|
},
|
|
{
|
|
name = 'CanolaDir',
|
|
link = 'Directory',
|
|
desc = 'Directory names in an canola buffer',
|
|
},
|
|
{
|
|
name = 'CanolaDirHidden',
|
|
link = 'CanolaHidden',
|
|
desc = 'Hidden directory names in an canola buffer',
|
|
},
|
|
{
|
|
name = 'CanolaDirIcon',
|
|
link = 'CanolaDir',
|
|
desc = 'Icon for directories',
|
|
},
|
|
{
|
|
name = 'CanolaFileIcon',
|
|
link = nil,
|
|
desc = 'Icon for files',
|
|
},
|
|
{
|
|
name = 'CanolaSocket',
|
|
link = 'Keyword',
|
|
desc = 'Socket files in an canola buffer',
|
|
},
|
|
{
|
|
name = 'CanolaSocketHidden',
|
|
link = 'CanolaHidden',
|
|
desc = 'Hidden socket files in an canola buffer',
|
|
},
|
|
{
|
|
name = 'CanolaLink',
|
|
link = nil,
|
|
desc = 'Soft links in an canola buffer',
|
|
},
|
|
{
|
|
name = 'CanolaOrphanLink',
|
|
link = nil,
|
|
desc = 'Orphaned soft links in an canola buffer',
|
|
},
|
|
{
|
|
name = 'CanolaLinkHidden',
|
|
link = 'CanolaHidden',
|
|
desc = 'Hidden soft links in an canola buffer',
|
|
},
|
|
{
|
|
name = 'CanolaOrphanLinkHidden',
|
|
link = 'CanolaLinkHidden',
|
|
desc = 'Hidden orphaned soft links in an canola buffer',
|
|
},
|
|
{
|
|
name = 'CanolaLinkTarget',
|
|
link = 'Comment',
|
|
desc = 'The target of a soft link',
|
|
},
|
|
{
|
|
name = 'CanolaOrphanLinkTarget',
|
|
link = 'DiagnosticError',
|
|
desc = 'The target of an orphaned soft link',
|
|
},
|
|
{
|
|
name = 'CanolaLinkTargetHidden',
|
|
link = 'CanolaHidden',
|
|
desc = 'The target of a hidden soft link',
|
|
},
|
|
{
|
|
name = 'CanolaOrphanLinkTargetHidden',
|
|
link = 'CanolaOrphanLinkTarget',
|
|
desc = 'The target of an hidden orphaned soft link',
|
|
},
|
|
{
|
|
name = 'CanolaFile',
|
|
link = nil,
|
|
desc = 'Normal files in an canola buffer',
|
|
},
|
|
{
|
|
name = 'CanolaFileHidden',
|
|
link = 'CanolaHidden',
|
|
desc = 'Hidden normal files in an canola buffer',
|
|
},
|
|
{
|
|
name = 'CanolaExecutable',
|
|
link = 'DiagnosticOk',
|
|
desc = 'Executable files in an canola buffer',
|
|
},
|
|
{
|
|
name = 'CanolaExecutableHidden',
|
|
link = 'CanolaHidden',
|
|
desc = 'Hidden executable files in an canola buffer',
|
|
},
|
|
{
|
|
name = 'CanolaCreate',
|
|
link = 'DiagnosticInfo',
|
|
desc = 'Create action in the canola preview window',
|
|
},
|
|
{
|
|
name = 'CanolaDelete',
|
|
link = 'DiagnosticError',
|
|
desc = 'Delete action in the canola preview window',
|
|
},
|
|
{
|
|
name = 'CanolaMove',
|
|
link = 'DiagnosticWarn',
|
|
desc = 'Move action in the canola preview window',
|
|
},
|
|
{
|
|
name = 'CanolaCopy',
|
|
link = 'DiagnosticHint',
|
|
desc = 'Copy action in the canola preview window',
|
|
},
|
|
{
|
|
name = 'CanolaChange',
|
|
link = 'Special',
|
|
desc = 'Change action in the canola preview window',
|
|
},
|
|
{
|
|
name = 'CanolaRestore',
|
|
link = 'CanolaCreate',
|
|
desc = 'Restore (from the trash) action in the canola preview window',
|
|
},
|
|
{
|
|
name = 'CanolaPurge',
|
|
link = 'CanolaDelete',
|
|
desc = 'Purge (Permanently delete a file from trash) action in the canola preview window',
|
|
},
|
|
{
|
|
name = 'CanolaTrash',
|
|
link = 'CanolaDelete',
|
|
desc = 'Trash (delete a file to trash) action in the canola preview window',
|
|
},
|
|
{
|
|
name = 'CanolaTrashSourcePath',
|
|
link = 'Comment',
|
|
desc = 'Virtual text that shows the original path of file in the trash',
|
|
},
|
|
}
|
|
end
|
|
|
|
local function set_colors()
|
|
for _, conf in ipairs(M._get_highlights()) do
|
|
if conf.link then
|
|
vim.api.nvim_set_hl(0, conf.name, { default = true, link = conf.link })
|
|
end
|
|
end
|
|
-- TODO can remove this call once we drop support for Neovim 0.8. FloatTitle was introduced as a
|
|
-- built-in highlight group in 0.9, and we can start to rely on colorschemes setting it.
|
|
---@diagnostic disable-next-line: deprecated
|
|
if vim.fn.has('nvim-0.9') == 0 and not pcall(vim.api.nvim_get_hl_by_name, 'FloatTitle', true) then
|
|
---@diagnostic disable-next-line: deprecated
|
|
local border = vim.api.nvim_get_hl_by_name('FloatBorder', true)
|
|
---@diagnostic disable-next-line: deprecated
|
|
local normal = vim.api.nvim_get_hl_by_name('Normal', true)
|
|
vim.api.nvim_set_hl(
|
|
0,
|
|
'FloatTitle',
|
|
{ fg = normal.foreground, bg = border.background or normal.background }
|
|
)
|
|
end
|
|
end
|
|
|
|
---Save all changes
|
|
---@param opts nil|table
|
|
--- confirm nil|boolean Show confirmation when true, never when false, respect skip_confirm_for_simple_edits if nil
|
|
---@param cb? fun(err: nil|string) Called when mutations complete.
|
|
---@note
|
|
--- If you provide your own callback function, there will be no notification for errors.
|
|
M.save = function(opts, cb)
|
|
opts = opts or {}
|
|
if not cb then
|
|
cb = function(err)
|
|
if err and err ~= 'Canceled' then
|
|
vim.notify(err, vim.log.levels.ERROR)
|
|
end
|
|
end
|
|
end
|
|
local mutator = require('canola.mutator')
|
|
mutator.try_write_changes(opts.confirm, cb)
|
|
end
|
|
|
|
local function restore_alt_buf()
|
|
if vim.bo.filetype == 'canola' then
|
|
require('canola.view').set_win_options()
|
|
vim.api.nvim_win_set_var(0, 'canola_did_enter', true)
|
|
elseif vim.w.canola_did_enter then
|
|
vim.api.nvim_win_del_var(0, 'canola_did_enter')
|
|
-- We are entering a non-canola buffer *after* having been in an canola buffer
|
|
local has_orig, orig_buffer = pcall(vim.api.nvim_win_get_var, 0, 'canola_original_buffer')
|
|
if has_orig and vim.api.nvim_buf_is_valid(orig_buffer) then
|
|
if vim.api.nvim_get_current_buf() ~= orig_buffer then
|
|
-- If we are editing a new file after navigating around canola, set the alternate buffer
|
|
-- to be the last buffer we were in before opening canola
|
|
vim.fn.setreg('#', orig_buffer)
|
|
else
|
|
-- If we are editing the same buffer that we started canola from, set the alternate to be
|
|
-- what it was before we opened canola
|
|
local has_orig_alt, alt_buffer =
|
|
pcall(vim.api.nvim_win_get_var, 0, 'canola_original_alternate')
|
|
if has_orig_alt and vim.api.nvim_buf_is_valid(alt_buffer) then
|
|
vim.fn.setreg('#', alt_buffer)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
---@private
|
|
---@param bufnr integer
|
|
M.load_canola_buffer = function(bufnr)
|
|
local config = require('canola.config')
|
|
local keymap_util = require('canola.keymap_util')
|
|
local loading = require('canola.loading')
|
|
local util = require('canola.util')
|
|
local view = require('canola.view')
|
|
local bufname = vim.api.nvim_buf_get_name(bufnr)
|
|
local scheme, path = util.parse_url(bufname)
|
|
if config.adapter_aliases[scheme] then
|
|
scheme = config.adapter_aliases[scheme]
|
|
bufname = scheme .. path
|
|
util.rename_buffer(bufnr, bufname)
|
|
end
|
|
|
|
-- Early return if we're already loading or have already loaded this buffer
|
|
if loading.is_loading(bufnr) or vim.b[bufnr].filetype ~= nil then
|
|
return
|
|
end
|
|
|
|
local adapter = assert(config.get_adapter_by_scheme(scheme))
|
|
|
|
if vim.endswith(bufname, '/') then
|
|
-- This is a small quality-of-life thing. If the buffer name ends with a `/`, we know it's a
|
|
-- directory, and can set the filetype early. This is helpful for adapters with a lot of latency
|
|
-- (e.g. ssh) because it will set up the filetype keybinds at the *beginning* of the loading
|
|
-- process.
|
|
vim.bo[bufnr].filetype = 'canola'
|
|
vim.bo[bufnr].buftype = 'acwrite'
|
|
keymap_util.set_keymaps(config.keymaps, bufnr)
|
|
end
|
|
loading.set_loading(bufnr, true)
|
|
local winid = vim.api.nvim_get_current_win()
|
|
local function finish(new_url)
|
|
-- If the buffer was deleted while we were normalizing the name, early return
|
|
if not vim.api.nvim_buf_is_valid(bufnr) then
|
|
return
|
|
end
|
|
-- Since this was async, we may have left the window with this buffer. People often write
|
|
-- BufReadPre/Post autocmds with the expectation that the current window is the one that
|
|
-- contains the buffer. Let's then do our best to make sure that that assumption isn't violated.
|
|
winid = util.buf_get_win(bufnr, winid) or vim.api.nvim_get_current_win()
|
|
vim.api.nvim_win_call(winid, function()
|
|
if new_url ~= bufname then
|
|
if util.rename_buffer(bufnr, new_url) then
|
|
-- If the buffer was replaced then don't initialize it. It's dead. The replacement will
|
|
-- have BufReadCmd called for it
|
|
return
|
|
end
|
|
|
|
-- If the renamed buffer doesn't have a scheme anymore, this is a normal file.
|
|
-- Finish setting it up as a normal buffer.
|
|
local new_scheme = util.parse_url(new_url)
|
|
if not new_scheme then
|
|
loading.set_loading(bufnr, false)
|
|
vim.cmd.doautocmd({ args = { 'BufReadPre', new_url }, mods = { emsg_silent = true } })
|
|
vim.cmd.doautocmd({ args = { 'BufReadPost', new_url }, mods = { emsg_silent = true } })
|
|
return
|
|
end
|
|
|
|
bufname = new_url
|
|
end
|
|
if vim.endswith(bufname, '/') then
|
|
vim.cmd.doautocmd({ args = { 'BufReadPre', bufname }, mods = { emsg_silent = true } })
|
|
view.initialize(bufnr)
|
|
vim.cmd.doautocmd({ args = { 'BufReadPost', bufname }, mods = { emsg_silent = true } })
|
|
else
|
|
vim.bo[bufnr].buftype = 'acwrite'
|
|
adapter.read_file(bufnr)
|
|
end
|
|
restore_alt_buf()
|
|
end)
|
|
end
|
|
|
|
adapter.normalize_url(bufname, finish)
|
|
end
|
|
|
|
local function close_preview_window_if_not_in_canola()
|
|
local util = require('canola.util')
|
|
local preview_win_id = util.get_preview_win()
|
|
if not preview_win_id or not vim.w[preview_win_id].canola_entry_id then
|
|
return
|
|
end
|
|
|
|
local canola_source_win = vim.w[preview_win_id].canola_source_win
|
|
if canola_source_win and vim.api.nvim_win_is_valid(canola_source_win) then
|
|
local src_buf = vim.api.nvim_win_get_buf(canola_source_win)
|
|
if util.is_canola_bufnr(src_buf) then
|
|
return
|
|
end
|
|
end
|
|
|
|
-- This can fail if it's the last window open
|
|
pcall(vim.api.nvim_win_close, preview_win_id, true)
|
|
end
|
|
|
|
local _on_key_ns = 0
|
|
---Initialize canola
|
|
---@param opts canola.setupOpts|nil
|
|
M.setup = function(opts)
|
|
local Ringbuf = require('canola.ringbuf')
|
|
local config = require('canola.config')
|
|
|
|
config.setup(opts)
|
|
set_colors()
|
|
local callback = function(args)
|
|
local util = require('canola.util')
|
|
if args.smods.tab > 0 then
|
|
vim.cmd.tabnew()
|
|
end
|
|
local float = false
|
|
local trash = false
|
|
local preview = false
|
|
local i = 1
|
|
while i <= #args.fargs do
|
|
local v = args.fargs[i]
|
|
if v == '--float' then
|
|
float = true
|
|
table.remove(args.fargs, i)
|
|
elseif v == '--trash' then
|
|
trash = true
|
|
table.remove(args.fargs, i)
|
|
elseif v == '--preview' then
|
|
-- In the future we may want to support specifying options for the preview window (e.g.
|
|
-- vertical/horizontal), but if you want that level of control maybe just use the API
|
|
preview = true
|
|
table.remove(args.fargs, i)
|
|
elseif v == '--progress' then
|
|
local mutator = require('canola.mutator')
|
|
if mutator.is_mutating() then
|
|
mutator.show_progress()
|
|
else
|
|
vim.notify('No mutation in progress', vim.log.levels.WARN)
|
|
end
|
|
return
|
|
else
|
|
i = i + 1
|
|
end
|
|
end
|
|
|
|
if not float and (args.smods.vertical or args.smods.horizontal or args.smods.split ~= '') then
|
|
local range = args.count > 0 and { args.count } or nil
|
|
local cmdargs = { mods = { split = args.smods.split }, range = range }
|
|
if args.smods.vertical then
|
|
vim.cmd.vsplit(cmdargs)
|
|
else
|
|
vim.cmd.split(cmdargs)
|
|
end
|
|
end
|
|
|
|
local method = float and 'open_float' or 'open'
|
|
local path = args.fargs[1]
|
|
local open_opts = {}
|
|
if trash then
|
|
local url = M.get_url_for_path(path, false)
|
|
local _, new_path = util.parse_url(url)
|
|
path = 'canola-trash://' .. new_path
|
|
end
|
|
if preview then
|
|
open_opts.preview = {}
|
|
end
|
|
M[method](path, open_opts)
|
|
end
|
|
vim.api.nvim_create_user_command('Canola', callback, {
|
|
desc = 'Open canola file browser on a directory',
|
|
nargs = '*',
|
|
complete = 'dir',
|
|
count = true,
|
|
})
|
|
local aug = vim.api.nvim_create_augroup('Canola', {})
|
|
|
|
if config.default_file_explorer then
|
|
vim.g.loaded_netrw = 1
|
|
vim.g.loaded_netrwPlugin = 1
|
|
-- If netrw was already loaded, clear this augroup
|
|
if vim.fn.exists('#FileExplorer') then
|
|
vim.api.nvim_create_augroup('FileExplorer', { clear = true })
|
|
end
|
|
end
|
|
|
|
local patterns = {}
|
|
local filetype_patterns = {}
|
|
for scheme in pairs(config.adapters) do
|
|
table.insert(patterns, scheme .. '*')
|
|
filetype_patterns[scheme .. '.*'] = { 'canola', { priority = 10 } }
|
|
end
|
|
for scheme in pairs(config.adapter_aliases) do
|
|
table.insert(patterns, scheme .. '*')
|
|
filetype_patterns[scheme .. '.*'] = { 'canola', { priority = 10 } }
|
|
end
|
|
local scheme_pattern = table.concat(patterns, ',')
|
|
-- We need to add these patterns to the filetype matcher so the filetype doesn't get overridden
|
|
-- by other patterns. See https://github.com/stevearc/canola.nvim/issues/47
|
|
vim.filetype.add({
|
|
pattern = filetype_patterns,
|
|
})
|
|
|
|
local keybuf = Ringbuf.new(7)
|
|
if _on_key_ns == 0 then
|
|
_on_key_ns = vim.on_key(function(char)
|
|
keybuf:push(char)
|
|
end, _on_key_ns)
|
|
end
|
|
vim.api.nvim_create_autocmd('ColorScheme', {
|
|
desc = 'Set default canola highlights',
|
|
group = aug,
|
|
pattern = '*',
|
|
callback = set_colors,
|
|
})
|
|
vim.api.nvim_create_autocmd('BufReadCmd', {
|
|
group = aug,
|
|
pattern = scheme_pattern,
|
|
nested = true,
|
|
callback = function(params)
|
|
M.load_canola_buffer(params.buf)
|
|
end,
|
|
})
|
|
vim.api.nvim_create_autocmd('BufWriteCmd', {
|
|
group = aug,
|
|
pattern = scheme_pattern,
|
|
nested = true,
|
|
callback = function(params)
|
|
local last_keys = keybuf:as_str()
|
|
local winid = vim.api.nvim_get_current_win()
|
|
-- If the user issued a :wq or similar, we should quit after saving
|
|
local quit_after_save = vim.endswith(last_keys, ':wq\r')
|
|
or vim.endswith(last_keys, ':x\r')
|
|
or vim.endswith(last_keys, 'ZZ')
|
|
local quit_all = vim.endswith(last_keys, ':wqa\r')
|
|
or vim.endswith(last_keys, ':wqal\r')
|
|
or vim.endswith(last_keys, ':wqall\r')
|
|
local bufname = vim.api.nvim_buf_get_name(params.buf)
|
|
if vim.endswith(bufname, '/') then
|
|
vim.cmd.doautocmd({ args = { 'BufWritePre', params.file }, mods = { silent = true } })
|
|
M.save(nil, function(err)
|
|
if err then
|
|
if err ~= 'Canceled' then
|
|
vim.notify(err, vim.log.levels.ERROR)
|
|
end
|
|
elseif winid == vim.api.nvim_get_current_win() then
|
|
if quit_after_save then
|
|
vim.cmd.quit()
|
|
elseif quit_all then
|
|
vim.cmd.quitall()
|
|
end
|
|
end
|
|
end)
|
|
vim.cmd.doautocmd({ args = { 'BufWritePost', params.file }, mods = { silent = true } })
|
|
else
|
|
local adapter = assert(config.get_adapter_by_scheme(bufname))
|
|
adapter.write_file(params.buf)
|
|
end
|
|
end,
|
|
})
|
|
vim.api.nvim_create_autocmd('BufLeave', {
|
|
desc = 'Save alternate buffer for later',
|
|
group = aug,
|
|
pattern = '*',
|
|
callback = function()
|
|
local util = require('canola.util')
|
|
if not util.is_canola_bufnr(0) then
|
|
vim.w.canola_original_buffer = vim.api.nvim_get_current_buf()
|
|
vim.w.canola_original_view = vim.fn.winsaveview()
|
|
---@diagnostic disable-next-line: param-type-mismatch
|
|
vim.w.canola_original_alternate = vim.fn.bufnr('#')
|
|
end
|
|
end,
|
|
})
|
|
vim.api.nvim_create_autocmd('BufEnter', {
|
|
desc = 'Set/unset canola window options and restore alternate buffer',
|
|
group = aug,
|
|
pattern = '*',
|
|
callback = function()
|
|
local util = require('canola.util')
|
|
local bufname = vim.api.nvim_buf_get_name(0)
|
|
local scheme = util.parse_url(bufname)
|
|
local is_canola_buf = scheme and config.adapters[scheme]
|
|
-- We want to filter out canola buffers that are not directories (i.e. ssh files)
|
|
local is_canola_dir_or_unknown = (vim.bo.filetype == 'canola' or vim.bo.filetype == '')
|
|
if is_canola_buf and is_canola_dir_or_unknown then
|
|
local view = require('canola.view')
|
|
view.maybe_set_cursor()
|
|
-- While we are in an canola buffer, set the alternate file to the buffer we were in prior to
|
|
-- opening canola
|
|
local has_orig, orig_buffer = pcall(vim.api.nvim_win_get_var, 0, 'canola_original_buffer')
|
|
if has_orig and vim.api.nvim_buf_is_valid(orig_buffer) then
|
|
vim.fn.setreg('#', orig_buffer)
|
|
end
|
|
view.set_win_options()
|
|
vim.w.canola_did_enter = true
|
|
elseif vim.fn.isdirectory(bufname) == 0 then
|
|
-- Only run this logic if we are *not* in an canola buffer (and it's not a directory, which
|
|
-- will be replaced by a canola:// url)
|
|
-- Canola buffers have to run it in BufReadCmd after confirming they are a directory or a file
|
|
restore_alt_buf()
|
|
end
|
|
|
|
close_preview_window_if_not_in_canola()
|
|
end,
|
|
})
|
|
|
|
vim.api.nvim_create_autocmd({ 'BufWinEnter', 'WinNew', 'WinEnter' }, {
|
|
desc = 'Reset bufhidden when entering a preview buffer',
|
|
group = aug,
|
|
pattern = '*',
|
|
callback = function()
|
|
-- If we have entered a "preview" buffer in a non-preview window, reset bufhidden
|
|
if vim.b.canola_preview_buffer and not vim.wo.previewwindow then
|
|
vim.bo.bufhidden = vim.api.nvim_get_option_value('bufhidden', { scope = 'global' })
|
|
vim.b.canola_preview_buffer = nil
|
|
end
|
|
end,
|
|
})
|
|
if not config.silence_scp_warning then
|
|
vim.api.nvim_create_autocmd('BufNew', {
|
|
desc = 'Warn about scp:// usage',
|
|
group = aug,
|
|
pattern = 'scp://*',
|
|
once = true,
|
|
callback = function()
|
|
vim.notify(
|
|
'If you are trying to browse using Canola, use canola-ssh:// instead of scp://\nSet `silence_scp_warning = true` in canola.setup() to disable this message.\nSee https://github.com/stevearc/canola.nvim/issues/27 for more information.',
|
|
vim.log.levels.WARN
|
|
)
|
|
end,
|
|
})
|
|
end
|
|
vim.api.nvim_create_autocmd('WinNew', {
|
|
desc = 'Restore window options when splitting an canola window',
|
|
group = aug,
|
|
pattern = '*',
|
|
nested = true,
|
|
callback = function(params)
|
|
local util = require('canola.util')
|
|
if not util.is_canola_bufnr(params.buf) or vim.w.canola_did_enter then
|
|
return
|
|
end
|
|
-- This new window is a split off of an canola window. We need to transfer the window
|
|
-- variables. First, locate the parent window
|
|
local parent_win
|
|
-- First search windows in this tab, then search all windows
|
|
local winids = vim.list_extend(vim.api.nvim_tabpage_list_wins(0), vim.api.nvim_list_wins())
|
|
for _, winid in ipairs(winids) do
|
|
if vim.api.nvim_win_is_valid(winid) then
|
|
if vim.w[winid].canola_did_enter then
|
|
parent_win = winid
|
|
break
|
|
end
|
|
end
|
|
end
|
|
if not parent_win then
|
|
vim.notify(
|
|
'Canola split could not find parent window. Please try to replicate whatever you just did and report a bug on github',
|
|
vim.log.levels.WARN
|
|
)
|
|
return
|
|
end
|
|
|
|
-- Then transfer over the relevant window vars
|
|
vim.w.canola_did_enter = true
|
|
vim.w.canola_original_buffer = vim.w[parent_win].canola_original_buffer
|
|
vim.w.canola_original_view = vim.w[parent_win].canola_original_view
|
|
vim.w.canola_original_alternate = vim.w[parent_win].canola_original_alternate
|
|
end,
|
|
})
|
|
-- mksession doesn't save canola buffers in a useful way. We have to manually load them after a
|
|
-- session finishes loading. See https://github.com/stevearc/canola.nvim/issues/29
|
|
vim.api.nvim_create_autocmd('SessionLoadPost', {
|
|
desc = 'Load canola buffers after a session is loaded',
|
|
group = aug,
|
|
pattern = '*',
|
|
callback = function(params)
|
|
if vim.g.SessionLoad ~= 1 then
|
|
return
|
|
end
|
|
local util = require('canola.util')
|
|
local scheme = util.parse_url(params.file)
|
|
if config.adapters[scheme] and vim.api.nvim_buf_line_count(params.buf) == 1 then
|
|
M.load_canola_buffer(params.buf)
|
|
end
|
|
end,
|
|
})
|
|
|
|
if config.default_file_explorer then
|
|
vim.api.nvim_create_autocmd('BufAdd', {
|
|
desc = 'Detect directory buffer and open canola file browser',
|
|
group = aug,
|
|
pattern = '*',
|
|
nested = true,
|
|
callback = function(params)
|
|
maybe_hijack_directory_buffer(params.buf)
|
|
end,
|
|
})
|
|
|
|
for _, bufnr in ipairs(vim.api.nvim_list_bufs()) do
|
|
if maybe_hijack_directory_buffer(bufnr) and vim.v.vim_did_enter == 1 then
|
|
M.load_canola_buffer(bufnr)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
return M
|