columns.lua

Branches > main > 72087d4 > files / nvim / lua / oil / columns.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)
      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 icon, hl = icon_provider(field_type, name, conf)
      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