Skip to content

Commit e345ad8

Browse files
authored
fix(renderer): preserve visual selection position post-render (#1866)
1 parent e10eed8 commit e345ad8

File tree

5 files changed

+116
-27
lines changed

5 files changed

+116
-27
lines changed

.github/workflows/luals-check.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ jobs:
99
luals-check:
1010
strategy:
1111
matrix:
12-
neovim: ["0.10"]
12+
neovim: ["0.11"]
1313
lua: ["5.1", "luajit-master"]
1414
runs-on: ubuntu-latest
1515

lua/neo-tree/sources/document_symbols/lib/symbols_utils.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ end
128128

129129
---Callback function for lsp request
130130
---@param lsp_resp table<integer, neotree.lsp.RespRaw> the response of the lsp clients
131-
---@param state neotree.State the state of the source
131+
---@param state neotree.StateWithTree the state of the source
132132
local on_lsp_resp = function(lsp_resp, state)
133133
if lsp_resp == nil or type(lsp_resp) ~= "table" then
134134
return

lua/neo-tree/sources/git_status/lib/items.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ local M = {}
77

88
---Get a table of all open buffers, along with all parent paths of those buffers.
99
---The paths are the keys of the table, and all the values are 'true'.
10-
---@param state neotree.State
10+
---@param state neotree.StateWithTree
1111
M.get_git_status = function(state)
1212
if state.loading then
1313
return

lua/neo-tree/sources/manager.lua

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,6 @@ end
5151

5252
---@alias neotree.Internal.SortFieldProvider fun(node: NuiTree.Node):any
5353

54-
---@class neotree.State.Position
55-
---@field topline integer?
56-
---@field lnum integer?
57-
---@field node_id string?
58-
5954
---@class neotree.State : neotree.Config.Source
6055
---@field name string
6156
---@field tabid integer

lua/neo-tree/ui/renderer.lua

Lines changed: 113 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,6 @@ local tabid_to_tabnr = function(tabid)
3232
return vim.api.nvim_tabpage_is_valid(tabid) and vim.api.nvim_tabpage_get_number(tabid)
3333
end
3434

35-
local buffer_is_usable = function(bufnr)
36-
return vim.api.nvim_buf_is_valid(bufnr) and vim.api.nvim_buf_is_loaded(bufnr)
37-
end
38-
3935
local cleaned_up = false
4036
---Clean up invalid neotree buffers (e.g after a session restore)
4137
---@param force boolean if true, force cleanup. Otherwise only cleanup once
@@ -75,6 +71,7 @@ local start_resize_monitor = function()
7571
local windows_exist = false
7672
local success, err = pcall(manager._for_each_state, nil, function(state)
7773
if state.win_width and M.tree_is_visible(state) then
74+
---@cast state neotree.StateWithTree
7875
windows_exist = true
7976
local current_size = utils.get_inner_win_width(state.winid)
8077
if current_size ~= state.win_width then
@@ -658,6 +655,39 @@ end
658655
---Functions to save and restore the focused node.
659656
M.position = {}
660657

658+
local visual_modes = {
659+
"v",
660+
"V",
661+
utils.keycode("<C-v>"),
662+
}
663+
664+
---@param a [integer, integer, integer, integer]
665+
---@param b [integer, integer, integer, integer]
666+
local position_sorter = function(a, b)
667+
if a[2] == b[2] then
668+
return a[3] < b[3]
669+
else
670+
return a[2] < b[2]
671+
end
672+
end
673+
---@generic T
674+
---@param positions T
675+
---@return T positions
676+
local sort_positions = function(positions)
677+
table.sort(positions, position_sorter)
678+
return positions
679+
end
680+
681+
---@class neotree.State.Position.VisualSelection
682+
---@field [1] [integer, integer, integer, integer]
683+
---@field [2] [integer, integer, integer, integer]
684+
685+
---@class neotree.State.Position
686+
---@field topline integer?
687+
---@field lnum integer?
688+
---@field node_id string?
689+
---@field visual_selection neotree.State.Position.VisualSelection?
690+
661691
---Saves a window position to be restored later
662692
---@param state neotree.State
663693
---@param force boolean?
@@ -666,12 +696,25 @@ M.position.save = function(state, force)
666696
log.debug("There's already a position saved to be restored. Cannot save another.")
667697
return
668698
end
669-
if state.tree and M.window_exists(state) then
670-
local win_state = vim.api.nvim_win_call(state.winid, vim.fn.winsaveview)
671-
state.position.topline = win_state.topline
672-
state.position.lnum = win_state.lnum
673-
log.debug("Saved cursor position with lnum: " .. state.position.lnum)
674-
log.debug("Saved window position with topline: " .. state.position.topline)
699+
if not state.tree then
700+
return
701+
end
702+
if not M.window_exists(state) then
703+
return
704+
end
705+
706+
local win_state = vim.api.nvim_win_call(state.winid, vim.fn.winsaveview)
707+
state.position.topline = win_state.topline
708+
state.position.lnum = win_state.lnum
709+
log.debug("Saved cursor position with lnum:", state.position.lnum)
710+
log.debug("Saved window position with topline:", state.position.topline)
711+
712+
-- Save last visual selection in the neo-tree buffer
713+
local curbuf = vim.api.nvim_get_current_buf()
714+
if state.bufnr == curbuf and vim.tbl_contains(visual_modes, vim.api.nvim_get_mode().mode) then
715+
local a = vim.fn.getpos(".")
716+
local b = vim.fn.getpos("v")
717+
state.position.visual_selection = { a, b }
675718
end
676719
end
677720

@@ -716,9 +759,25 @@ M.position.restore = function(state)
716759
M.focus_node(state, state.position.node_id, true)
717760
end
718761

762+
M.position.restore_selection(state)
763+
719764
M.position.clear(state)
720765
end
721766

767+
---@param state neotree.State
768+
M.position.restore_selection = function(state)
769+
if state.winid ~= vim.api.nvim_get_current_win() then
770+
return
771+
end
772+
if not state.position.visual_selection then
773+
return
774+
end
775+
-- assertion unneeded but lua-ls isn't narrowing properly
776+
local selection = assert(sort_positions(state.position.visual_selection))
777+
vim.fn.setpos([['<]], selection[1])
778+
vim.fn.setpos([['>]], selection[2])
779+
end
780+
722781
---Redraw the tree without reloading from the source.
723782
---@param state neotree.State State of the tree.
724783
M.redraw = function(state)
@@ -804,9 +863,10 @@ M.set_expanded_nodes = function(tree, expanded_nodes)
804863
end
805864
end
806865

866+
---@param state neotree.State
807867
create_tree = function(state)
808868
if state.tree and state.tree.bufnr == state.bufnr then
809-
if buffer_is_usable(state.tree.bufnr) then
869+
if vim.api.nvim_buf_is_loaded(state.tree.bufnr) then
810870
log.debug("Tree already exists and buffer is valid, skipping creation", state.name, state.id)
811871
state.tree.winid = state.winid
812872
return
@@ -825,17 +885,18 @@ create_tree = function(state)
825885
})
826886
end
827887

888+
---@param state neotree.StateWithTree
828889
---@return NuiTree.Node[]?
829890
local get_selected_nodes = function(state)
830891
if state.winid ~= vim.api.nvim_get_current_win() then
831892
return nil
832893
end
833-
local start_pos = vim.fn.getpos("'<")[2]
834-
local end_pos = vim.fn.getpos("'>")[2]
835-
if end_pos < start_pos then
836-
-- I'm not sure if this could actually happen, but just in case
837-
start_pos, end_pos = end_pos, start_pos
894+
if not state.position.visual_selection then
895+
return nil
838896
end
897+
sort_positions(state.position.visual_selection)
898+
local start_pos = state.position.visual_selection[1][2]
899+
local end_pos = state.position.visual_selection[2][2]
839900
local selected_nodes = {}
840901
while start_pos <= end_pos do
841902
local node = state.tree:get_node(start_pos)
@@ -1103,14 +1164,17 @@ M.acquire_window = function(state)
11031164
vim.api.nvim_buf_set_name(state.bufnr, bufname)
11041165
vim.api.nvim_set_current_win(state.winid)
11051166
-- Used to track the position of the cursor within the tree as it gains and loses focus
1106-
win:on({ "CursorMoved" }, function()
1167+
win:on({ "CursorMoved", "ModeChanged" }, function()
11071168
if win.winid == vim.api.nvim_get_current_win() then
11081169
M.position.save(state, true)
11091170
end
11101171
end)
11111172
win:on({ "BufDelete" }, function()
11121173
M.position.save(state)
11131174
end)
1175+
win:on({ "WinEnter" }, function()
1176+
M.position.restore_selection(state)
1177+
end)
11141178
win:on({ "BufDelete" }, function()
11151179
vim.schedule(function()
11161180
win:unmount()
@@ -1195,13 +1259,40 @@ M.tree_is_visible = function(state)
11951259
return M.window_exists(state) and vim.api.nvim_win_get_buf(state.winid) == state.bufnr
11961260
end
11971261

1262+
---@alias neotree.renderer.MarkedNodes table<vim.fn.getmarklist.ret.item, NuiTree.Node>
1263+
1264+
---@param state neotree.StateWithTree
1265+
---@return neotree.renderer.MarkedNodes
1266+
local save_marks = function(state)
1267+
local marks = vim.fn.getmarklist(state.bufnr)
1268+
local marked_nodes = {}
1269+
1270+
for _, mark in ipairs(marks) do
1271+
local node = state.tree:get_node(mark[2])
1272+
if node then
1273+
marked_nodes[mark] = node
1274+
end
1275+
end
1276+
return marked_nodes
1277+
end
1278+
1279+
---@param state neotree.StateWithTree
1280+
---@param marks neotree.renderer.MarkedNodes
1281+
local restore_marks = function(state, marks)
1282+
for mark, node in pairs(marks) do
1283+
vim.fn.setpos(mark.mark, mark.pos)
1284+
end
1285+
end
1286+
11981287
---Renders the given tree and expands window width if needed
1199-
---@param state neotree.State The state containing tree to render. Almost same as state.tree:render()
1288+
---@param state neotree.StateWithTree The state containing tree to render. Almost same as state.tree:render()
12001289
render_tree = function(state)
12011290
local add_blank_line_at_top = nt.config.add_blank_line_at_top
12021291
local should_auto_expand = state.window.auto_expand_width and state.current_position ~= "float"
12031292
local should_pre_render = should_auto_expand or state.current_position == "current"
12041293

1294+
local marks = save_marks(state)
1295+
12051296
if should_pre_render and M.tree_is_visible(state) then
12061297
log.trace("pre-rendering tree")
12071298
state._in_pre_render = true
@@ -1220,6 +1311,7 @@ render_tree = function(state)
12201311
state.win_width = desired_width
12211312
end
12221313
end
1314+
12231315
if M.tree_is_visible(state) then
12241316
if add_blank_line_at_top then
12251317
state.tree:render(2)
@@ -1230,6 +1322,7 @@ render_tree = function(state)
12301322

12311323
log.debug("render_tree: Restoring position")
12321324
M.position.restore(state)
1325+
restore_marks(state, marks)
12331326
end
12341327

12351328
---Draws the given nodes on the screen.
@@ -1263,6 +1356,7 @@ draw = function(nodes, state, parent_id)
12631356
M.acquire_window(state)
12641357
create_tree(state)
12651358
end
1359+
---@cast state neotree.StateWithTree
12661360

12671361
-- draw the given nodes
12681362
local success, msg = pcall(state.tree.set_nodes, state.tree, nodes, parent_id)
@@ -1313,7 +1407,7 @@ end
13131407

13141408
---Shows the given items as a tree.
13151409
---@param sourceItems table? The list of items to transform.
1316-
---@param state neotree.State The current state of the plugin.
1410+
---@param state neotree.StateWithTree The current state of the plugin.
13171411
---@param parentId string? The id of the parent node to display these nodes at
13181412
---@param callback function? The id of the parent node to display these nodes at
13191413
M.show_nodes = function(sourceItems, state, parentId, callback)

0 commit comments

Comments
 (0)