Meh I'll figure out submodules later

This commit is contained in:
mustard 2025-09-16 01:01:02 +02:00
parent 4ca9d44a90
commit 8cb281f436
352 changed files with 66107 additions and 0 deletions

View file

@ -0,0 +1,324 @@
local log = require("neo-tree.log")
local utils = require("neo-tree.utils")
local M = {}
---@type integer
M.ns_id = vim.api.nvim_create_namespace("neo-tree.nvim")
M.BUFFER_NUMBER = "NeoTreeBufferNumber"
M.CURSOR_LINE = "NeoTreeCursorLine"
M.DIM_TEXT = "NeoTreeDimText"
M.DIRECTORY_ICON = "NeoTreeDirectoryIcon"
M.DIRECTORY_NAME = "NeoTreeDirectoryName"
M.DOTFILE = "NeoTreeDotfile"
M.FADE_TEXT_1 = "NeoTreeFadeText1"
M.FADE_TEXT_2 = "NeoTreeFadeText2"
M.FILE_ICON = "NeoTreeFileIcon"
M.FILE_NAME = "NeoTreeFileName"
M.FILE_NAME_OPENED = "NeoTreeFileNameOpened"
M.FILE_STATS = "NeoTreeFileStats"
M.FILE_STATS_HEADER = "NeoTreeFileStatsHeader"
M.FILTER_TERM = "NeoTreeFilterTerm"
M.FLOAT_BORDER = "NeoTreeFloatBorder"
M.FLOAT_NORMAL = "NeoTreeFloatNormal"
M.FLOAT_TITLE = "NeoTreeFloatTitle"
M.GIT_ADDED = "NeoTreeGitAdded"
M.GIT_CONFLICT = "NeoTreeGitConflict"
M.GIT_DELETED = "NeoTreeGitDeleted"
M.GIT_IGNORED = "NeoTreeGitIgnored"
M.GIT_MODIFIED = "NeoTreeGitModified"
M.GIT_RENAMED = "NeoTreeGitRenamed"
M.GIT_STAGED = "NeoTreeGitStaged"
M.GIT_UNTRACKED = "NeoTreeGitUntracked"
M.GIT_UNSTAGED = "NeoTreeGitUnstaged"
M.HIDDEN_BY_NAME = "NeoTreeHiddenByName"
M.INDENT_MARKER = "NeoTreeIndentMarker"
M.MESSAGE = "NeoTreeMessage"
M.MODIFIED = "NeoTreeModified"
M.NORMAL = "NeoTreeNormal"
M.NORMALNC = "NeoTreeNormalNC"
M.SIGNCOLUMN = "NeoTreeSignColumn"
M.STATUS_LINE = "NeoTreeStatusLine"
M.STATUS_LINE_NC = "NeoTreeStatusLineNC"
M.TAB_ACTIVE = "NeoTreeTabActive"
M.TAB_INACTIVE = "NeoTreeTabInactive"
M.TAB_SEPARATOR_ACTIVE = "NeoTreeTabSeparatorActive"
M.TAB_SEPARATOR_INACTIVE = "NeoTreeTabSeparatorInactive"
M.VERTSPLIT = "NeoTreeVertSplit"
M.WINSEPARATOR = "NeoTreeWinSeparator"
M.END_OF_BUFFER = "NeoTreeEndOfBuffer"
M.ROOT_NAME = "NeoTreeRootName"
M.SYMBOLIC_LINK_TARGET = "NeoTreeSymbolicLinkTarget"
M.TITLE_BAR = "NeoTreeTitleBar"
M.EXPANDER = "NeoTreeExpander"
M.WINDOWS_HIDDEN = "NeoTreeWindowsHidden"
M.PREVIEW = "NeoTreePreview"
---@param n integer
---@param chars integer?
local function dec_to_hex(n, chars)
chars = chars or 6
local hex = string.format("%0" .. chars .. "x", n)
while #hex < chars do
hex = "0" .. hex
end
return hex
end
---@param name string
local get_hl_by_name = function(name)
if vim.api.nvim_get_hl then
local hl = vim.api.nvim_get_hl(0, { name = name })
---@diagnostic disable-next-line: inject-field
hl.foreground = hl.fg
---@diagnostic disable-next-line: inject-field
hl.background = hl.bg
return hl
end
---TODO: remove in 4.0
---@diagnostic disable-next-line: deprecated
return vim.api.nvim_get_hl_by_name(name, true)
end
---If the given highlight group is not defined, define it.
---@param hl_group_name string The name of the highlight group.
---@param link_to_if_exists string[] A list of highlight groups to link to, in order of priority. The first one that exists will be used.
---@param background string? The background color to use, in hex, if the highlight group is not defined and it is not linked to another group.
---@param foreground string? The foreground color to use, in hex, if the highlight group is not defined and it is not linked to another group.
---@param gui string? The gui to use, if the highlight group is not defined and it is not linked to another group.
---@return table hlgroups The highlight group values.
M.create_highlight_group = function(hl_group_name, link_to_if_exists, background, foreground, gui)
local success, hl_group = pcall(get_hl_by_name, hl_group_name, true)
if not success or not hl_group.foreground or not hl_group.background then
for _, link_to in ipairs(link_to_if_exists) do
success, hl_group = pcall(get_hl_by_name, link_to, true)
if success then
local new_group_has_settings = background or foreground or gui
local link_to_has_settings = hl_group.foreground or hl_group.background
if link_to_has_settings or not new_group_has_settings then
vim.cmd("highlight default link " .. hl_group_name .. " " .. link_to)
return hl_group
end
end
end
if type(background) == "number" then
background = dec_to_hex(background)
end
if type(foreground) == "number" then
foreground = dec_to_hex(foreground)
end
local cmd = "highlight default " .. hl_group_name
if background then
cmd = cmd .. " guibg=#" .. background
end
if foreground then
cmd = cmd .. " guifg=#" .. foreground
else
cmd = cmd .. " guifg=NONE"
end
if gui then
cmd = cmd .. " gui=" .. gui
end
vim.cmd(cmd)
return {
background = background and tonumber(background, 16) or nil,
foreground = foreground and tonumber(foreground, 16) or nil,
}
end
return hl_group
end
---@param hl_group_name string
---@param fade_percentage number
local calculate_faded_highlight_group = function(hl_group_name, fade_percentage)
local normal = get_hl_by_name("Normal")
if type(normal.foreground) ~= "number" then
if vim.go.background == "dark" then
normal.foreground = 0xffffff
else
normal.foreground = 0x000000
end
end
if type(normal.background) ~= "number" then
if vim.go.background == "dark" then
normal.background = 0x000000
else
normal.background = 0xffffff
end
end
local foreground = dec_to_hex(normal.foreground)
local background = dec_to_hex(normal.background)
local hl_group = get_hl_by_name(hl_group_name)
if type(hl_group.foreground) == "number" then
foreground = dec_to_hex(hl_group.foreground)
end
if type(hl_group.background) == "number" then
background = dec_to_hex(hl_group.background)
end
local gui = {}
if hl_group.bold then
table.insert(gui, "bold")
end
if hl_group.italic then
table.insert(gui, "italic")
end
if hl_group.underline then
table.insert(gui, "underline")
end
if hl_group.undercurl then
table.insert(gui, "undercurl")
end
local hl
if #gui > 0 then
hl = table.concat(gui, ",")
end
local f_red = tonumber(foreground:sub(1, 2), 16)
local f_green = tonumber(foreground:sub(3, 4), 16)
local f_blue = tonumber(foreground:sub(5, 6), 16)
local b_red = tonumber(background:sub(1, 2), 16)
local b_green = tonumber(background:sub(3, 4), 16)
local b_blue = tonumber(background:sub(5, 6), 16)
local red = (f_red * fade_percentage) + (b_red * (1 - fade_percentage))
local green = (f_green * fade_percentage) + (b_green * (1 - fade_percentage))
local blue = (f_blue * fade_percentage) + (b_blue * (1 - fade_percentage))
local new_foreground =
string.format("%s%s%s", dec_to_hex(red, 2), dec_to_hex(green, 2), dec_to_hex(blue, 2))
return {
background = hl_group.background,
foreground = new_foreground,
gui = hl,
}
end
local faded_highlight_group_cache = {}
---@param hl_group_name string
---@param fade_percentage number
M.get_faded_highlight_group = function(hl_group_name, fade_percentage)
if type(hl_group_name) ~= "string" then
error("hl_group_name must be a string")
end
if type(fade_percentage) ~= "number" then
error("hl_group_name must be a number")
end
if fade_percentage < 0 or fade_percentage > 1 then
error("fade_percentage must be between 0 and 1")
end
local key = hl_group_name .. "_" .. tostring(math.floor(fade_percentage * 100))
if faded_highlight_group_cache[key] then
return faded_highlight_group_cache[key]
end
local faded = calculate_faded_highlight_group(hl_group_name, fade_percentage)
M.create_highlight_group(key, {}, faded.background, faded.foreground, faded.gui)
faded_highlight_group_cache[key] = key
return key
end
local nvim_0_10 = vim.fn.has("nvim-0.10")
M.setup = function()
local added_hl_name = nvim_0_10 and "Added" or "diffAdded"
local changed_hl_name = nvim_0_10 and "Changed" or "diffChanged"
local removed_hl_name = nvim_0_10 and "Removed" or "diffRemoved"
-- Reset this here in case of color scheme change
faded_highlight_group_cache = {}
local normal_hl = M.create_highlight_group(M.NORMAL, { "Normal" })
local normalnc_hl = M.create_highlight_group(M.NORMALNC, { "NormalNC", M.NORMAL })
M.create_highlight_group(M.SIGNCOLUMN, { "SignColumn", M.NORMAL })
M.create_highlight_group(M.STATUS_LINE, { "StatusLine" })
M.create_highlight_group(M.STATUS_LINE_NC, { "StatusLineNC" })
M.create_highlight_group(M.VERTSPLIT, { "VertSplit" })
M.create_highlight_group(M.WINSEPARATOR, { "WinSeparator" })
M.create_highlight_group(M.END_OF_BUFFER, { "EndOfBuffer" })
local float_border_hl =
M.create_highlight_group(M.FLOAT_BORDER, { "FloatBorder" }, normalnc_hl.background, "444444")
M.create_highlight_group(M.FLOAT_NORMAL, { "NormalFloat", M.NORMAL })
M.create_highlight_group(M.FLOAT_TITLE, {}, float_border_hl.background, normal_hl.foreground)
local title_fg = normal_hl.background
if title_fg == float_border_hl.foreground then
title_fg = normal_hl.foreground
end
M.create_highlight_group(M.TITLE_BAR, {}, float_border_hl.foreground, title_fg)
local dim_text = calculate_faded_highlight_group("NeoTreeNormal", 0.3)
M.create_highlight_group(M.BUFFER_NUMBER, { "SpecialChar" })
--M.create_highlight_group(M.DIM_TEXT, {}, nil, "505050")
M.create_highlight_group(M.MESSAGE, {}, nil, dim_text.foreground, "italic")
M.create_highlight_group(M.FADE_TEXT_1, {}, nil, "626262")
M.create_highlight_group(M.FADE_TEXT_2, {}, nil, "444444")
M.create_highlight_group(M.DOTFILE, {}, nil, "626262")
M.create_highlight_group(M.HIDDEN_BY_NAME, { M.DOTFILE }, nil, nil)
M.create_highlight_group(M.CURSOR_LINE, { "CursorLine" }, nil, nil, "bold")
M.create_highlight_group(M.DIM_TEXT, {}, nil, dim_text.foreground)
M.create_highlight_group(M.DIRECTORY_NAME, { "Directory" }, "NONE", "NONE")
M.create_highlight_group(M.DIRECTORY_ICON, { "Directory" }, nil, "73cef4")
M.create_highlight_group(M.FILE_ICON, { M.DIRECTORY_ICON })
M.create_highlight_group(M.FILE_NAME, {}, "NONE", "NONE")
M.create_highlight_group(M.FILE_NAME_OPENED, {}, nil, nil, "bold")
M.create_highlight_group(M.SYMBOLIC_LINK_TARGET, { M.FILE_NAME })
M.create_highlight_group(M.FILTER_TERM, { "SpecialChar", "Normal" })
M.create_highlight_group(M.ROOT_NAME, {}, nil, nil, "bold,italic")
M.create_highlight_group(M.INDENT_MARKER, { M.DIM_TEXT })
M.create_highlight_group(M.EXPANDER, { M.DIM_TEXT })
M.create_highlight_group(M.MODIFIED, {}, nil, "d7d787")
M.create_highlight_group(M.WINDOWS_HIDDEN, { M.DOTFILE }, nil, nil)
M.create_highlight_group(M.PREVIEW, { "Search" }, nil, nil)
M.create_highlight_group(
M.GIT_ADDED,
{ "GitGutterAdd", "GitSignsAdd", added_hl_name },
nil,
"5faf5f"
)
M.create_highlight_group(
M.GIT_DELETED,
{ "GitGutterDelete", "GitSignsDelete", removed_hl_name },
nil,
"ff5900"
)
M.create_highlight_group(
M.GIT_MODIFIED,
{ "GitGutterChange", "GitSignsChange", changed_hl_name },
nil,
"d7af5f"
)
local conflict = M.create_highlight_group(M.GIT_CONFLICT, {}, nil, "ff8700", "italic,bold")
M.create_highlight_group(M.GIT_IGNORED, { M.DOTFILE }, nil, nil)
M.create_highlight_group(M.GIT_RENAMED, { M.GIT_MODIFIED }, nil, nil)
M.create_highlight_group(M.GIT_STAGED, { M.GIT_ADDED }, nil, nil)
M.create_highlight_group(M.GIT_UNSTAGED, { M.GIT_CONFLICT }, nil, nil)
M.create_highlight_group(M.GIT_UNTRACKED, {}, nil, conflict.foreground, "italic")
M.create_highlight_group(M.TAB_ACTIVE, {}, nil, nil, "bold")
M.create_highlight_group(M.TAB_INACTIVE, {}, "141414", "777777")
M.create_highlight_group(M.TAB_SEPARATOR_ACTIVE, {}, nil, "0a0a0a")
M.create_highlight_group(M.TAB_SEPARATOR_INACTIVE, {}, "141414", "101010")
local faded_normal = calculate_faded_highlight_group("NeoTreeNormal", 0.4)
M.create_highlight_group(M.FILE_STATS, {}, nil, faded_normal.foreground)
local faded_root = calculate_faded_highlight_group("NeoTreeRootName", 0.5)
M.create_highlight_group(M.FILE_STATS_HEADER, {}, nil, faded_root.foreground, faded_root.gui)
end
return M

View file

@ -0,0 +1,109 @@
local NuiInput = require("nui.input")
local nt = require("neo-tree")
local popups = require("neo-tree.ui.popups")
local events = require("neo-tree.events")
local M = {}
---@param input NuiInput
---@param callback function?
M.show_input = function(input, callback)
input:mount()
input:map("i", "<esc>", function()
vim.cmd("stopinsert")
input:unmount()
end, { noremap = true })
input:map("n", "<esc>", function()
input:unmount()
end, { noremap = true })
input:map("n", "q", function()
input:unmount()
end, { noremap = true })
input:map("i", "<C-w>", "<C-S-w>", { noremap = true })
local event = require("nui.utils.autocmd").event
input:on({ event.BufLeave, event.BufDelete }, function()
input:unmount()
if callback then
callback()
end
end, { once = true })
if input.prompt_type ~= "confirm" then
vim.schedule(function()
events.fire_event(events.NEO_TREE_POPUP_INPUT_READY, {
bufnr = input.bufnr,
winid = input.winid,
})
end)
end
end
---@param message string
---@param default_value string?
---@param callback function
---@param options nui_popup_options?
---@param completion string?
M.input = function(message, default_value, callback, options, completion)
if nt.config.use_popups_for_input then
local popup_options = popups.popup_options(message, 10, options)
local input = NuiInput(popup_options, {
prompt = " ",
default_value = default_value,
on_submit = callback,
})
M.show_input(input)
else
local opts = {
prompt = message .. "\n",
default = default_value,
}
if vim.opt.cmdheight:get() == 0 then
-- NOTE: I really don't know why but letters before the first '\n' is not rendered except in noice.nvim
-- when vim.opt.cmdheight = 0 <2023-10-24, pysan3>
opts.prompt = "Neo-tree Popup\n" .. opts.prompt
end
if completion then
opts.completion = completion
end
vim.ui.input(opts, callback)
end
end
---Blocks if callback is omitted
---@param message string
---@param callback? fun(confirmed: boolean)
---@return boolean? confirmed_if_no_callback
M.confirm = function(message, callback)
if callback then
if nt.config.use_popups_for_input then
local popup_options = popups.popup_options(message, 10)
---@class NuiInput
local input = NuiInput(popup_options, {
prompt = " y/n: ",
on_close = function()
callback(false)
end,
on_submit = function(value)
callback(value == "y" or value == "Y")
end,
})
input.prompt_type = "confirm"
M.show_input(input)
else
callback(vim.fn.confirm(message, "&Yes\n&No") == 1)
end
else
return vim.fn.confirm(message, "&Yes\n&No") == 1
end
end
return M

View file

@ -0,0 +1,147 @@
local NuiText = require("nui.text")
local NuiPopup = require("nui.popup")
local nt = require("neo-tree")
local highlights = require("neo-tree.ui.highlights")
local log = require("neo-tree.log")
local M = {}
local winborder_option_exists = vim.fn.exists("&winborder") > 0
-- These borders will cause errors when trying to display border text with them
local invalid_borders = { "", "none", "shadow" }
---@param title string
---@param min_width integer?
---@param override_options table?
M.popup_options = function(title, min_width, override_options)
if string.len(title) ~= 0 then
title = " " .. title .. " "
end
min_width = min_width or 30
local width = string.len(title) + 2
local popup_border_style = nt.config.popup_border_style
if popup_border_style == "" then
-- Try to use winborder
if not winborder_option_exists or vim.tbl_contains(invalid_borders, vim.o.winborder) then
popup_border_style = "single"
else
---@diagnostic disable-next-line: cast-local-type
popup_border_style = vim.o.winborder
end
end
local popup_border_text = NuiText(title, highlights.FLOAT_TITLE)
local col = 0
-- fix popup position when using multigrid
local popup_last_col = vim.api.nvim_win_get_position(0)[2] + width + 2
if popup_last_col >= vim.o.columns then
col = vim.o.columns - popup_last_col
end
---@type nui_popup_options
local popup_options = {
ns_id = highlights.ns_id,
relative = "cursor",
position = {
row = 1,
col = col,
},
size = width,
border = {
text = {
top = popup_border_text,
},
---@diagnostic disable-next-line: assign-type-mismatch
style = popup_border_style,
highlight = highlights.FLOAT_BORDER,
},
win_options = {
winhighlight = "Normal:"
.. highlights.FLOAT_NORMAL
.. ",FloatBorder:"
.. highlights.FLOAT_BORDER,
},
buf_options = {
bufhidden = "delete",
buflisted = false,
filetype = "neo-tree-popup",
},
}
if popup_border_style == "NC" then
local blank = NuiText(" ", highlights.TITLE_BAR)
popup_border_text = NuiText(title, highlights.TITLE_BAR)
popup_options.border = {
style = { "", blank, "", "", " ", "", " ", "" },
highlight = highlights.FLOAT_BORDER,
text = {
top = popup_border_text,
top_align = "left",
},
}
end
if override_options then
return vim.tbl_extend("force", popup_options, override_options)
else
return popup_options
end
end
---@param title string
---@param message elem_or_list<string|integer>
---@param size integer?
M.alert = function(title, message, size)
local lines = {}
local max_line_width = title:len()
---@param line any
local add_line = function(line)
line = tostring(line)
if line:len() > max_line_width then
max_line_width = line:len()
end
table.insert(lines, line)
end
if type(message) == "table" then
for _, v in ipairs(message) do
add_line(v)
end
else
add_line(message)
end
add_line("")
add_line(" Press <Escape> or <Enter> to close")
local win_options = M.popup_options(title, 80)
win_options.zindex = 60
win_options.size = {
width = max_line_width + 4,
height = #lines + 1,
}
local win = NuiPopup(win_options)
win:mount()
local success, msg = pcall(vim.api.nvim_buf_set_lines, win.bufnr, 0, 0, false, lines)
if success then
win:map("n", "<esc>", function()
win:unmount()
end, { noremap = true })
win:map("n", "<enter>", function()
win:unmount()
end, { noremap = true })
local event = require("nui.utils.autocmd").event
win:on({ event.BufLeave, event.BufDelete }, function()
win:unmount()
end, { once = true })
-- why is this necessary?
vim.api.nvim_set_current_win(win.winid)
else
log.error(msg)
win:unmount()
end
end
return M

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,405 @@
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
---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 (utils.truncate_by_cell(text, output_width))
else
return (utils.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 = assert(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 neotree.State:
---@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 = config.source_selector.padding
local padding
if type(padding_config) == "number" then
padding = { left = padding_config, right = padding_config }
else
padding = padding_config or { left = 0, right = 0 }
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 or {}
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 or "equal"
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 then -- not enough width
tabs_layout = "equal" -- other methods cannot handle this
end
if tabs_layout == "active" then
local active_tab_length = width - length_sum + length_active - 1
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 neotree.State: state
---@return nil
M.set_source_selector = function(state)
if state.enable_source_selector == false then
return
end
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 or {}
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

View file

@ -0,0 +1,27 @@
local locations = {}
locations.get_location = function(location)
local tab = vim.api.nvim_get_current_tabpage()
if not locations[tab] then
locations[tab] = {}
end
local loc = locations[tab][location]
if loc then
if loc.winid ~= 0 then
-- verify the window before we return it
if not vim.api.nvim_win_is_valid(loc.winid) then
loc.winid = 0
end
end
return loc
end
loc = {
source = nil,
name = location,
winid = 0,
}
locations[tab][location] = loc
return loc
end
return locations