nvim-tree.lua/lua/nvim-tree/explorer/sorters.lua
Alexander Courtis 38aac09151
refactor(#2871, #2886): multi instance: node classes created (#2916)
* refactor(#2875): multi instance renderer

* refactor(#2875): multi instance renderer

* refactor(#2875): multi instance renderer

* refactor(#2875): multi instance renderer

* node classes and constructors

* node methods

* refactor(#2875): multi instance renderer

* node classes and constructors

* explorer is a directory node

* extract methods from explore_node

* extract methods from explore_node

* extract methods from explore_node

* extract methods from lib

* use .. name for root node for compatibility

* use node.explorer

* extract node factory, remove unused code

* factories for all nodes, add RootNode

* factories for all nodes, add RootNode

* use factory pattern for decorators

* note regression and commit

* fix dir git status regression

* destroy nodes, not explorer

* add BaseNode:is

* revert changes to create-file, handle in #2924

* extract methods from explorer

* extract methods from explorer

* extract methods from explorer

* use Node everywhere in luadoc

* extract methods from lib

* extract methods from lib

* lint

* remove unused code

* don't call methods on fake root node

* get_node_at_cursor returns explorer (root) node instead of { name = '..' }

* remove unused inject_node

* refactor(#2875): multi instance renderer

* refactor(#2875): multi instance renderer

* refactor(#2875): multi instance renderer

* extract methods from lib

* node factory uses stat only

* temporary DirectoryNode casting until method extraction into child classes

* lua-language-server 3.10.5 -> 3.11.0

* explicitly call Explorer constructor

* normalise explorer RootNode new call, tidy annotations
2024-10-07 13:46:56 +11:00

317 lines
7.1 KiB
Lua

local C = {}
---@class Sorter
local Sorter = {}
function Sorter:new(opts)
local o = {}
setmetatable(o, self)
self.__index = self
o.config = vim.deepcopy(opts.sort)
if type(o.config.sorter) == "function" then
o.user = o.config.sorter
end
return o
end
--- Predefined comparator, defaulting to name
---@param sorter string as per options
---@return function
function Sorter:get_comparator(sorter)
return function(a, b)
return (C[sorter] or C.name)(a, b, self.config)
end
end
---Create a shallow copy of a portion of a list.
---@param t table
---@param first integer First index, inclusive
---@param last integer Last index, inclusive
---@return table
local function tbl_slice(t, first, last)
local slice = {}
for i = first, last or #t, 1 do
table.insert(slice, t[i])
end
return slice
end
---Evaluate `sort.folders_first` and `sort.files_first`
---@param a Node
---@param b Node
---@return boolean|nil
local function folders_or_files_first(a, b, cfg)
if not (cfg.folders_first or cfg.files_first) then
return
end
if not a.nodes and b.nodes then
-- file <> folder
return cfg.files_first
elseif a.nodes and not b.nodes then
-- folder <> file
return not cfg.files_first
end
end
---@param t table
---@param first number
---@param mid number
---@param last number
---@param comparator fun(a: Node, b: Node): boolean
local function merge(t, first, mid, last, comparator)
local n1 = mid - first + 1
local n2 = last - mid
local ls = tbl_slice(t, first, mid)
local rs = tbl_slice(t, mid + 1, last)
local i = 1
local j = 1
local k = first
while i <= n1 and j <= n2 do
if comparator(ls[i], rs[j]) then
t[k] = ls[i]
i = i + 1
else
t[k] = rs[j]
j = j + 1
end
k = k + 1
end
while i <= n1 do
t[k] = ls[i]
i = i + 1
k = k + 1
end
while j <= n2 do
t[k] = rs[j]
j = j + 1
k = k + 1
end
end
---@param t table
---@param first number
---@param last number
---@param comparator fun(a: Node, b: Node): boolean
local function split_merge(t, first, last, comparator)
if (last - first) < 1 then
return
end
local mid = math.floor((first + last) / 2)
split_merge(t, first, mid, comparator)
split_merge(t, mid + 1, last, comparator)
merge(t, first, mid, last, comparator)
end
---Perform a merge sort using sorter option.
---@param t Node[]
function Sorter:sort(t)
if self.user then
local t_user = {}
local origin_index = {}
for _, n in ipairs(t) do
table.insert(t_user, {
absolute_path = n.absolute_path,
executable = n.executable,
extension = n.extension,
filetype = vim.filetype.match({ filename = n.name }),
link_to = n.link_to,
name = n.name,
type = n.type,
})
table.insert(origin_index, n)
end
local predefined = self.user(t_user)
if predefined then
split_merge(t, 1, #t, self:get_comparator(predefined))
return
end
-- do merge sort for prevent memory exceed
local user_index = {}
for i, v in ipairs(t_user) do
if type(v.absolute_path) == "string" and user_index[v.absolute_path] == nil then
user_index[v.absolute_path] = i
end
end
-- if missing value found, then using origin_index
local mini_comparator = function(a, b)
local a_index = user_index[a.absolute_path] or origin_index[a.absolute_path]
local b_index = user_index[b.absolute_path] or origin_index[b.absolute_path]
if type(a_index) == "number" and type(b_index) == "number" then
return a_index <= b_index
end
return (a_index or 0) <= (b_index or 0)
end
split_merge(t, 1, #t, mini_comparator) -- sort by user order
else
split_merge(t, 1, #t, self:get_comparator(self.config.sorter))
end
end
---@param a Node
---@param b Node
---@param ignorecase boolean|nil
---@return boolean
local function node_comparator_name_ignorecase_or_not(a, b, ignorecase, cfg)
if not (a and b) then
return true
end
local early_return = folders_or_files_first(a, b, cfg)
if early_return ~= nil then
return early_return
end
if ignorecase then
return a.name:lower() <= b.name:lower()
else
return a.name <= b.name
end
end
function C.case_sensitive(a, b, cfg)
return node_comparator_name_ignorecase_or_not(a, b, false, cfg)
end
function C.name(a, b, cfg)
return node_comparator_name_ignorecase_or_not(a, b, true, cfg)
end
function C.modification_time(a, b, cfg)
if not (a and b) then
return true
end
local early_return = folders_or_files_first(a, b, cfg)
if early_return ~= nil then
return early_return
end
local last_modified_a = 0
local last_modified_b = 0
if a.fs_stat ~= nil then
last_modified_a = a.fs_stat.mtime.sec
end
if b.fs_stat ~= nil then
last_modified_b = b.fs_stat.mtime.sec
end
return last_modified_b <= last_modified_a
end
function C.suffix(a, b, cfg)
if not (a and b) then
return true
end
-- directories go first
local early_return = folders_or_files_first(a, b, cfg)
if early_return ~= nil then
return early_return
elseif a.nodes and b.nodes then
return C.name(a, b, cfg)
end
-- dotfiles go second
if a.name:sub(1, 1) == "." and b.name:sub(1, 1) ~= "." then
return true
elseif a.name:sub(1, 1) ~= "." and b.name:sub(1, 1) == "." then
return false
elseif a.name:sub(1, 1) == "." and b.name:sub(1, 1) == "." then
return C.name(a, b, cfg)
end
-- unsuffixed go third
local a_suffix_ndx = a.name:find("%.%w+$")
local b_suffix_ndx = b.name:find("%.%w+$")
if not a_suffix_ndx and b_suffix_ndx then
return true
elseif a_suffix_ndx and not b_suffix_ndx then
return false
elseif not (a_suffix_ndx and b_suffix_ndx) then
return C.name(a, b, cfg)
end
-- finally, compare by suffixes
local a_suffix = a.name:sub(a_suffix_ndx)
local b_suffix = b.name:sub(b_suffix_ndx)
if a_suffix and not b_suffix then
return true
elseif not a_suffix and b_suffix then
return false
elseif a_suffix:lower() == b_suffix:lower() then
return C.name(a, b, cfg)
end
return a_suffix:lower() < b_suffix:lower()
end
function C.extension(a, b, cfg)
if not (a and b) then
return true
end
local early_return = folders_or_files_first(a, b, cfg)
if early_return ~= nil then
return early_return
end
if a.extension and not b.extension then
return true
elseif not a.extension and b.extension then
return false
end
local a_ext = (a.extension or ""):lower()
local b_ext = (b.extension or ""):lower()
if a_ext == b_ext then
return C.name(a, b, cfg)
end
return a_ext < b_ext
end
function C.filetype(a, b, cfg)
local a_ft = vim.filetype.match({ filename = a.name })
local b_ft = vim.filetype.match({ filename = b.name })
-- directories first
local early_return = folders_or_files_first(a, b, cfg)
if early_return ~= nil then
return early_return
end
-- one is nil, the other wins
if a_ft and not b_ft then
return true
elseif not a_ft and b_ft then
return false
end
-- same filetype or both nil, sort by name
if a_ft == b_ft then
return C.name(a, b, cfg)
end
return a_ft < b_ft
end
return Sorter