aboutsummaryrefslogtreecommitdiff
path: root/dot_config/herdr/plugins/github/herdr-splits-d880c4924f2b/lua/herdr-splits/nav.lua
blob: 56ecca37040cae81738e7dc8c44e8e705d7f744c (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
---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