mirror of
https://github.com/SpaceVim/SpaceVim.git
synced 2025-02-04 06:00:04 +08:00
433 lines
16 KiB
Lua
433 lines
16 KiB
Lua
local vim = vim
|
|
local utils = require("neo-tree.utils")
|
|
local log = require("neo-tree.log")
|
|
local manager = require("neo-tree.sources.manager")
|
|
|
|
local M = {}
|
|
|
|
---calc_click_id_from_source:
|
|
-- Calculates click_id that stores information of the source and window id
|
|
-- DANGER: Do not change this function unless you know what you are doing
|
|
---@param winid integer: window id of the window source_selector is placed
|
|
---@param source_index integer: index of the source
|
|
---@return integer
|
|
local calc_click_id_from_source = function(winid, source_index)
|
|
local base_number = #require("neo-tree").config.source_selector.sources + 1
|
|
return base_number * winid + source_index
|
|
end
|
|
|
|
---calc_source_from_click_id:
|
|
-- Calculates source index and window id from click_id. Paired with `M.calc_click_id_from_source`
|
|
-- DANGER: Do not change this function unless you know what you are doing
|
|
---@param click_id integer: click_id
|
|
---@return integer, integer
|
|
local calc_source_from_click_id = function(click_id)
|
|
local base_number = #require("neo-tree").config.source_selector.sources + 1
|
|
return math.floor(click_id / base_number), click_id % base_number
|
|
end
|
|
---sep_tbl:
|
|
-- Returns table expression of separator.
|
|
-- Converts to table expression if sep is string.
|
|
---@param sep string | table:
|
|
---@return table: `{ left = .., right = .., override = .. }`
|
|
local sep_tbl = function(sep)
|
|
if type(sep) == "nil" then
|
|
return {}
|
|
elseif type(sep) ~= "table" then
|
|
return { left = sep, right = sep, override = "active" }
|
|
end
|
|
return sep
|
|
end
|
|
|
|
-- Function below provided by @akinsho
|
|
-- https://github.com/nvim-neo-tree/neo-tree.nvim/pull/427#discussion_r924947766
|
|
|
|
-- truncate a string based on number of display columns/cells it occupies
|
|
-- so that multibyte characters are not broken up mid-character
|
|
---@param str string
|
|
---@param col_limit number
|
|
---@return string
|
|
local function truncate_by_cell(str, col_limit)
|
|
local api = vim.api
|
|
local fn = vim.fn
|
|
if str and str:len() == api.nvim_strwidth(str) then
|
|
return fn.strcharpart(str, 0, col_limit)
|
|
end
|
|
local short = fn.strcharpart(str, 0, col_limit)
|
|
if api.nvim_strwidth(short) > col_limit then
|
|
while api.nvim_strwidth(short) > col_limit do
|
|
short = fn.strcharpart(short, 0, fn.strchars(short) - 1)
|
|
end
|
|
end
|
|
return short
|
|
end
|
|
|
|
---get_separators
|
|
-- Returns information about separator on each tab.
|
|
---@param source_index integer: index of source
|
|
---@param active_index integer: index of active source. used to check if source is active and when `override = "active"`
|
|
---@param force_ignore_left boolean: overwrites calculated results with "" if set to true
|
|
---@param force_ignore_right boolean: overwrites calculated results with "" if set to true
|
|
---@return table: something like `{ left = "|", right = "|" }`
|
|
local get_separators = function(source_index, active_index, force_ignore_left, force_ignore_right)
|
|
local config = require("neo-tree").config
|
|
local is_active = source_index == active_index
|
|
local sep = sep_tbl(config.source_selector.separator)
|
|
if is_active then
|
|
sep = vim.tbl_deep_extend("force", sep, sep_tbl(config.source_selector.separator_active))
|
|
end
|
|
local show_left = sep.override == "left"
|
|
or (sep.override == "active" and source_index <= active_index)
|
|
or sep.override == nil
|
|
local show_right = sep.override == "right"
|
|
or (sep.override == "active" and source_index >= active_index)
|
|
or sep.override == nil
|
|
return {
|
|
left = (show_left and not force_ignore_left) and sep.left or "",
|
|
right = (show_right and not force_ignore_right) and sep.right or "",
|
|
}
|
|
end
|
|
|
|
---get_selector_tab_info:
|
|
-- Returns information to create a tab
|
|
---@param source_name string: name of source. should be same as names in `config.sources`
|
|
---@param source_index integer: index of source_name
|
|
---@param is_active boolean: whether this source is currently focused
|
|
---@param separator table: `{ left = .., right = .. }`: output from `get_separators()`
|
|
---@return table (see code): Note: `length`: length of whole tab (including seps), `text_length`: length of tab excluding seps
|
|
local get_selector_tab_info = function(source_name, source_index, is_active, separator)
|
|
local config = require("neo-tree").config
|
|
local separator_config = utils.resolve_config_option(config, "source_selector", nil)
|
|
if separator_config == nil then
|
|
log.warn("Cannot find source_selector config. `get_selector` abort.")
|
|
return {}
|
|
end
|
|
local source_config = config[source_name] or {}
|
|
local get_strlen = vim.api.nvim_strwidth
|
|
local text = separator_config.sources[source_index].display_name or source_config.display_name or source_name
|
|
local text_length = get_strlen(text)
|
|
if separator_config.tabs_min_width ~= nil and text_length < separator_config.tabs_min_width then
|
|
text = M.text_layout(text, separator_config.content_layout, separator_config.tabs_min_width)
|
|
text_length = separator_config.tabs_min_width
|
|
end
|
|
if separator_config.tabs_max_width ~= nil and text_length > separator_config.tabs_max_width then
|
|
text = M.text_layout(text, separator_config.content_layout, separator_config.tabs_max_width)
|
|
text_length = separator_config.tabs_max_width
|
|
end
|
|
local tab_hl = is_active and separator_config.highlight_tab_active
|
|
or separator_config.highlight_tab
|
|
local sep_hl = is_active and separator_config.highlight_separator_active
|
|
or separator_config.highlight_separator
|
|
return {
|
|
index = source_index,
|
|
is_active = is_active,
|
|
left = separator.left,
|
|
right = separator.right,
|
|
text = text,
|
|
tab_hl = tab_hl,
|
|
sep_hl = sep_hl,
|
|
length = text_length + get_strlen(separator.left) + get_strlen(separator.right),
|
|
text_length = text_length,
|
|
}
|
|
end
|
|
|
|
---text_with_hl:
|
|
-- Returns text with highlight syntax for winbar / statusline
|
|
---@param text string: text to highlight
|
|
---@param tab_hl string | nil: if nil, does nothing
|
|
---@return string: e.g. "%#HiName#text"
|
|
local text_with_hl = function(text, tab_hl)
|
|
if tab_hl == nil then
|
|
return text
|
|
end
|
|
return string.format("%%#%s#%s", tab_hl, text)
|
|
end
|
|
|
|
---add_padding:
|
|
-- Use for creating padding with highlight
|
|
---@param padding_legth number: number of padding. if float, value is rounded with `math.floor`
|
|
---@param padchar string | nil: if nil, " " (space) is used
|
|
---@return string
|
|
local add_padding = function(padding_legth, padchar)
|
|
if padchar == nil then
|
|
padchar = " "
|
|
end
|
|
return string.rep(padchar, math.floor(padding_legth))
|
|
end
|
|
|
|
---text_layout:
|
|
-- Add padding to fill `output_width`.
|
|
-- If `output_width` is less than `text_length`, text is truncated to fit `output_width`.
|
|
---@param text string:
|
|
---@param content_layout string: `"start", "center", "end"`: see `config.source_selector.tabs_layout` for more details
|
|
---@param output_width integer: exact `strdisplaywidth` of the output string
|
|
---@param trunc_char string | nil: Character used to indicate truncation. If nil, "…" (ellipsis) is used.
|
|
---@return string
|
|
local text_layout = function(text, content_layout, output_width, trunc_char)
|
|
if output_width < 1 then
|
|
return ""
|
|
end
|
|
local text_length = vim.fn.strdisplaywidth(text)
|
|
local pad_length = output_width - text_length
|
|
local left_pad, right_pad = 0, 0
|
|
if pad_length < 0 then
|
|
if output_width < 4 then
|
|
return truncate_by_cell(text, output_width)
|
|
else
|
|
return truncate_by_cell(text, output_width - 1) .. trunc_char
|
|
end
|
|
elseif content_layout == "start" then
|
|
left_pad, right_pad = 0, pad_length
|
|
elseif content_layout == "end" then
|
|
left_pad, right_pad = pad_length, 0
|
|
elseif content_layout == "center" then
|
|
left_pad, right_pad = pad_length / 2, math.ceil(pad_length / 2)
|
|
end
|
|
return add_padding(left_pad) .. text .. add_padding(right_pad)
|
|
end
|
|
|
|
---render_tab:
|
|
-- Renders string to express one tab for winbar / statusline.
|
|
---@param left_sep string: left separator
|
|
---@param right_sep string: right separator
|
|
---@param sep_hl string: highlight of separators
|
|
---@param text string: text, mostly name of source in this case
|
|
---@param tab_hl string: highlight of text
|
|
---@param click_id integer: id passed to `___neotree_selector_click`, should be calculated with `M.calc_click_id_from_source`
|
|
---@return string: complete string to render one tab
|
|
local render_tab = function(left_sep, right_sep, sep_hl, text, tab_hl, click_id)
|
|
local res = "%" .. click_id .. "@v:lua.___neotree_selector_click@"
|
|
if left_sep ~= nil then
|
|
res = res .. text_with_hl(left_sep, sep_hl)
|
|
end
|
|
res = res .. text_with_hl(text, tab_hl)
|
|
if right_sep ~= nil then
|
|
res = res .. text_with_hl(right_sep, sep_hl)
|
|
end
|
|
return res
|
|
end
|
|
|
|
M.get_scrolled_off_node_text = function(state)
|
|
if state == nil then
|
|
state = require("neo-tree.sources.manager").get_state_for_window()
|
|
if state == nil then
|
|
return
|
|
end
|
|
end
|
|
local win_top_line = vim.fn.line("w0")
|
|
if win_top_line == nil or win_top_line == 1 then
|
|
return
|
|
end
|
|
local node = state.tree:get_node(win_top_line)
|
|
return " " .. vim.fn.fnamemodify(node.path, ":~:h")
|
|
end
|
|
|
|
M.get = function()
|
|
local state = require("neo-tree.sources.manager").get_state_for_window()
|
|
if state == nil then
|
|
return
|
|
else
|
|
local config = require("neo-tree").config
|
|
local scrolled_off =
|
|
utils.resolve_config_option(config, "source_selector.show_scrolled_off_parent_node", false)
|
|
if scrolled_off then
|
|
local node_text = M.get_scrolled_off_node_text(state)
|
|
if node_text ~= nil then
|
|
return node_text
|
|
end
|
|
end
|
|
return M.get_selector(state, vim.api.nvim_win_get_width(0))
|
|
end
|
|
end
|
|
|
|
---get_selector:
|
|
-- Does everything to generate the string for source_selector in winbar / statusline.
|
|
---@param state table:
|
|
---@param width integer: width of the entire window where the source_selector is displayed
|
|
---@return string | nil
|
|
M.get_selector = function(state, width)
|
|
local config = require("neo-tree").config
|
|
if config == nil then
|
|
log.warn("Cannot find config. `get_selector` abort.")
|
|
return nil
|
|
end
|
|
local winid = state.winid or vim.api.nvim_get_current_win()
|
|
|
|
-- load padding from config
|
|
local padding = config.source_selector.padding
|
|
if type(padding) == "number" then
|
|
padding = { left = padding, right = padding }
|
|
end
|
|
width = math.floor(width - padding.left - padding.right)
|
|
|
|
-- generate information of each tab (look `get_selector_tab_info` for type hint)
|
|
local tabs = {}
|
|
local sources = config.source_selector.sources
|
|
local active_index = #sources
|
|
local length_sum, length_active, length_separators = 0, 0, 0
|
|
for i, source_info in ipairs(sources) do
|
|
local is_active = source_info.source == state.name
|
|
if is_active then
|
|
active_index = i
|
|
end
|
|
local separator = get_separators(
|
|
i,
|
|
active_index,
|
|
config.source_selector.show_separator_on_edge == false and i == 1,
|
|
config.source_selector.show_separator_on_edge == false and i == #sources
|
|
)
|
|
local element = get_selector_tab_info(source_info.source, i, is_active, separator)
|
|
length_sum = length_sum + element.length
|
|
length_separators = length_separators + element.length - element.text_length
|
|
if is_active then
|
|
length_active = element.length
|
|
end
|
|
table.insert(tabs, element)
|
|
end
|
|
|
|
-- start creating string to display
|
|
local tabs_layout = config.source_selector.tabs_layout
|
|
local content_layout = config.source_selector.content_layout or "center"
|
|
local hl_background = config.source_selector.highlight_background
|
|
local trunc_char = config.source_selector.truncation_character or "…"
|
|
local remaining_width = width - length_separators
|
|
local return_string = text_with_hl(add_padding(padding.left), hl_background)
|
|
if width < length_sum and config.source_selector.text_trunc_to_fit then -- not enough width
|
|
local each_width = math.floor(remaining_width / #tabs)
|
|
local remaining = remaining_width % each_width
|
|
tabs_layout = "start"
|
|
length_sum = width
|
|
for _, tab in ipairs(tabs) do
|
|
tab.text = text_layout( -- truncate text and pass it to "start"
|
|
tab.text,
|
|
"center",
|
|
each_width + (tab.is_active and remaining or 0),
|
|
trunc_char
|
|
)
|
|
end
|
|
end
|
|
if tabs_layout == "active" then
|
|
local active_tab_length = width - length_sum + length_active
|
|
for _, tab in ipairs(tabs) do
|
|
return_string = return_string
|
|
.. render_tab(
|
|
tab.left,
|
|
tab.right,
|
|
tab.sep_hl,
|
|
text_layout(
|
|
tab.text,
|
|
tab.is_active and content_layout or nil,
|
|
active_tab_length,
|
|
trunc_char
|
|
),
|
|
tab.tab_hl,
|
|
calc_click_id_from_source(winid, tab.index)
|
|
)
|
|
.. text_with_hl("", hl_background)
|
|
end
|
|
elseif tabs_layout == "equal" then
|
|
for _, tab in ipairs(tabs) do
|
|
return_string = return_string
|
|
.. render_tab(
|
|
tab.left,
|
|
tab.right,
|
|
tab.sep_hl,
|
|
text_layout(tab.text, content_layout, math.floor(remaining_width / #tabs), trunc_char),
|
|
tab.tab_hl,
|
|
calc_click_id_from_source(winid, tab.index)
|
|
)
|
|
.. text_with_hl("", hl_background)
|
|
end
|
|
else -- config.source_selector.tab_labels == "start", "end", "center"
|
|
-- calculate padding based on tabs_layout
|
|
local pad_length = width - length_sum
|
|
local left_pad, right_pad = 0, 0
|
|
if pad_length > 0 then
|
|
if tabs_layout == "start" then
|
|
left_pad, right_pad = 0, pad_length
|
|
elseif tabs_layout == "end" then
|
|
left_pad, right_pad = pad_length, 0
|
|
elseif tabs_layout == "center" then
|
|
left_pad, right_pad = pad_length / 2, math.ceil(pad_length / 2)
|
|
end
|
|
end
|
|
|
|
for i, tab in ipairs(tabs) do
|
|
if width == 0 then
|
|
break
|
|
end
|
|
|
|
-- only render trunc_char if there is no space for the tab
|
|
local sep_length = tab.length - tab.text_length
|
|
if width <= sep_length + 1 then
|
|
return_string = return_string
|
|
.. text_with_hl(trunc_char .. add_padding(width - 1), hl_background)
|
|
width = 0
|
|
break
|
|
end
|
|
|
|
-- tab_length should not exceed width
|
|
local tab_length = width < tab.length and width or tab.length
|
|
width = width - tab_length
|
|
|
|
-- add padding for first and last tab
|
|
local tab_text = tab.text
|
|
if i == 1 then
|
|
tab_text = add_padding(left_pad) .. tab_text
|
|
tab_length = tab_length + left_pad
|
|
end
|
|
if i == #tabs then
|
|
tab_text = tab_text .. add_padding(right_pad)
|
|
tab_length = tab_length + right_pad
|
|
end
|
|
|
|
return_string = return_string
|
|
.. render_tab(
|
|
tab.left,
|
|
tab.right,
|
|
tab.sep_hl,
|
|
text_layout(tab_text, tabs_layout, tab_length - sep_length, trunc_char),
|
|
tab.tab_hl,
|
|
calc_click_id_from_source(winid, tab.index)
|
|
)
|
|
end
|
|
end
|
|
return return_string .. "%<%0@v:lua.___neotree_selector_click@"
|
|
end
|
|
|
|
---set_source_selector:
|
|
-- (public): Directly set source_selector to current window's winbar / statusline
|
|
---@param state table: state
|
|
---@return nil
|
|
M.set_source_selector = function(state)
|
|
local sel_config = utils.resolve_config_option(require("neo-tree").config, "source_selector", {})
|
|
if sel_config and sel_config.winbar then
|
|
vim.wo[state.winid].winbar = "%{%v:lua.require'neo-tree.ui.selector'.get()%}"
|
|
end
|
|
if sel_config and sel_config.statusline then
|
|
vim.wo[state.winid].statusline = "%{%v:lua.require'neo-tree.ui.selector'.get()%}"
|
|
end
|
|
end
|
|
|
|
-- @v:lua@ in the tabline only supports global functions, so this is
|
|
-- the only way to add click handlers without autoloaded vimscript functions
|
|
_G.___neotree_selector_click = function(id, _, _, _)
|
|
if id < 1 then
|
|
return
|
|
end
|
|
local sources = require("neo-tree").config.source_selector.sources
|
|
local winid, source_index = calc_source_from_click_id(id)
|
|
local state = manager.get_state_for_window(winid)
|
|
if state == nil then
|
|
log.warn("state not found for window ", winid, "; ignoring click")
|
|
return
|
|
end
|
|
require("neo-tree.command").execute({
|
|
source = sources[source_index].source,
|
|
position = state.current_position,
|
|
action = "focus",
|
|
})
|
|
end
|
|
|
|
return M
|