Some checks are pending
quality / changes (push) Waiting to run
quality / Lua Format Check (push) Blocked by required conditions
quality / Lua Lint Check (push) Blocked by required conditions
quality / Lua Type Check (push) Blocked by required conditions
quality / Markdown Format Check (push) Blocked by required conditions
test / Test (Neovim nightly) (push) Waiting to run
test / Test (Neovim stable) (push) Waiting to run
* 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.
* refactor: remove `vim.g.oil` declarative config
Problem: the `vim.g.oil` configuration path was added prematurely.
It adds a second config entrypoint before the plugin has stabilized
enough to justify it.
Solution: remove `vim.g.oil` support from `plugin/oil.lua`,
`config.setup()`, docs, and tests. Users configure via
`require("oil").setup({})`.
287 lines
7.8 KiB
Lua
287 lines
7.8 KiB
Lua
local config = require('oil.config')
|
|
local constants = require('oil.constants')
|
|
local util = require('oil.util')
|
|
local M = {}
|
|
|
|
local FIELD_NAME = constants.FIELD_NAME
|
|
local FIELD_TYPE = constants.FIELD_TYPE
|
|
local FIELD_META = constants.FIELD_META
|
|
|
|
local all_columns = {}
|
|
|
|
---@alias oil.ColumnSpec string|{[1]: string, [string]: any}
|
|
|
|
---@class (exact) oil.ColumnDefinition
|
|
---@field render fun(entry: oil.InternalEntry, conf: nil|table, bufnr: integer): nil|oil.TextChunk
|
|
---@field parse fun(line: string, conf: nil|table): nil|string, nil|string
|
|
---@field compare? fun(entry: oil.InternalEntry, parsed_value: any): boolean
|
|
---@field render_action? fun(action: oil.ChangeAction): string
|
|
---@field perform_action? fun(action: oil.ChangeAction, callback: fun(err: nil|string))
|
|
---@field get_sort_value? fun(entry: oil.InternalEntry): number|string
|
|
---@field create_sort_value_factory? fun(num_entries: integer): fun(entry: oil.InternalEntry): number|string
|
|
|
|
---@param name string
|
|
---@param column oil.ColumnDefinition
|
|
M.register = function(name, column)
|
|
all_columns[name] = column
|
|
end
|
|
|
|
---@param adapter oil.Adapter
|
|
---@param defn oil.ColumnSpec
|
|
---@return nil|oil.ColumnDefinition
|
|
M.get_column = function(adapter, defn)
|
|
local name = util.split_config(defn)
|
|
return all_columns[name] or adapter.get_column(name)
|
|
end
|
|
|
|
---@param adapter_or_scheme string|oil.Adapter
|
|
---@return oil.ColumnSpec[]
|
|
M.get_supported_columns = function(adapter_or_scheme)
|
|
local adapter
|
|
if type(adapter_or_scheme) == 'string' then
|
|
adapter = config.get_adapter_by_scheme(adapter_or_scheme)
|
|
else
|
|
adapter = adapter_or_scheme
|
|
end
|
|
assert(adapter)
|
|
local ret = {}
|
|
for _, def in ipairs(config.columns) do
|
|
if M.get_column(adapter, def) then
|
|
table.insert(ret, def)
|
|
end
|
|
end
|
|
return ret
|
|
end
|
|
|
|
local EMPTY = { '-', 'OilEmpty' }
|
|
|
|
M.EMPTY = EMPTY
|
|
|
|
---@param adapter oil.Adapter
|
|
---@param col_def oil.ColumnSpec
|
|
---@param entry oil.InternalEntry
|
|
---@param bufnr integer
|
|
---@return oil.TextChunk
|
|
M.render_col = function(adapter, col_def, entry, bufnr)
|
|
local name, conf = util.split_config(col_def)
|
|
local column = M.get_column(adapter, name)
|
|
if not column then
|
|
-- This shouldn't be possible because supports_col should return false
|
|
return EMPTY
|
|
end
|
|
|
|
local chunk = column.render(entry, conf, bufnr)
|
|
if type(chunk) == 'table' then
|
|
if chunk[1]:match('^%s*$') then
|
|
return EMPTY
|
|
end
|
|
else
|
|
if not chunk or chunk:match('^%s*$') then
|
|
return EMPTY
|
|
end
|
|
if conf and conf.highlight then
|
|
local highlight = conf.highlight
|
|
if type(highlight) == 'function' then
|
|
highlight = conf.highlight(chunk)
|
|
end
|
|
return { chunk, highlight }
|
|
end
|
|
end
|
|
return chunk
|
|
end
|
|
|
|
---@param adapter oil.Adapter
|
|
---@param line string
|
|
---@param col_def oil.ColumnSpec
|
|
---@return nil|string
|
|
---@return nil|string
|
|
M.parse_col = function(adapter, line, col_def)
|
|
local name, conf = util.split_config(col_def)
|
|
-- If rendering failed, there will just be a "-"
|
|
local empty_col, rem = line:match('^%s*(-%s+)(.*)$')
|
|
if empty_col then
|
|
return nil, rem
|
|
end
|
|
local column = M.get_column(adapter, name)
|
|
if column then
|
|
return column.parse(line:gsub('^%s+', ''), conf)
|
|
end
|
|
end
|
|
|
|
---@param adapter oil.Adapter
|
|
---@param col_name string
|
|
---@param entry oil.InternalEntry
|
|
---@param parsed_value any
|
|
---@return boolean
|
|
M.compare = function(adapter, col_name, entry, parsed_value)
|
|
local column = M.get_column(adapter, col_name)
|
|
if column and column.compare then
|
|
return column.compare(entry, parsed_value)
|
|
else
|
|
return false
|
|
end
|
|
end
|
|
|
|
---@param adapter oil.Adapter
|
|
---@param action oil.ChangeAction
|
|
---@return string
|
|
M.render_change_action = function(adapter, action)
|
|
local column = M.get_column(adapter, action.column)
|
|
if not column then
|
|
error(string.format('Received change action for nonexistant column %s', action.column))
|
|
end
|
|
if column.render_action then
|
|
return column.render_action(action)
|
|
else
|
|
return string.format('CHANGE %s %s = %s', action.url, action.column, action.value)
|
|
end
|
|
end
|
|
|
|
---@param adapter oil.Adapter
|
|
---@param action oil.ChangeAction
|
|
---@param callback fun(err: nil|string)
|
|
M.perform_change_action = function(adapter, action, callback)
|
|
local column = M.get_column(adapter, action.column)
|
|
if not column then
|
|
return callback(
|
|
string.format('Received change action for nonexistant column %s', action.column)
|
|
)
|
|
end
|
|
column.perform_action(action, callback)
|
|
end
|
|
|
|
local icon_provider = util.get_icon_provider()
|
|
if icon_provider then
|
|
M.register('icon', {
|
|
render = function(entry, conf, bufnr)
|
|
local field_type = entry[FIELD_TYPE]
|
|
local name = entry[FIELD_NAME]
|
|
local meta = entry[FIELD_META]
|
|
if field_type == 'link' and meta then
|
|
if meta.link then
|
|
name = meta.link
|
|
end
|
|
if meta.link_stat then
|
|
field_type = meta.link_stat.type
|
|
end
|
|
end
|
|
if meta and meta.display_name then
|
|
name = meta.display_name
|
|
end
|
|
|
|
local ft = nil
|
|
if conf and conf.use_slow_filetype_detection and field_type == 'file' then
|
|
local bufname = vim.api.nvim_buf_get_name(bufnr)
|
|
local _, path = util.parse_url(bufname)
|
|
if path then
|
|
local lines = vim.fn.readfile(path .. name, '', 16)
|
|
if lines and #lines > 0 then
|
|
ft = vim.filetype.match({ filename = name, contents = lines })
|
|
end
|
|
end
|
|
end
|
|
|
|
local icon, hl = icon_provider(field_type, name, conf, ft)
|
|
if not conf or conf.add_padding ~= false then
|
|
icon = icon .. ' '
|
|
end
|
|
if conf and conf.highlight then
|
|
if type(conf.highlight) == 'function' then
|
|
hl = conf.highlight(icon)
|
|
else
|
|
hl = conf.highlight
|
|
end
|
|
end
|
|
return { icon, hl }
|
|
end,
|
|
|
|
parse = function(line, conf)
|
|
return line:match('^(%S+)%s+(.*)$')
|
|
end,
|
|
})
|
|
end
|
|
|
|
local default_type_icons = {
|
|
directory = 'dir',
|
|
socket = 'sock',
|
|
}
|
|
---@param entry oil.InternalEntry
|
|
---@return boolean
|
|
local function is_entry_directory(entry)
|
|
local type = entry[FIELD_TYPE]
|
|
if type == 'directory' then
|
|
return true
|
|
elseif type == 'link' then
|
|
local meta = entry[FIELD_META]
|
|
return (meta and meta.link_stat and meta.link_stat.type == 'directory') == true
|
|
else
|
|
return false
|
|
end
|
|
end
|
|
M.register('type', {
|
|
render = function(entry, conf)
|
|
local entry_type = entry[FIELD_TYPE]
|
|
if conf and conf.icons then
|
|
return conf.icons[entry_type] or entry_type
|
|
else
|
|
return default_type_icons[entry_type] or entry_type
|
|
end
|
|
end,
|
|
|
|
parse = function(line, conf)
|
|
return line:match('^(%S+)%s+(.*)$')
|
|
end,
|
|
|
|
get_sort_value = function(entry)
|
|
if is_entry_directory(entry) then
|
|
return 1
|
|
else
|
|
return 2
|
|
end
|
|
end,
|
|
})
|
|
|
|
local function adjust_number(int)
|
|
return string.format('%03d%s', #int, int)
|
|
end
|
|
|
|
M.register('name', {
|
|
render = function(entry, conf)
|
|
error('Do not use the name column. It is for sorting only')
|
|
end,
|
|
|
|
parse = function(line, conf)
|
|
error('Do not use the name column. It is for sorting only')
|
|
end,
|
|
|
|
create_sort_value_factory = function(num_entries)
|
|
if
|
|
config.view_options.natural_order == false
|
|
or (config.view_options.natural_order == 'fast' and num_entries > 5000)
|
|
then
|
|
if config.view_options.case_insensitive then
|
|
return function(entry)
|
|
return entry[FIELD_NAME]:lower()
|
|
end
|
|
else
|
|
return function(entry)
|
|
return entry[FIELD_NAME]
|
|
end
|
|
end
|
|
else
|
|
local memo = {}
|
|
return function(entry)
|
|
if memo[entry] == nil then
|
|
local name = entry[FIELD_NAME]:gsub('0*(%d+)', adjust_number)
|
|
if config.view_options.case_insensitive then
|
|
name = name:lower()
|
|
end
|
|
memo[entry] = name
|
|
end
|
|
return memo[entry]
|
|
end
|
|
end
|
|
end,
|
|
})
|
|
|
|
return M
|