Files
dotfiles/dot_config/herdr/plugins/github/herdr-splits-d880c4924f2b/lua/herdr-splits/nav.lua
2026-07-29 08:36:58 +02:00

207 lines
7.1 KiB
Lua

---Navigation logic: seamless movement between Neovim splits and Herdr panes.
---@class HerdrSplitsNav
local M = {}
local config = require('herdr-splits.config')
local herdr = require('herdr-splits.herdr')
local win = require('herdr-splits.win')
---Treat the current window as a sidebar that should not be navigated through.
---Combines the configured ignore lists with the embedded-float heuristic.
---@return boolean
local function is_sidebar()
return win.is_ignored_or_preview() or win.is_embedded_floating_window()
end
---Split a new Neovim window using the user's placement preferences.
---@param direction '"left"'|'"right"'|'"up"'|'"down"'
local function split_edge(direction)
if direction == 'left' or direction == 'right' then
vim.cmd('vsp')
else
vim.cmd('sp')
end
end
---Move cursor between Neovim splits, falling through to Herdr at edges.
---This is the core navigation function.
---@param direction '"left"'|'"right"'|'"up"'|'"down"'
---@param opts table|nil { same_row: boolean|nil, at_edge: string|function|nil }
function M.move_cursor(direction, opts)
local same_row = config.move_cursor_same_row
local at_edge_behavior = config.at_edge
if type(opts) == 'table' then
if opts.same_row ~= nil then
same_row = opts.same_row
end
if opts.at_edge ~= nil then
at_edge_behavior = opts.at_edge
end
end
-- Handle floating windows: just forward to Herdr,
-- EXCEPT for embedded sidebars (snacks/neo-tree float/aerial float):
-- those should stay in Neovim so the user can keep interacting
-- with the picker.
if win.is_floating() and not win.is_embedded_floating_window() then
herdr.focus_pane(direction)
return
end
local dir_key = win.dir_keys[direction]
local offset = vim.fn.winline() + vim.api.nvim_win_get_position(0)[1]
-- Save current window to detect if wincmd changes it
local prev_win = vim.api.nvim_get_current_win()
-- Try moving within Neovim first
local will_wrap = false
local count = vim.v.count1
local target_winnr = vim.fn.winnr(count .. dir_key)
if count > 1 then
local prev_winnr = vim.fn.winnr((count - 1) .. dir_key)
will_wrap = target_winnr == prev_winnr
else
will_wrap = target_winnr == vim.fn.winnr()
end
-- Command-line window (q:, q/, q?): Neovim forbids all window commands
-- (E11). Never wincmd; at a Neovim screen edge, delegate to Herdr
-- (subprocess-safe, does not close the cmdwin); otherwise silent no-op.
-- Mirrors smart-splits.nvim PR #464.
if win.is_command_line_window() then
if will_wrap and herdr.is_in_session() then
local at_herdr_edge = herdr.current_pane_at_edge(direction)
if at_herdr_edge == false then
herdr.focus_pane(direction)
elseif at_herdr_edge == true and herdr.nav_at_edge() ~= 'stop' then
herdr.focus_pane(win.reverse_direction[direction])
end
end
return
end
-- Execute the wincmd
if will_wrap and count == 1 then
vim.cmd('wincmd ' .. dir_key)
else
vim.cmd(count .. 'wincmd ' .. dir_key)
end
if vim.api.nvim_get_current_win() ~= prev_win then
-- Moved within Neovim. Restore same-row if configured.
if (direction == 'left' or direction == 'right') and same_row then
local row = offset - vim.api.nvim_win_get_position(0)[1]
if row > 0 then
vim.cmd('normal! ' .. row .. 'H')
end
end
return
end
-- We're at a Neovim edge. Try to cross into Herdr.
if not herdr.is_in_session() then
if will_wrap and count == 1 then
local sidebar = is_sidebar()
if type(at_edge_behavior) == 'function' then
at_edge_behavior({
direction = direction,
split = function() split_edge(direction) end,
is_sidebar = sidebar,
wrap = function()
vim.cmd('wincmd ' .. win.dir_keys_reverse[direction])
end,
})
elseif at_edge_behavior == 'stop' then
return
elseif at_edge_behavior == 'split' then
if not sidebar then
split_edge(direction)
end
else -- 'wrap' (default)
-- Wrap is allowed even from filetype-based sidebars (dbui, neo-tree,
-- ...) so the user can leave them; only embedded floats stay gated.
if not win.is_embedded_floating_window() then
vim.cmd('wincmd ' .. win.dir_keys_reverse[direction])
end
end
end
return
end
-- Check zoom state: unzoom first, then retry Neovim navigation.
-- Must happen BEFORE the at_herdr_edge check — when zoomed, the pane fills
-- the screen so herdr reports it as being at every edge, making
-- at_herdr_edge useless until we unzoom.
if herdr.unzoom_enabled() and herdr.current_pane_is_zoomed() then
herdr.unzoom()
-- Retry wincmd — other Neovim splits may now be visible
vim.cmd('wincmd ' .. dir_key)
if vim.api.nvim_get_current_win() ~= prev_win then
if (direction == 'left' or direction == 'right') and same_row then
local row = offset - vim.api.nvim_win_get_position(0)[1]
if row > 0 then
vim.cmd('normal! ' .. row .. 'H')
end
end
return
end
-- Still at edge after unzoom; fall through to Herdr edge check.
end
-- Check if we're at the Herdr edge too
local at_herdr_edge = herdr.current_pane_at_edge(direction)
if at_herdr_edge == nil then
if will_wrap and count == 1 and not win.is_embedded_floating_window() then
vim.cmd('wincmd ' .. win.dir_keys_reverse[direction])
end
return
end
if not at_herdr_edge then
-- There's a Herdr pane in this direction. Cross the boundary.
local moved = herdr.focus_pane(direction)
if not moved and will_wrap and count == 1 and not win.is_embedded_floating_window() then
vim.cmd('wincmd ' .. win.dir_keys_reverse[direction])
end
return
end
-- At both Neovim AND Herdr edges (no herdr pane to cross into).
-- Apply at_edge behavior. (Any needed unzoom already happened above.)
if type(at_edge_behavior) == 'function' then
at_edge_behavior({
direction = direction,
split = function() split_edge(direction) end,
is_sidebar = is_sidebar(),
wrap = function()
vim.cmd('wincmd ' .. win.dir_keys_reverse[direction])
end,
})
elseif at_edge_behavior == 'stop' then
return
elseif at_edge_behavior == 'split' then
if not is_sidebar() then
split_edge(direction)
end
else -- 'wrap' (default)
-- Wrap is allowed from filetype-based sidebars (dbui, neo-tree, ...) so
-- you can leave them at an edge; only embedded floating overlays stay gated.
if will_wrap and count == 1 and not win.is_embedded_floating_window() then
-- Wrap to the opposite side. If a Herdr pane exists there AND nav_at_edge
-- allows wrap-across-boundary (the default), cross into it; otherwise
-- wrap within Neovim. nav_at_edge=stop keeps the wrap inside Neovim.
if herdr.nav_at_edge() ~= 'stop'
and herdr.current_pane_at_edge(win.reverse_direction[direction]) == false
then
herdr.focus_pane(win.reverse_direction[direction])
else
vim.cmd('wincmd ' .. win.dir_keys_reverse[direction])
end
end
end
end
return M