1

Refresh generated nvim config

This commit is contained in:
2024-06-06 00:28:16 +02:00
parent 67ae09d87f
commit f0fadf6d12
24 changed files with 2626 additions and 1519 deletions

View File

@ -0,0 +1,200 @@
local config = require("winshift.config")
local api = vim.api
local M = {}
--#region TYPES
---@class HiSpec
---@field fg string
---@field bg string
---@field ctermfg integer
---@field ctermbg integer
---@field gui string
---@field sp string
---@field blend integer
---@field default boolean
---@class HiLinkSpec
---@field force boolean
---@field default boolean
--#endregion
---@param name string Syntax group name.
---@param attr string Attribute name.
---@param trans boolean Translate the syntax group (follows links).
function M.get_hl_attr(name, attr, trans)
local id = api.nvim_get_hl_id_by_name(name)
if id and trans then
id = vim.fn.synIDtrans(id)
end
if not id then
return
end
local value = vim.fn.synIDattr(id, attr)
if not value or value == "" then
return
end
return value
end
---@param groups string|string[] Syntax group name, or an ordered list of
---groups where the first found value will be returned.
---@param trans boolean Translate the syntax group (follows links). True by default.
function M.get_fg(groups, trans)
if type(trans) ~= "boolean" then trans = true end
if type(groups) == "table" then
local v
for _, group in ipairs(groups) do
v = M.get_hl_attr(group, "fg", trans)
if v then return v end
end
return
end
return M.get_hl_attr(groups, "fg", trans)
end
---@param groups string|string[] Syntax group name, or an ordered list of
---groups where the first found value will be returned.
---@param trans boolean Translate the syntax group (follows links). True by default.
function M.get_bg(groups, trans)
if type(trans) ~= "boolean" then trans = true end
if type(groups) == "table" then
local v
for _, group in ipairs(groups) do
v = M.get_hl_attr(group, "bg", trans)
if v then return v end
end
return
end
return M.get_hl_attr(groups, "bg", trans)
end
---@param groups string|string[] Syntax group name, or an ordered list of
---groups where the first found value will be returned.
---@param trans boolean Translate the syntax group (follows links). True by default.
function M.get_gui(groups, trans)
if type(trans) ~= "boolean" then trans = true end
if type(groups) ~= "table" then groups = { groups } end
local hls
for _, group in ipairs(groups) do
hls = {}
local attributes = {
"bold",
"italic",
"reverse",
"standout",
"underline",
"undercurl",
"strikethrough"
}
for _, attr in ipairs(attributes) do
if M.get_hl_attr(group, attr, trans) == "1" then
table.insert(hls, attr)
end
end
if #hls > 0 then
return table.concat(hls, ",")
end
end
end
---@param group string Syntax group name.
---@param opt HiSpec
function M.hi(group, opt)
local use_tc = vim.o.termguicolors
local g = use_tc and "gui" or "cterm"
if not use_tc then
if opt.ctermfg then
opt.fg = opt.ctermfg
end
if opt.ctermbg then
opt.bg = opt.ctermbg
end
end
vim.cmd(string.format(
"hi %s %s %s %s %s %s %s",
opt.default and "def" or "",
group,
opt.fg and (g .. "fg=" .. opt.fg) or "",
opt.bg and (g .. "bg=" .. opt.bg) or "",
opt.gui and ((use_tc and "gui=" or "cterm=") .. opt.gui) or "",
opt.sp and ("guisp=" .. opt.sp) or "",
opt.blend and ("blend=" .. opt.blend) or ""
))
end
---@param from string Syntax group name.
---@param to? string Syntax group name. (default: `"NONE"`)
---@param opt? HiLinkSpec
function M.hi_link(from, to, opt)
opt = opt or {}
vim.cmd(string.format(
"hi%s %s link %s %s",
opt.force and "!" or "",
opt.default and "default" or "",
from,
to or "NONE"
))
end
function M.get_colors()
return {
white = M.get_fg("Normal") or "White",
red = M.get_fg("Keyword") or "Red",
green = M.get_fg("Character") or "Green",
yellow = M.get_fg("PreProc") or "Yellow",
blue = M.get_fg("Include") or "Blue",
purple = M.get_fg("Define") or "Purple",
cyan = M.get_fg("Conditional") or "Cyan",
dark_red = M.get_fg("Keyword") or "DarkRed",
orange = M.get_fg("Number") or "Orange",
}
end
function M.get_hl_groups()
local hl_focused = config.get_config().focused_hl_group
local reverse = M.get_hl_attr(hl_focused, "reverse") == "1"
local bg_focused = reverse
and (M.get_fg({ hl_focused, "Normal" }) or "white")
or (M.get_bg({ hl_focused, "Normal" }) or "white")
local fg_focused = reverse and (M.get_bg({ hl_focused, "Normal" }) or "black") or nil
return {
Normal = { fg = fg_focused, bg = bg_focused },
EndOfBuffer = { fg = bg_focused, bg = bg_focused },
LineNr = { fg = M.get_fg("LineNr"), bg = bg_focused, gui = M.get_gui("LineNr") },
CursorLineNr = { fg = M.get_fg("CursorLineNr"), bg = bg_focused, gui = M.get_gui("CursorLineNr") },
SignColumn = { fg = M.get_fg("SignColumn"), bg = bg_focused },
FoldColumn = { fg = M.get_fg("FoldColumn"), bg = bg_focused },
WindowPicker = { fg = "#ededed", bg = "#4493c8", ctermfg = 255, ctermbg = 33, gui = "bold" },
}
end
M.hl_links = {
LineNrAbove = "WinShiftLineNr",
LineNrBelow = "WinShiftLineNr",
}
function M.setup()
for name, opt in pairs(M.get_hl_groups()) do
M.hi("WinShift" .. name, opt)
end
for from, to in pairs(M.hl_links) do
M.hi_link("WinShift" .. from, to, { default = true })
end
end
return M

View File

@ -0,0 +1,111 @@
local utils = require("winshift.utils")
local M = {}
-- stylua: ignore start
M.defaults = {
highlight_moving_win = true,
focused_hl_group = "Visual",
moving_win_options = {
wrap = false,
cursorline = false,
cursorcolumn = false,
colorcolumn = "",
},
keymaps = {
disable_defaults = false,
win_move_mode = {
["h"] = "left",
["j"] = "down",
["k"] = "up",
["l"] = "right",
["H"] = "far_left",
["J"] = "far_down",
["K"] = "far_up",
["L"] = "far_right",
["<left>"] = "left",
["<down>"] = "down",
["<up>"] = "up",
["<right>"] = "right",
["<S-left>"] = "far_left",
["<S-down>"] = "far_down",
["<S-up>"] = "far_up",
["<S-right>"] = "far_right",
},
},
---A function that should prompt the user to select a window.
---
---The window picker is used to select a window while swapping windows with
---`:WinShift swap`.
---@return integer? winid # Either the selected window ID, or `nil` to
--- indicate that the user cancelled / gave an invalid selection.
window_picker = function()
return require("winshift.lib").pick_window({
picker_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890",
filter_rules = {
cur_win = true,
floats = true,
filetype = {},
buftype = {},
bufname = {},
},
---A function used to filter the list of selectable windows.
---@param winids integer[] # The list of selectable window IDs.
---@return integer[] filtered # The filtered list of window IDs.
filter_func = nil,
})
end,
}
-- stylua: ignore end
M._config = M.defaults
function M.get_key_dir_map()
local t = {}
for lhs, rhs in pairs(M._config.keymaps.win_move_mode) do
if lhs:match("%b<>") then
-- Get raw key code for special keys
lhs = utils.raw_key(lhs)
end
t[lhs] = rhs
end
return t
end
function M.get_config()
return M._config
end
function M.setup(user_config)
user_config = user_config or {}
M._config = utils.tbl_deep_clone(M.defaults)
M._config = vim.tbl_deep_extend("force", M._config, user_config)
M._config.moving_win_options = user_config.moving_win_options or M._config.moving_win_options
--#region DEPRECATION NOTICES
if M._config.window_picker_chars or M._config.window_picker_ignore then
utils.warn(table.concat({
"'window_picker_chars' and 'window_picker_ignore' has been deprecated!",
" See ':h winshift.changelog-11' for more information.",
}, ""))
end
--#endregion
if M._config.keymaps.disable_defaults then
for name, _ in pairs(M._config.keymaps) do
if name ~= "disable_defaults" then
M._config.keymaps[name] = vim.tbl_get(user_config, "keymaps", name) or {}
end
end
end
require("winshift.colors").setup()
end
return M

View File

@ -0,0 +1,659 @@
local utils = require("winshift.utils")
local config = require("winshift.config")
local api = vim.api
local M = {}
local win_option_store = {}
---@class Node : { [integer]: Node }
---@field type '"leaf"'|'"row"'|'"col"'
---@field parent Node
---@field index integer
---@field winid integer|nil
---@class VirtualNode : Node
---@field target Node
---@alias HDirection '"left"'|'"right"'
---@alias VDirection '"up"'|'"down"'
---@alias Direction HDirection|VDirection|'"far_left"'|'"far_right"'|'"far_up"'|'"far_down"'
M.dir_move_map = {
far_left = "H",
far_down = "J",
far_up = "K",
far_right = "L",
}
function M.process_layout(layout)
local function recurse(parent)
---@type Node
local node = { type = parent[1] }
if node.type == "leaf" then
node.winid = parent[2]
else
for i, child in ipairs(parent[2]) do
node[#node + 1] = recurse(child)
node[#node].index = i
node[#node].parent = node
end
end
return node
end
return recurse(layout)
end
function M.get_layout_tree()
return M.process_layout(vim.fn.winlayout())
end
---@param node Node
---@return Node
function M.get_first_leaf(node)
local cur = node
while cur.type ~= "leaf" do
cur = cur[1]
end
return cur
end
---@param node Node
---@return Node
function M.get_last_leaf(node)
local cur = node
while cur.type ~= "leaf" do
cur = cur[#cur]
end
return cur
end
---@param tree Node
---@param winid integer
function M.find_leaf(tree, winid)
---@param node Node
---@return Node
local function recurse(node)
if node.type == "leaf" and node.winid == winid then
return node
else
for _, child in ipairs(node) do
local target = recurse(child)
if target then
return target
end
end
end
end
return recurse(tree)
end
---@param a Node
---@param b Node
function M.swap_leaves(a, b)
vim.cmd(
string.format(
"noautocmd keepjumps %dwindo belowright %s",
api.nvim_win_get_number(a.winid),
a.parent.type == "col" and "vsp" or "sp"
)
)
local temp_a = api.nvim_get_current_win()
local opt_a = { vertical = a.parent.type == "col", rightbelow = false }
vim.cmd(
string.format(
"noautocmd keepjumps %dwindo belowright %s",
api.nvim_win_get_number(b.winid),
b.parent.type == "col" and "vsp" or "sp"
)
)
local temp_b = api.nvim_get_current_win()
local opt_b = { vertical = b.parent.type == "col", rightbelow = false }
vim.fn.win_splitmove(a.winid, temp_b, opt_b)
vim.fn.win_splitmove(b.winid, temp_a, opt_a)
api.nvim_win_close(temp_a, true)
api.nvim_win_close(temp_b, true)
end
---Move a row into a target window, replacing the target.
---@param row Node
---@param target integer Window id
---@param ignore? table<integer, boolean>
function M.move_row(row, target, ignore)
ignore = ignore or {}
local opt = { vertical = true, rightbelow = false }
---@type Node
for _, node in ipairs(row) do
if node.type == "col" then
local nr = api.nvim_win_get_number(target)
vim.cmd("noautocmd keepjumps " .. nr .. "windo aboveleft vsp")
M.move_col(node, api.nvim_get_current_win(), ignore)
elseif not ignore[node.winid] then
vim.fn.win_splitmove(node.winid, target, opt)
end
end
api.nvim_win_close(target, true)
end
---Move a column into a target window, replacing the target.
---@param col Node
---@param target integer Window id
---@param ignore? table<integer, boolean>
function M.move_col(col, target, ignore)
ignore = ignore or {}
local opt = { vertical = false, rightbelow = false }
---@type Node
for _, node in ipairs(col) do
if node.type == "row" then
local nr = api.nvim_win_get_number(target)
vim.cmd("noautocmd keepjumps " .. nr .. "windo aboveleft sp")
M.move_row(node, api.nvim_get_current_win(), ignore)
elseif not ignore[node.winid] then
vim.fn.win_splitmove(node.winid, target, opt)
end
end
api.nvim_win_close(target, true)
end
---Move a leaf out of a row in a given direction.
---@param leaf Node
---@param row Node
---@param dir VDirection
function M.row_move_out(leaf, row, dir)
vim.cmd(
string.format(
"noautocmd keepjumps %dwindo %s sp",
api.nvim_win_get_number(leaf.winid),
dir == "up" and "belowright" or "aboveleft"
)
)
local tempwin = api.nvim_get_current_win()
M.move_row(row, tempwin, { [leaf.winid] = true })
end
---Move a leaf out of a column in a given direction.
---@param leaf Node
---@param col Node
---@param dir HDirection
function M.col_move_out(leaf, col, dir)
vim.cmd(
string.format(
"noautocmd keepjumps %dwindo %s vsp",
api.nvim_win_get_number(leaf.winid),
dir == "left" and "belowright" or "aboveleft"
)
)
local tempwin = api.nvim_get_current_win()
M.move_col(col, tempwin, { [leaf.winid] = true })
end
---Move a leaf into a row.
---@param leaf Node
---@param row Node
---@param dir HDirection Determines what side of the row the leaf is moved to.
function M.row_move_in(leaf, row, dir)
local target_leaf = dir == "right" and M.get_last_leaf(row) or M.get_first_leaf(row)
local opt = { vertical = true, rightbelow = dir == "right" }
vim.fn.win_splitmove(leaf.winid, target_leaf.winid, opt)
vim.cmd(
string.format(
"noautocmd keepjumps %dwindo %s vsp",
api.nvim_win_get_number(leaf.winid),
dir == "right" and "aboveleft" or "belowright"
)
)
local tempwin = api.nvim_get_current_win()
M.move_row(row, tempwin)
end
---Move a leaf into a column.
---@param leaf Node
---@param col Node
---@param dir VDirection Determines what side of the col the leaf is moved to.
function M.col_move_in(leaf, col, dir)
local target_leaf = dir == "down" and M.get_last_leaf(col) or M.get_first_leaf(col)
local opt = { vertical = false, rightbelow = dir == "down" }
vim.fn.win_splitmove(leaf.winid, target_leaf.winid, opt)
vim.cmd(
string.format(
"noautocmd keepjumps %dwindo %s sp",
api.nvim_win_get_number(leaf.winid),
dir == "down" and "aboveleft" or "belowright"
)
)
local tempwin = api.nvim_get_current_win()
M.move_col(col, tempwin)
end
---Get the next node in a given direction in the given leaf's closest row
---parent. Returns `nil` if there's no node in the given direction.
---@param leaf Node
---@param dir HDirection
---@return Node|nil
function M.next_node_horizontal(leaf, dir)
local outside_parent = (dir == "left" and leaf.index == 1)
or (dir == "right" and leaf.index == #leaf.parent)
if leaf.parent.type == "col" or outside_parent then
local outer_parent = leaf.parent.parent
if not outer_parent or outer_parent.type == "col" then
return
end
return outer_parent[leaf.parent.index + ((dir == "left" and -1) or 1)]
else
return leaf.parent[leaf.index + ((dir == "left" and -1) or 1)]
end
end
---Get the next node in a given direction in the given leaf's closest column
---parent. Returns `nil` if there's no node in the given direction.
---@param leaf Node
---@param dir VDirection
---@return Node|nil
function M.next_node_vertical(leaf, dir)
local outside_parent = (dir == "up" and leaf.index == 1)
or (dir == "down" and leaf.index == #leaf.parent)
if leaf.parent.type == "row" or outside_parent then
local outer_parent = leaf.parent.parent
if not outer_parent or outer_parent.type == "row" then
return
end
return outer_parent[leaf.parent.index + ((dir == "up" and -1) or 1)]
else
return leaf.parent[leaf.index + ((dir == "up" and -1) or 1)]
end
end
---@class WindowPickerFilterRules
---@field cur_win boolean
---@field floats boolean
---@field filetype string[]
---@field buftype string[]
---@field bufname string[]
---@class WindowPickerSpec
---@field picker_chars string
---@field filter_rules WindowPickerFilterRules
---@field filter_func fun(winids: integer[]): integer[]
---Get user to pick a window. Selectable windows are all windows in the current
---tabpage.
---@param opt? WindowPickerSpec
---@return integer|nil -- If a valid window was picked, return its id. If an
--- invalid window was picked / user canceled, return nil. If there are
--- no selectable windows, return -1.
function M.pick_window(opt)
opt = opt or {}
local tabpage = api.nvim_get_current_tabpage()
local win_ids = api.nvim_tabpage_list_wins(tabpage)
local curwin = api.nvim_get_current_win()
local filter_rules = opt.filter_rules or {}
local selectable = vim.tbl_filter(function (id)
if filter_rules.cur_win and curwin == id then
return false
elseif filter_rules.floats and api.nvim_win_get_config(id).relative ~= "" then
return false
end
local bufid = api.nvim_win_get_buf(id)
local bufname = api.nvim_buf_get_name(bufid)
for _, option in ipairs({ "filetype", "buftype" }) do
if vim.tbl_contains(filter_rules[option] or {}, vim.bo[bufid][option]) then
return false
end
end
for _, pattern in ipairs(filter_rules.bufname or {}) do
local regex = vim.regex(pattern)
if regex:match_str(bufname) ~= nil then
return false
end
end
return true
end, win_ids)
if opt.filter_func then
selectable = opt.filter_func(selectable)
end
-- If there are no selectable windows: return. If there's only 1, return it without picking.
if #selectable == 0 then return -1 end
if #selectable == 1 then return selectable[1] end
local chars = (opt.picker_chars or "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"):upper()
local i = 1
local win_opts = {}
local win_map = {}
local laststatus = vim.o.laststatus
vim.o.laststatus = 2
-- Setup UI
for _, id in ipairs(selectable) do
local char = chars:sub(i, i)
win_map[char] = id
win_opts[id] = {
statusline = vim.wo[id].statusline,
winhl = vim.wo[id].winhl,
}
utils.set_local(id, {
statusline = "%=" .. char .. "%=",
winhl = {
"StatusLine:WinShiftWindowPicker,StatusLineNC:WinShiftWindowPicker",
opt = { method = "append" },
},
})
i = i + 1
if i > #chars then break end
end
vim.cmd("redraw")
local ok, resp = pcall(utils.input_char, "Pick window: ", { prompt_hl = "ModeMsg" })
if not ok then
utils.clear_prompt()
end
resp = (resp or ""):upper()
-- Restore window options
for _, id in ipairs(selectable) do
utils.set_local(id, win_opts[id])
end
vim.o.laststatus = laststatus
return win_map[resp]
end
---@param leaf Node
---@param flatten? boolean
---@return VirtualNode|nil
function M.create_virtual_set(leaf, flatten)
if not leaf.parent then
return
end
local parent = leaf.parent
-- Handle case where moving the leaf will result in a different virtual set.
if flatten
and #parent == 2
and parent[leaf.index % 2 + 1].type == "leaf"
and parent.parent then
leaf = utils.tbl_clone(leaf)
parent = utils.tbl_clone(parent.parent)
parent[leaf.parent.index] = leaf
table.insert(parent, leaf.parent.index, utils.tbl_clone(leaf.parent[leaf.index % 2 + 1]))
for i, l in ipairs(parent) do
l.index = i
l.parent = parent
end
end
local first, last = leaf.index, leaf.index
for i = leaf.index - 1, 1, -1 do
if parent[i].type ~= "leaf" then
break
end
first = i
end
for i = leaf.index + 1, #parent do
if parent[i].type ~= "leaf" then
break
end
last = i
end
if (first == leaf.index and last == leaf.index) -- Virtual set is empty
or (first == 1 and last == #parent) -- Virtual set is the same as the normal set
or last - first + 1 == 2 then -- Virtual sets of 2 leaves are pointless
return
end
local target = utils.tbl_clone(leaf)
local set = { target = target }
for k, v in pairs(parent) do
if type(k) ~= "number" then
set[k] = v
end
end
for i = first, last do
set[#set + 1] = parent[i]
end
set.parent = parent.parent
target.parent = set
return set
end
---@param winid integer
---@param dir Direction
function M.move_win(winid, dir)
if M.dir_move_map[dir] then
vim.cmd(string.format("%dwincmd %s", api.nvim_win_get_number(winid), M.dir_move_map[dir]))
return
end
local tree = M.get_layout_tree()
local target_leaf = M.find_leaf(tree, winid)
local outer_parent = (target_leaf.parent and target_leaf.parent.parent) or {}
-- If the target leaf has no parent, there is only one window in the layout.
if target_leaf and target_leaf.parent then
local ok, err = utils.no_win_event_call(function()
if dir == "left" or dir == "right" then
-- Horizontal
if target_leaf.parent.type == "col" then
local set = M.create_virtual_set(target_leaf)
target_leaf = (set and set.target) or target_leaf
M.col_move_out(target_leaf, target_leaf.parent, dir)
else
local next_node = M.next_node_horizontal(target_leaf, dir)
local set = M.create_virtual_set(target_leaf, true)
if next_node then
if
target_leaf.parent.type == "row"
and #target_leaf.parent == 2
and target_leaf.parent[1].type == "leaf"
and target_leaf.parent[2].type == "leaf"
then
-- Swap the windows
M.swap_leaves(target_leaf.parent[1], target_leaf.parent[2])
else
M.col_move_in(target_leaf, next_node, dir)
end
elseif set and set.type == "col" then
M.col_move_out(set.target, set, dir)
elseif outer_parent.type == "col" then
M.col_move_out(target_leaf, outer_parent, dir)
end
end
else
-- Vertical
if target_leaf.parent.type == "row" then
local set = M.create_virtual_set(target_leaf)
target_leaf = (set and set.target) or target_leaf
M.row_move_out(target_leaf, target_leaf.parent, dir)
else
local next_node = M.next_node_vertical(target_leaf, dir)
local set = M.create_virtual_set(target_leaf, true)
if next_node then
if
target_leaf.parent.type == "col"
and #target_leaf.parent == 2
and target_leaf.parent[1].type == "leaf"
and target_leaf.parent[2].type == "leaf"
then
-- Swap the windows
M.swap_leaves(target_leaf.parent[1], target_leaf.parent[2])
else
M.row_move_in(target_leaf, next_node, dir)
end
elseif set and set.type == "row" then
M.row_move_out(set.target, set, dir)
elseif outer_parent.type == "row" then
M.row_move_out(target_leaf, outer_parent, dir)
end
end
end
end)
api.nvim_set_current_win(winid)
if not ok then
utils.err(err)
utils.err(debug.traceback())
end
end
end
function M.start_move_mode()
local char, raw
local esc = utils.raw_key("<esc>")
local cur_win = api.nvim_get_current_win()
local lasthl = vim.wo[cur_win].winhl
local conf = config.get_config()
local key_dir_map = config.get_key_dir_map()
M.save_win_options(cur_win)
if conf.highlight_moving_win then
M.highlight_win(cur_win)
end
utils.set_local(cur_win, conf.moving_win_options)
vim.cmd("redraw")
local ok, err = pcall(function()
while not (char == "q" or raw == esc) do
api.nvim_echo({ { "-- WIN MOVE MODE -- press 'q' to exit", "ModeMsg" } }, false, {})
char, raw = utils.input_char(nil, { clear_prompt = false, allow_non_ascii = true })
local dir = key_dir_map[char or raw]
if dir then M.move_win(cur_win, dir) end
vim.cmd("redraw")
api.nvim_exec_autocmds({ "WinScrolled" }, { modeline = false })
vim.schedule(function() vim.cmd("redraw") end)
end
end)
utils.clear_prompt()
if conf.highlight_moving_win then
vim.wo[cur_win].winhl = lasthl
end
M.restore_win_options(cur_win)
if not ok then
utils._echo_multiline(err, "ErrorMsg")
end
end
function M.start_swap_mode()
local cur_win = api.nvim_get_current_win()
local lasthl = vim.wo[cur_win].winhl
local conf = config.get_config()
M.save_win_options(cur_win)
if conf.highlight_moving_win then
M.highlight_win(cur_win)
end
utils.set_local(cur_win, conf.moving_win_options)
vim.cmd("redraw")
local ok, err = pcall(function()
local target = conf.window_picker()
if target == -1 or target == nil then
return
end
local tree = M.get_layout_tree()
local cur_leaf = M.find_leaf(tree, cur_win)
local target_leaf = M.find_leaf(tree, target)
M.swap_leaves(cur_leaf, target_leaf)
end)
if conf.highlight_moving_win then
vim.wo[cur_win].winhl = lasthl
end
M.restore_win_options(cur_win)
if not ok then
utils._echo_multiline(err, "ErrorMsg")
end
end
function M.save_win_options(winid)
win_option_store[winid] = {}
local last_winid = api.nvim_get_current_win()
utils.no_win_event_call(function()
api.nvim_set_current_win(winid)
for option, _ in pairs(config.get_config().moving_win_options) do
local value = vim.opt_local[option]._value
if value ~= "" then
win_option_store[winid][option] = value
end
end
end)
api.nvim_set_current_win(last_winid)
end
function M.restore_win_options(winid)
for option, _ in pairs(config.get_config().moving_win_options) do
if win_option_store[winid][option] then
utils.set_local(winid, { [option] = win_option_store[winid][option] })
else
utils.unset_local(winid, option)
end
end
end
function M.highlight_win(winid)
local curhl = vim.wo[winid].winhl
local hl = {
"Normal:WinShiftNormal",
"EndOfBuffer:WinShiftEndOfBuffer",
"LineNr:WinShiftLineNr",
"CursorLineNr:WinShiftCursorLineNr",
"SignColumn:WinShiftSignColumn",
"FoldColumn:WinShiftFoldColumn",
curhl ~= "" and curhl or nil,
}
if vim.fn.has("nvim-0.6") == 1 then
hl = utils.vec_join(
hl,
{
"LineNrAbove:WinShiftLineNrAbove",
"LineNrBelow:WinShiftLineNrBelow",
}
)
end
vim.wo[winid].winhl = table.concat(hl, ",")
end
return M

View File

@ -0,0 +1,360 @@
local api = vim.api
local M = {}
---@alias vector<T> T[]
function M._echo_multiline(msg, hl, schedule)
if schedule then
vim.schedule(function()
M._echo_multiline(msg, hl, false)
end)
return
end
vim.cmd("echohl " .. (hl or "None"))
for _, line in ipairs(vim.split(msg, "\n")) do
vim.cmd(string.format('echom "%s"', vim.fn.escape(line, [["\]])))
end
vim.cmd("echohl None")
end
---@param msg string
---@param schedule? boolean Schedule the echo call.
function M.info(msg, schedule)
M._echo_multiline("[WinShift.nvim] " .. msg, "Directory", schedule)
end
---@param msg string
---@param schedule? boolean Schedule the echo call.
function M.warn(msg, schedule)
M._echo_multiline("[WinShift.nvim] " .. msg, "WarningMsg", schedule)
end
---@param msg string
---@param schedule? boolean Schedule the echo call.
function M.err(msg, schedule)
M._echo_multiline("[WinShift.nvim] " .. msg, "ErrorMsg", schedule)
end
---Call the function `f`, ignoring most of the window and buffer related
---events. The function is called in protected mode.
---@param f function
---@return boolean success
---@return any result Return value
function M.no_win_event_call(f)
local last = vim.o.eventignore
---@diagnostic disable-next-line: undefined-field
vim.opt.eventignore:prepend(
"WinEnter,WinLeave,WinNew,WinClosed,BufWinEnter,BufWinLeave,BufEnter,BufLeave"
)
local ok, err = pcall(f)
vim.opt.eventignore = last
return ok, err
end
---Escape a string for use as a pattern.
---@param s string
---@return string
function M.pattern_esc(s)
local result = string.gsub(s, "[%(|%)|%%|%[|%]|%-|%.|%?|%+|%*|%^|%$]", {
["%"] = "%%",
["-"] = "%-",
["("] = "%(",
[")"] = "%)",
["."] = "%.",
["["] = "%[",
["]"] = "%]",
["?"] = "%?",
["+"] = "%+",
["*"] = "%*",
["^"] = "%^",
["$"] = "%$",
})
return result
end
function M.tbl_clone(t)
if not t then
return
end
local clone = {}
for k, v in pairs(t) do
clone[k] = v
end
return clone
end
function M.tbl_deep_clone(t)
if not t then
return
end
local clone = {}
for k, v in pairs(t) do
if type(v) == "table" then
clone[k] = M.tbl_deep_clone(v)
else
clone[k] = v
end
end
return clone
end
function M.tbl_pack(...)
return { n = select("#", ...), ... }
end
function M.tbl_unpack(t, i, j)
return unpack(t, i or 1, j or t.n or #t)
end
function M.tbl_clear(t)
for k, _ in pairs(t) do
t[k] = nil
end
end
---Create a shallow copy of a portion of a vector.
---@param t vector
---@param first? integer First index, inclusive
---@param last? integer Last index, inclusive
---@return vector
function M.vec_slice(t, first, last)
local slice = {}
for i = first or 1, last or #t do
table.insert(slice, t[i])
end
return slice
end
---Join multiple vectors into one.
---@vararg vector
---@return vector
function M.vec_join(...)
local result = {}
local args = {...}
local n = 0
for i = 1, select("#", ...) do
if type(args[i]) ~= "nil" then
if type(args[i]) ~= "table" then
result[n + 1] = args[i]
n = n + 1
else
for j, v in ipairs(args[i]) do
result[n + j] = v
end
n = n + #args[i]
end
end
end
return result
end
---Return the first index a given object can be found in a vector, or -1 if
---it's not present.
---@param t vector
---@param v any
---@return integer
function M.vec_indexof(t, v)
for i, vt in ipairs(t) do
if vt == v then
return i
end
end
return -1
end
---Append any number of objects to the end of a vector. Pushing `nil`
---effectively does nothing.
---@param t vector
---@return vector t
function M.vec_push(t, ...)
for _, v in ipairs({...}) do
t[#t + 1] = v
end
return t
end
---Simple string templating
---Example template: "${name} is ${value}"
---@param str string Template string
---@param table table Key-value pairs to replace in the string
function M.str_template(str, table)
return (str:gsub("($%b{})", function(w)
return table[w:sub(3, -2)] or w
end))
end
function M.clear_prompt()
vim.api.nvim_echo({ { "" } }, false, {})
vim.cmd("redraw")
end
---@class InputCharSpec
---@field clear_prompt boolean (default: true)
---@field allow_non_ascii boolean (default: true)
---@field prompt_hl string (default: nil)
---@param prompt string
---@param opt InputCharSpec
---@return string Char
---@return string Raw
function M.input_char(prompt, opt)
opt = vim.tbl_extend("keep", opt or {}, {
clear_prompt = true,
allow_non_ascii = false,
prompt_hl = nil,
})
if prompt then
vim.api.nvim_echo({ { prompt, opt.prompt_hl } }, false, {})
end
local c
if not opt.allow_non_ascii then
while type(c) ~= "number" do
c = vim.fn.getchar()
end
else
c = vim.fn.getchar()
end
if opt.clear_prompt then
M.clear_prompt()
end
local s = type(c) == "number" and vim.fn.nr2char(c) or nil
local raw = type(c) == "number" and s or c
---@diagnostic disable-next-line: return-type-mismatch
return s, raw
end
function M.input(prompt, default, completion)
local v = vim.fn.input({
prompt = prompt,
default = default,
completion = completion,
cancelreturn = "__INPUT_CANCELLED__",
})
M.clear_prompt()
return v
end
function M.raw_key(vim_key)
return api.nvim_eval(string.format([["\%s"]], vim_key))
end
function M.pause(msg)
vim.cmd("redraw")
M.input_char(
"-- PRESS ANY KEY TO CONTINUE -- " .. (msg or ""),
{ allow_non_ascii = true, prompt_hl = "Directory" }
)
end
---Map of options that accept comma separated, list-like values, but don't work
---correctly with Option:set(), Option:append(), Option:prepend(), and
---Option:remove() (seemingly for legacy reasons).
---WARN: This map is incomplete!
local list_like_options = {
winhighlight = true,
listchars = true,
fillchars = true,
}
---@class utils.set_local.Opt
---@field method '"set"'|'"remove"'|'"append"'|'"prepend"' Assignment method. (default: "set")
---@class utils.set_local.ListSpec : string[]
---@field opt utils.set_local.Opt
---@alias WindowOptions table<string, boolean|integer|string|utils.set_local.ListSpec>
---@param winids number[]|number Either a list of winids, or a single winid (0 for current window).
---@param option_map WindowOptions
---@param opt? utils.set_local.Opt
function M.set_local(winids, option_map, opt)
if type(winids) ~= "table" then
winids = { winids }
end
opt = vim.tbl_extend("keep", opt or {}, { method = "set" }) --[[@as table ]]
for _, id in ipairs(winids) do
api.nvim_win_call(id, function()
for option, value in pairs(option_map) do
local o = opt
local fullname = api.nvim_get_option_info(option).name
local is_list_like = list_like_options[fullname]
local cur_value = vim.o[fullname]
if type(value) == "table" then
if value.opt then
o = vim.tbl_extend("force", opt, value.opt) --[[@as table ]]
end
if is_list_like then
value = table.concat(value, ",")
end
end
if o.method == "set" then
vim.opt_local[option] = value
else
if o.method == "remove" then
if is_list_like then
vim.opt_local[fullname] = cur_value:gsub(",?" .. vim.pesc(value), "")
else
vim.opt_local[fullname]:remove(value)
end
elseif o.method == "append" then
if is_list_like then
vim.opt_local[fullname] = ("%s%s"):format(
cur_value ~= "" and cur_value .. "," or "",
value
)
else
vim.opt_local[fullname]:append(value)
end
elseif o.method == "prepend" then
if is_list_like then
vim.opt_local[fullname] = ("%s%s%s"):format(
value,
cur_value ~= "" and "," or "",
cur_value
)
else
vim.opt_local[fullname]:prepend(value)
end
end
end
end
end)
end
end
---@param winids number[]|number Either a list of winids, or a single winid (0 for current window).
---@param option string
function M.unset_local(winids, option)
if type(winids) ~= "table" then
winids = { winids }
end
for _, id in ipairs(winids) do
api.nvim_win_call(id, function()
vim.opt_local[option] = nil
end)
end
end
return M