Skip to content

Commit

Permalink
(mini.misc) FEATURE: implement setup_restore_cursor().
Browse files Browse the repository at this point in the history
Co-authored-by: Evgeni Chasnovski <evgeni.chasnovski@gmail.com>
  • Loading branch information
cryptomilk and echasnovski committed Jan 31, 2023
1 parent e742f41 commit 5be4676
Show file tree
Hide file tree
Showing 6 changed files with 255 additions and 1 deletion.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Introduction of a new module.
## mini.misc

- FEATURE: Add `MiniMisc.setup_auto_root()` and `MiniMisc.find_root()` for root finding functionality. NOTE: requires Neovim>=0.8.
- FEATURE: Add `MiniMisc.setup_restore_cursor()` for automatically restoring latest cursor position on file reopen. By @cryptomilk, PR #198.

## mini.move

Expand Down
24 changes: 24 additions & 0 deletions doc/mini-misc.txt
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,30 @@ Parameters~
identify a root directory. Forwarded to |vim.fs.find()|.
Default: `{ '.git', 'Makefile' }`.

------------------------------------------------------------------------------
*MiniMisc.setup_restore_cursor()*
`MiniMisc.setup_restore_cursor`({opts})
Restore cursor position on file open

When reopening a file this will make sure the cursor is placed back to the
position where you left before. This implements |restore-cursor| in a nicer way.
File should have a recognized file type (see 'filetype') and be opened in
a normal buffer (see 'buftype').

Note: it relies on file mark data stored in 'shadafile' (see |shada-f|).
Be sure to enable it.

Parameters~
{opts} `(table|nil)` Options for |MiniMisc.restore_cursor|. Possible fields:
- <center> - (boolean) Center the window after we restored the cursor.
Default: `true`.
- <ignore_filetype> - Array with file types to be ignored (see 'filetype').
Default: `{ "gitcommit", "gitrebase" }`.

Usage~
>
require('mini.misc').setup_restore_cursor()
------------------------------------------------------------------------------
*MiniMisc.stat_summary()*
`MiniMisc.stat_summary`({t})
Expand Down
62 changes: 62 additions & 0 deletions lua/mini/misc.lua
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,68 @@ end

H.root_cache = {}

--- Restore cursor position on file open
---
--- When reopening a file this will make sure the cursor is placed back to the
--- position where you left before. This implements |restore-cursor| in a nicer way.
--- File should have a recognized file type (see 'filetype') and be opened in
--- a normal buffer (see 'buftype').
---
--- Note: it relies on file mark data stored in 'shadafile' (see |shada-f|).
--- Be sure to enable it.
---
---@param opts table|nil Options for |MiniMisc.restore_cursor|. Possible fields:
--- - <center> - (boolean) Center the window after we restored the cursor.
--- Default: `true`.
--- - <ignore_filetype> - Array with file types to be ignored (see 'filetype').
--- Default: `{ "gitcommit", "gitrebase" }`.
---
---@usage >
--- require('mini.misc').setup_restore_cursor()
MiniMisc.setup_restore_cursor = function(opts)
opts = opts or {}

opts.ignore_filetype = opts.ignore_filetype or { 'gitcommit', 'gitrebase' }
if not H.is_array_of(opts.ignore_filetype, H.is_string) then
H.error('In `setup_restore_cursor()` `opts.ignore_filetype` should be an array of strings.')
end

if opts.center == nil then opts.center = true end
if type(opts.center) ~= 'boolean' then H.error('In `setup_restore_cursor()` `opts.center` should be a boolean.') end

-- TODO: use `nvim_create_autocmd()` after Neovim<=0.6 support is dropped
local au_command = string.format(
[[augroup MiniMiscRestoreCursor
au!
au BufReadPre * au FileType <buffer> ++once lua require('mini.misc').restore_cursor(%s)
augroup END]],
vim.inspect(opts, { newline = ' ', indent = '' })
)
vim.api.nvim_exec(au_command, false)
end

-- TODO: Make local once Lua autocmd is used inside `setup_restore_cursor()`
MiniMisc.restore_cursor = function(opts)
-- Stop if not a normal buffer
if vim.bo.buftype ~= '' then return end

-- Stop if filetype is ignored
if vim.tbl_contains(opts.ignore_filetype, vim.bo.filetype) then return end

-- Stop if line is already specified (like during start with `nvim file +num`)
if vim.fn.line('.') > 1 then return end

-- Stop if can't restore proper line for some reason
local last_line = vim.fn.line([['"]])
if not (1 <= last_line and last_line <= vim.fn.line('$')) then return end

-- Restore cursor and open just enough folds
vim.cmd([[normal! g`"zv]])

-- Center window
if opts.center then vim.cmd('normal! zz') end
end

--- Compute summary statistics of numerical array
---
--- This might be useful to compute summary of time benchmarking with
Expand Down
21 changes: 21 additions & 0 deletions tests/dir-misc/init-restore-cursor.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
dofile('scripts/minimal_init.lua')

-- Enable reading correct shada file
vim.o.shadafile = 'tests/dir-misc/restore-cursor.shada'

-- Set testable size
vim.o.lines = 10
vim.o.columns = 20

-- Set up tested functionality based on test type
local test_type = vim.env.RESTORE_CURSOR_TEST_TYPE

if test_type == 'set-not-normal-buftype' then vim.cmd('au BufReadPost * set buftype=help') end
if test_type == 'set-position' then vim.cmd('au BufReadPost * call cursor(4, 0)') end
if test_type == 'make-folds' then vim.cmd('au BufReadPost * 2,3 fold | 9,10 fold') end

local opts = {}
if test_type == 'not-center' then opts = { center = false } end
if test_type == 'ignore-lua' then opts = { ignore_filetype = { 'lua' } } end

require('mini.misc').setup_restore_cursor(opts)
10 changes: 10 additions & 0 deletions tests/dir-misc/restore-cursor.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
-- aaa
-- aaa
-- aaa
-- aaa
-- aaa
-- aaa
-- aaa
-- aaa
-- aaa
-- aaa
138 changes: 137 additions & 1 deletion tests/test_misc.lua
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ local new_set = MiniTest.new_set

local path_sep = package.config:sub(1, 1)
local project_root = vim.fn.fnamemodify(vim.fn.getcwd(), ':p')
local dir_misc_path = project_root .. 'tests/dir-misc/'

-- Helpers with child processes
--stylua: ignore start
Expand All @@ -17,6 +18,9 @@ local get_lines = function(...) return child.get_lines(...) end
local make_path = function(...) return table.concat({...}, path_sep):gsub(path_sep .. path_sep, path_sep) end
local make_abspath = function(...) return make_path(project_root, ...) end
local getcwd = function() return child.fn.fnamemodify(child.fn.getcwd(), ':p') end
local set_cursor = function(...) return child.set_cursor(...) end
local get_cursor = function(...) return child.get_cursor(...) end
local edit = function(x) child.cmd('edit ' .. x) end
--stylua: ignore end

-- Output test set ============================================================
Expand Down Expand Up @@ -230,7 +234,6 @@ T['resize_window()']['correctly computes default `text_width` argument'] = funct
eq(child.api.nvim_win_get_width(0), 40 + 4)
end

local dir_misc_path = make_abspath('tests/dir-misc/')
local git_repo_path = make_abspath('tests/dir-misc/mocked-git-repo/')
local git_path = make_abspath('tests/dir-misc/mocked-git-repo/.git')
local test_file_makefile = make_abspath('tests/dir-misc/aaa.lua')
Expand Down Expand Up @@ -417,6 +420,139 @@ T['find_root()']['uses cache'] = function()
eq(find_root(), dir_misc_path)
end

local restore_cursor_test_file = make_path(dir_misc_path, 'restore-cursor.lua')
local restore_cursor_init_file = make_path(dir_misc_path, 'init-restore-cursor.lua')
local restore_cursor_shada_path = make_path(dir_misc_path, 'restore-cursor.shada')

local cursor_set_test_type = function(x)
vim.env.RESTORE_CURSOR_TEST_TYPE = x
MiniTest.finally(function() vim.env.RESTORE_CURSOR_TEST_TYPE = '' end)
end

T['setup_restore_cursor()'] = new_set({
hooks = {
pre_case = function()
-- Ensure that shada file is correctly set
child.o.shadafile = restore_cursor_shada_path
end,
post_case = function()
-- Don't save new shada file on child stop
child.o.shadafile = ''

-- Clean up
child.fn.delete(restore_cursor_shada_path)
end,
},
})

T['setup_restore_cursor()']['works'] = function()
edit(restore_cursor_test_file)
set_cursor(10, 3)
child.cmd('wshada!')

child.restart({ '-u', restore_cursor_init_file, '--', restore_cursor_test_file })

eq(get_cursor(), { 10, 3 })
-- Should center by default
eq(child.fn.line('w0'), 7)
end

T['setup_restore_cursor()']['validates input'] = function()
local setup_restore_cursor = function(...) child.lua('MiniMisc.setup_restore_cursor(...)', { ... }) end

expect.error(setup_restore_cursor, '`opts.center`.*boolean', { center = 1 })
expect.error(setup_restore_cursor, '`opts.ignore_filetype`.*array', { ignore_filetype = 1 })
end

T['setup_restore_cursor()']['respects `opts.center`'] = function()
edit(restore_cursor_test_file)
set_cursor(10, 3)
child.cmd('wshada!')

cursor_set_test_type('not-center')
child.restart({ '-u', restore_cursor_init_file, '--', restore_cursor_test_file })

eq(get_cursor(), { 10, 3 })
-- Should not center line
eq(child.fn.line('w$'), 10)
end

T['setup_restore_cursor()']['respects `opts.ignore_filetype`'] = function()
edit(restore_cursor_test_file)
set_cursor(10, 3)
child.cmd('wshada!')

cursor_set_test_type('ignore-lua')
child.restart({ '-u', restore_cursor_init_file, '--', restore_cursor_test_file })

eq(get_cursor(), { 1, 0 })
end

T['setup_restore_cursor()']['restores only in normal buffer'] = function()
edit(restore_cursor_test_file)
set_cursor(10, 3)
child.cmd('wshada!')

cursor_set_test_type('set-not-normal-buftype')
child.restart({ '-u', restore_cursor_init_file, '--', restore_cursor_test_file })

eq(get_cursor(), { 1, 0 })
end

T['setup_restore_cursor()']['does not restore if position is already set'] = function()
edit(restore_cursor_test_file)
set_cursor(10, 3)
child.cmd('wshada!')

cursor_set_test_type('set-position')
child.restart({ '-u', restore_cursor_init_file, '--', restore_cursor_test_file })

eq(get_cursor(), { 4, 0 })

-- Double check that `setup_restore_cursor()` was run
expect.match(child.cmd_capture('au MiniMiscRestoreCursor'), 'BufRead')
end

T['setup_restore_cursor()']['does not restore if position is outdated'] = function()
edit(restore_cursor_test_file)

-- Ensure that file content won't change even on test case error
local true_lines = get_lines()
MiniTest.finally(function() vim.fn.writefile(true_lines, restore_cursor_test_file) end)

set_cursor(10, 3)
child.cmd('wshada!')
child.cmd('bwipeout')

-- Modify file so that position will appear outdated
child.fn.writefile({ '-- bbb', '-- bbb' }, restore_cursor_test_file)

child.restart({ '-u', restore_cursor_init_file, '--', restore_cursor_test_file })

eq(get_cursor(), { 1, 0 })

-- Double check that `setup_restore_cursor()` was run
expect.match(child.cmd_capture('au MiniMiscRestoreCursor'), 'BufRead')
end

T['setup_restore_cursor()']['opens just enough folds'] = function()
edit(restore_cursor_test_file)
set_cursor(10, 3)
child.cmd('wshada!')

cursor_set_test_type('make-folds')
child.restart({ '-u', restore_cursor_init_file, '--', restore_cursor_test_file })

-- Should open only needed folds
eq(get_cursor(), { 10, 3 })

eq({ child.fn.foldclosed(2), child.fn.foldclosed(3) }, { 2, 2 })
eq({ child.fn.foldclosed(9), child.fn.foldclosed(10) }, { -1, -1 })

-- Double check that `setup_restore_cursor()` was run
expect.match(child.cmd_capture('au MiniMiscRestoreCursor'), 'BufRead')
end

local stat_summary = function(...) return child.lua_get('MiniMisc.stat_summary({ ... })', { ... }) end

T['stat_summary()'] = new_set()
Expand Down

0 comments on commit 5be4676

Please sign in to comment.