feat: api to sort directory contents (#169)

This commit is contained in:
Steven Arcangeli 2023-09-08 18:55:45 -07:00
parent ca2560cae8
commit 879d280617
11 changed files with 256 additions and 54 deletions

View file

@ -179,6 +179,7 @@ require("oil").setup({
["_"] = "actions.open_cwd",
["`"] = "actions.cd",
["~"] = "actions.tcd",
["gs"] = "actions.change_sort",
["g."] = "actions.toggle_hidden",
},
-- Set to false to disable all of the above keymaps
@ -194,6 +195,12 @@ require("oil").setup({
is_always_hidden = function(name, bufnr)
return false
end,
sort = {
-- sort order can be "asc" or "desc"
-- see :help oil-columns to see which columns are sortable
{ "type", "asc" },
{ "name", "asc" },
},
},
-- Configuration for the floating window in oil.open_float
float = {
@ -277,6 +284,7 @@ Note that at the moment the ssh adapter does not support Windows machines, and i
- [get_cursor_entry()](doc/api.md#get_cursor_entry)
- [discard_all_changes()](doc/api.md#discard_all_changes)
- [set_columns(cols)](doc/api.md#set_columnscols)
- [set_sort(sort)](doc/api.md#set_sortsort)
- [set_is_hidden_file(is_hidden_file)](doc/api.md#set_is_hidden_fileis_hidden_file)
- [toggle_hidden()](doc/api.md#toggle_hidden)
- [get_current_dir()](doc/api.md#get_current_dir)

View file

@ -6,6 +6,7 @@
- [get_cursor_entry()](#get_cursor_entry)
- [discard_all_changes()](#discard_all_changes)
- [set_columns(cols)](#set_columnscols)
- [set_sort(sort)](#set_sortsort)
- [set_is_hidden_file(is_hidden_file)](#set_is_hidden_fileis_hidden_file)
- [toggle_hidden()](#toggle_hidden)
- [get_current_dir()](#get_current_dir)
@ -52,6 +53,15 @@ Change the display columns for oil
| ----- | ------------------ | ---- |
| cols | `oil.ColumnSpec[]` | |
## set_sort(sort)
`set_sort(sort)` \
Change the sort order for oil
| Param | Type | Desc |
| ----- | ---------- | ---- |
| sort | `string[]` | [] |
## set_is_hidden_file(is_hidden_file)
`set_is_hidden_file(is_hidden_file)` \

View file

@ -12,7 +12,7 @@ CONTENTS *oil-content
--------------------------------------------------------------------------------
OPTIONS *oil-options*
>
>lua
require("oil").setup({
-- Oil will take over directory buffers (e.g. `vim .` or `:e src/`)
-- Set to false if you still want to use netrw.
@ -70,6 +70,7 @@ OPTIONS *oil-option
["_"] = "actions.open_cwd",
["`"] = "actions.cd",
["~"] = "actions.tcd",
["gs"] = "actions.change_sort",
["g."] = "actions.toggle_hidden",
},
-- Set to false to disable all of the above keymaps
@ -85,6 +86,12 @@ OPTIONS *oil-option
is_always_hidden = function(name, bufnr)
return false
end,
sort = {
-- sort order can be "asc" or "desc"
-- see :help oil-columns to see which columns are sortable
{ "type", "asc" },
{ "name", "asc" },
},
},
-- Configuration for the floating window in oil.open_float
float = {
@ -166,6 +173,12 @@ set_columns({cols}) *oil.set_column
Parameters:
{cols} `oil.ColumnSpec[]`
set_sort({sort}) *oil.set_sort*
Change the sort order for oil
Parameters:
{sort} `string[]` []
set_is_hidden_file({is_hidden_file}) *oil.set_is_hidden_file*
Change how oil determines if the file is hidden
@ -244,6 +257,7 @@ or as a table to pass parameters (e.g. `{"size", highlight = "Special"}`)
type *column-type*
Adapters: *
Sortable: this column can be used in view_props.sort
The type of the entry (file, directory, link, etc)
Parameters:
@ -266,6 +280,7 @@ icon *column-ico
size *column-size*
Adapters: files, ssh
Sortable: this column can be used in view_props.sort
The size of the file
Parameters:
@ -283,6 +298,7 @@ permissions *column-permission
ctime *column-ctime*
Adapters: files
Sortable: this column can be used in view_props.sort
Change timestamp of the file
Parameters:
@ -292,6 +308,7 @@ ctime *column-ctim
mtime *column-mtime*
Adapters: files
Sortable: this column can be used in view_props.sort
Last modified time of the file
Parameters:
@ -301,6 +318,7 @@ mtime *column-mtim
atime *column-atime*
Adapters: files
Sortable: this column can be used in view_props.sort
Last access time of the file
Parameters:
@ -310,6 +328,7 @@ atime *column-atim
birthtime *column-birthtime*
Adapters: files
Sortable: this column can be used in view_props.sort
The time the file was created
Parameters:
@ -325,6 +344,9 @@ These are actions that can be used in the `keymaps` section of config options.
cd *actions.cd*
:cd to the current oil directory
change_sort *actions.change_sort*
Change the sort order
close *actions.close*
Close oil and restore original buffer

View file

@ -237,6 +237,32 @@ M.open_cmdline_dir = {
end,
}
M.change_sort = {
desc = "Change the sort order",
callback = function()
local sort_cols = { "name", "size", "atime", "mtime", "ctime", "birthtime" }
vim.ui.select(sort_cols, { prompt = "Sort by", kind = "oil_sort_col" }, function(col)
if not col then
return
end
vim.ui.select(
{ "ascending", "descending" },
{ prompt = "Sort order", kind = "oil_sort_order" },
function(order)
if not order then
return
end
order = order == "ascending" and "asc" or "desc"
oil.set_sort({
{ "type", "asc" },
{ col, order },
})
end
)
end)
end,
}
---List actions for documentation generation
---@private
M._get_actions = function()

View file

@ -13,22 +13,20 @@ local FIELD_NAME = constants.FIELD_NAME
local FIELD_META = constants.FIELD_META
local function read_link_data(path, cb)
uv.fs_readlink(
path,
vim.schedule_wrap(function(link_err, link)
if link_err then
cb(link_err)
else
local stat_path = link
if not fs.is_absolute(link) then
stat_path = fs.join(vim.fn.fnamemodify(path, ":h"), link)
end
uv.fs_stat(stat_path, function(stat_err, stat)
cb(nil, link, stat)
end)
uv.fs_readlink(path, function(link_err, link)
if link_err then
cb(link_err)
else
assert(link)
local stat_path = link
if not fs.is_absolute(link) then
stat_path = fs.join(vim.fn.fnamemodify(path, ":h"), link)
end
end)
)
uv.fs_stat(stat_path, function(stat_err, stat)
cb(nil, link, stat)
end)
end
end)
end
---@param path string
@ -60,7 +58,7 @@ file_columns.size = {
local meta = entry[FIELD_META]
local stat = meta.stat
if not stat then
return ""
return columns.EMPTY
end
if stat.size >= 1e9 then
return string.format("%.1fG", stat.size / 1e9)
@ -73,6 +71,16 @@ file_columns.size = {
end
end,
get_sort_value = function(entry)
local meta = entry[FIELD_META]
local stat = meta.stat
if stat then
return stat.size
else
return 0
end
end,
parse = function(line, conf)
return line:match("^(%d+%S*)%s+(.*)$")
end,
@ -87,7 +95,7 @@ if not fs.is_windows then
local meta = entry[FIELD_META]
local stat = meta.stat
if not stat then
return ""
return columns.EMPTY
end
return permissions.mode_to_str(stat.mode)
end,
@ -145,6 +153,9 @@ for _, time_key in ipairs({ "ctime", "mtime", "atime", "birthtime" }) do
render = function(entry, conf)
local meta = entry[FIELD_META]
local stat = meta.stat
if not stat then
return columns.EMPTY
end
local fmt = conf and conf.format
local ret
if fmt then
@ -170,6 +181,16 @@ for _, time_key in ipairs({ "ctime", "mtime", "atime", "birthtime" }) do
end
return line:match("^(" .. pattern .. ")%s+(.+)$")
end,
get_sort_value = function(entry)
local meta = entry[FIELD_META]
local stat = meta.stat
if stat then
return stat[time_key].sec
else
return 0
end
end,
}
end

View file

@ -157,6 +157,15 @@ ssh_columns.size = {
parse = function(line, conf)
return line:match("^(%d+%S*)%s+(.*)$")
end,
get_sort_value = function(entry)
local meta = entry[FIELD_META]
if meta.size then
return meta.size
else
return 0
end
end,
}
---@param name string

View file

@ -15,10 +15,11 @@ local all_columns = {}
---@class (exact) oil.ColumnDefinition
---@field render fun(entry: oil.InternalEntry, conf: nil|table): nil|oil.TextChunk
---@field parse fun(line: string, conf: nil|table): nil|string, nil|string
---@field meta_fields nil|table<string, fun(parent_url: string, entry: oil.InternalEntry, cb: fun(err: nil|string))>
---@field meta_fields? table<string, fun(parent_url: string, entry: oil.InternalEntry, cb: fun(err: 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
---@param name string
---@param column oil.ColumnDefinition
@ -29,7 +30,7 @@ end
---@param adapter oil.Adapter
---@param defn oil.ColumnSpec
---@return nil|oil.ColumnDefinition
local function get_column(adapter, defn)
M.get_column = function(adapter, defn)
local name = util.split_config(defn)
return all_columns[name] or adapter.get_column(name)
end
@ -46,7 +47,7 @@ M.get_supported_columns = function(adapter_or_scheme)
assert(adapter)
local ret = {}
for _, def in ipairs(config.columns) do
if get_column(adapter, def) then
if M.get_column(adapter, def) then
table.insert(ret, def)
end
end
@ -61,7 +62,7 @@ M.get_metadata_fetcher = function(adapter, column_defs)
local num_keys = 0
for _, def in ipairs(column_defs) do
local name = util.split_config(def)
local column = get_column(adapter, name)
local column = M.get_column(adapter, name)
if column and column.meta_fields then
for k, v in pairs(column.meta_fields) do
if not keyfetches[k] then
@ -95,13 +96,15 @@ end
local EMPTY = { "-", "Comment" }
M.EMPTY = EMPTY
---@param adapter oil.Adapter
---@param col_def oil.ColumnSpec
---@param entry oil.InternalEntry
---@return oil.TextChunk
M.render_col = function(adapter, col_def, entry)
local name, conf = util.split_config(col_def)
local column = get_column(adapter, name)
local column = M.get_column(adapter, name)
if not column then
-- This shouldn't be possible because supports_col should return false
return EMPTY
@ -150,7 +153,7 @@ M.parse_col = function(adapter, line, col_def)
if vim.startswith(line, "- ") then
return nil, line:sub(3)
end
local column = get_column(adapter, name)
local column = M.get_column(adapter, name)
if column then
return column.parse(line, conf)
end
@ -162,7 +165,7 @@ end
---@param parsed_value any
---@return boolean
M.compare = function(adapter, col_name, entry, parsed_value)
local column = get_column(adapter, col_name)
local column = M.get_column(adapter, col_name)
if column and column.compare then
return column.compare(entry, parsed_value)
else
@ -174,7 +177,7 @@ end
---@param action oil.ChangeAction
---@return string
M.render_change_action = function(adapter, action)
local column = get_column(adapter, action.column)
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
@ -189,7 +192,7 @@ end
---@param action oil.ChangeAction
---@param callback fun(err: nil|string)
M.perform_change_action = function(adapter, action, callback)
local column = get_column(adapter, action.column)
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)
@ -236,6 +239,19 @@ 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"
else
return false
end
end
M.register("type", {
render = function(entry, conf)
local entry_type = entry[FIELD_TYPE]
@ -249,6 +265,28 @@ M.register("type", {
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,
})
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,
get_sort_value = function(entry)
return entry[FIELD_NAME]
end,
})
return M

View file

@ -55,6 +55,7 @@ local default_config = {
["_"] = "actions.open_cwd",
["`"] = "actions.cd",
["~"] = "actions.tcd",
["gs"] = "actions.change_sort",
["g."] = "actions.toggle_hidden",
},
-- Set to false to disable all of the above keymaps
@ -70,6 +71,12 @@ local default_config = {
is_always_hidden = function(name, bufnr)
return false
end,
sort = {
-- sort order can be "asc" or "desc"
-- see :help oil-columns to see which columns are sortable
{ "type", "asc" },
{ "name", "asc" },
},
},
-- Configuration for the floating window in oil.open_float
float = {

View file

@ -142,6 +142,12 @@ M.set_columns = function(cols)
require("oil.view").set_columns(cols)
end
---Change the sort order for oil
---@param sort string[][]
M.set_sort = function(sort)
require("oil.view").set_sort(sort)
end
---Change how oil determines if the file is hidden
---@param is_hidden_file fun(filename: string, bufnr: nil|integer): boolean Return true if the file/dir should be hidden
M.set_is_hidden_file = function(is_hidden_file)

View file

@ -100,6 +100,17 @@ M.set_columns = function(cols)
end
end
M.set_sort = function(new_sort)
local any_modified = are_any_modified()
if any_modified then
vim.notify("Cannot change sorting when you have unsaved changes", vim.log.levels.WARN)
else
config.view_options.sort = new_sort
-- TODO only refetch if we don't have all the necessary data for the columns
M.rerender_all_oil_buffers({ refetch = true })
end
end
-- List of bufnrs
local session = {}
@ -351,17 +362,46 @@ M.initialize = function(bufnr)
keymap_util.set_keymaps("", config.keymaps, bufnr)
end
---@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"
else
return false
---@param adapter oil.Adapter
---@return fun(a: oil.InternalEntry, b: oil.InternalEntry): boolean
local function get_sort_function(adapter)
local idx_funs = {}
for _, sort_pair in ipairs(config.view_options.sort) do
local col_name, order = unpack(sort_pair)
if order ~= "asc" and order ~= "desc" then
vim.notify_once(
string.format(
"Column '%s' has invalid sort order '%s'. Should be either 'asc' or 'desc'",
col_name,
order
),
vim.log.levels.WARN
)
end
local col = columns.get_column(adapter, col_name)
if col and col.get_sort_value then
table.insert(idx_funs, { col.get_sort_value, order })
else
vim.notify_once(
string.format("Column '%s' does not support sorting", col_name),
vim.log.levels.WARN
)
end
end
return function(a, b)
for _, sort_fn in ipairs(idx_funs) do
local get_sort_value, order = unpack(sort_fn)
local a_val = get_sort_value(a)
local b_val = get_sort_value(b)
if a_val ~= b_val then
if order == "desc" then
return a_val > b_val
else
return a_val < b_val
end
end
end
return a[FIELD_NAME] < b[FIELD_NAME]
end
end
@ -390,14 +430,7 @@ local function render_buffer(bufnr, opts)
local entries = cache.list_url(bufname)
local entry_list = vim.tbl_values(entries)
table.sort(entry_list, function(a, b)
local a_isdir = is_entry_directory(a)
local b_isdir = is_entry_directory(b)
if a_isdir ~= b_isdir then
return a_isdir
end
return a[FIELD_NAME] < b[FIELD_NAME]
end)
table.sort(entry_list, get_sort_function(adapter))
local jump_idx
if opts.jump_first then
@ -512,6 +545,21 @@ M.format_entry_cols = function(entry, column_defs, col_width, adapter)
return cols
end
---Get the column names that are used for view and sort
---@return string[]
local function get_used_columns()
local cols = {}
for _, def in ipairs(config.columns) do
local name = util.split_config(def)
table.insert(cols, name)
end
for _, sort_pair in ipairs(config.view_options.sort) do
local name = sort_pair[1]
table.insert(cols, name)
end
return cols
end
---@param bufnr integer
---@param opts nil|table
--- preserve_undo nil|boolean
@ -579,7 +627,7 @@ M.render_buffer_async = function(bufnr, opts, callback)
end
cache.begin_update_url(bufname)
adapter.list(bufname, config.columns, function(err, entries, fetch_more)
adapter.list(bufname, get_used_columns(), function(err, entries, fetch_more)
loading.set_loading(bufnr, false)
if err then
cache.end_update_url(bufname)

View file

@ -88,6 +88,7 @@ class ColumnDef:
name: str
adapters: str
editable: bool
sortable: bool
summary: str
params: List["LuaParam"] = field(default_factory=list)
@ -107,6 +108,7 @@ COL_DEFS = [
"type",
"*",
False,
True,
"The type of the entry (file, directory, link, etc)",
HL
+ [LuaParam("icons", "table<string, string>", "Mapping of entry type to icon")],
@ -115,6 +117,7 @@ COL_DEFS = [
"icon",
"*",
False,
False,
"An icon for the entry's type (requires nvim-web-devicons)",
HL
+ [
@ -123,17 +126,17 @@ COL_DEFS = [
LuaParam("add_padding", "boolean", "Set to false to remove the extra whitespace after the icon"),
],
),
ColumnDef("size", "files, ssh", False, "The size of the file", HL + []),
ColumnDef("size", "files, ssh", False, True, "The size of the file", HL + []),
ColumnDef(
"permissions", "files, ssh", True, "Access permissions of the file", HL + []
"permissions", "files, ssh", True, False, "Access permissions of the file", HL + []
),
ColumnDef("ctime", "files", False, "Change timestamp of the file", HL + TIME + []),
ColumnDef("ctime", "files", False, True, "Change timestamp of the file", HL + TIME + []),
ColumnDef(
"mtime", "files", False, "Last modified time of the file", HL + TIME + []
"mtime", "files", False, True, "Last modified time of the file", HL + TIME + []
),
ColumnDef("atime", "files", False, "Last access time of the file", HL + TIME + []),
ColumnDef("atime", "files", False, True, "Last access time of the file", HL + TIME + []),
ColumnDef(
"birthtime", "files", False, "The time the file was created", HL + TIME + []
"birthtime", "files", False, True, "The time the file was created", HL + TIME + []
),
]
@ -142,7 +145,7 @@ def get_options_vimdoc() -> "VimdocSection":
section = VimdocSection("options", "oil-options")
config_file = os.path.join(ROOT, "lua", "oil", "config.lua")
opt_lines = read_section(config_file, r"^local default_config =", r"^}$")
lines = ["\n", ">\n", ' require("oil").setup({\n']
lines = ["\n", ">lua\n", ' require("oil").setup({\n']
lines.extend(indent(opt_lines, 4))
lines.extend([" })\n", "<\n"])
section.body = lines
@ -193,6 +196,10 @@ def get_columns_vimdoc() -> "VimdocSection":
for col in COL_DEFS:
section.body.append(leftright(col.name, f"*column-{col.name}*"))
section.body.extend(wrap(f"Adapters: {col.adapters}", 4))
if col.sortable:
section.body.extend(
wrap(f"Sortable: this column can be used in view_props.sort", 4)
)
if col.editable:
section.body.extend(wrap(f"Editable: this column is read/write", 4))
section.body.extend(wrap(col.summary, 4))