From 00eba9cc9d74c459a370e44e49f801aac810abb0 Mon Sep 17 00:00:00 2001 From: Rishikesh Vaishnav Date: Thu, 22 Jul 2021 08:21:57 -0700 Subject: [PATCH] Implement infoview autoclose. --- README.rst | 2 + lua/lean/infoview.lua | 92 +++++++---- lua/lean/init.lua | 4 +- lua/tests/helpers.lua | 5 +- lua/tests/infoview/autoclose_spec.lua | 220 ++++++++++++++++++++++++++ lua/tests/infoview/lsp_spec.lua | 4 +- 6 files changed, 293 insertions(+), 34 deletions(-) create mode 100644 lua/tests/infoview/autoclose_spec.lua diff --git a/README.rst b/README.rst index e8a6f98e..a6b823e0 100644 --- a/README.rst +++ b/README.rst @@ -187,6 +187,8 @@ In e.g. your ``init.lua``: enable = true, -- Automatically open an infoview on entering a Lean buffer? autoopen = true, + -- Automatically close and infoview when leaving from its last associated Lean file? + autoclose = true, -- Set the infoview windows' widths width = 50, }, diff --git a/lua/lean/infoview.lua b/lua/lean/infoview.lua index f930f5d1..45314ca1 100644 --- a/lua/lean/infoview.lua +++ b/lua/lean/infoview.lua @@ -5,7 +5,7 @@ local is_lean_buffer = require('lean').is_lean_buffer local set_augroup = require('lean._util').set_augroup local infoview = { _by_id = {} } -local options = { _DEFAULTS = { autoopen = true, width = 50 } } +local options = { _DEFAULTS = { autoopen = true, width = 50, autoclose = true} } local _DEFAULT_BUF_OPTIONS = { bufhidden = 'wipe', @@ -41,6 +41,16 @@ end ---@param open boolean: whether to open the infoview after initializing function Infoview:new(id, width, open) local new_infoview = {id = id, width = width} + + -- a set with a counter method + new_infoview.lean_windows = {} + setmetatable(new_infoview.lean_windows, { __index = { + get_count = function() + local count = 0 + for _, _ in pairs(new_infoview.lean_windows) do count = count + 1 end + return count + end}}) + self.__index = self setmetatable(new_infoview, self) @@ -69,7 +79,7 @@ function Infoview:open() end -- Make sure we notice even if someone manually :q's the infoview window. set_augroup("LeanInfoviewClose", string.format([[ - autocmd WinClosed lua require'lean.infoview'.__was_closed(%d) + autocmd WinClosed lua require'lean.infoview'._by_id[%d]:close() ]], self.id), 0) vim.api.nvim_set_current_win(window_before_split) @@ -109,13 +119,20 @@ end function Infoview:focus_on_current_buffer() if not is_lean_buffer() then return end if self.is_open then - set_augroup("LeanInfoviewUpdate", [[ - autocmd CursorHold lua require'lean.infoview'.__update() - autocmd CursorHoldI lua require'lean.infoview'.__update() - ]], 0) + set_augroup("LeanInfoviewUpdate", string.format([[ + autocmd CursorHold lua require'lean.infoview'.infoview._by_id[%d]:update() + autocmd CursorHoldI lua require'lean.infoview'.infoview._by_id[%d]:update() + ]], self.id, self.id), 0) else set_augroup("LeanInfoviewUpdate", "", 0) end + self.lean_windows[vim.api.nvim_get_current_win()] = true +end + +-- Clears the given window, possibly autoclosing this infoview if it was the last remaining one. +function Infoview:__clear_win(win, do_not_close) + self.lean_windows[win] = nil + if not do_not_close and options.autoclose and self.lean_windows.get_count() == 0 then self:close() end end --- Update this infoview's contents given the current position. @@ -154,12 +171,6 @@ function Infoview:render() vim.api.nvim_buf_set_option(self.bufnr, 'modifiable', false) end ---- Update the infoview contents appropriately for Lean 4 or 3. ---- Normally will be called on each CursorHold for a buffer containing Lean. -function infoview.__update() - infoview._by_id[get_id()]:update() -end - --- Retrieve the contents of the infoview as a table. function Infoview:get_lines(start_line, end_line) if not self.is_open then return end @@ -190,31 +201,51 @@ function infoview.close_all() end end ---- An infoview was closed, either directly via `Infoview.close` or manually. ---- Will be triggered via a `WinClosed` autocmd. -function infoview.__was_closed(id) - infoview._by_id[id]:close() -end - --- Enable and open the infoview across all Lean buffers. function infoview.enable(opts) - options = vim.tbl_extend("force", options._DEFAULTS, opts) + options = vim.tbl_extend("keep", opts, options._DEFAULTS) set_augroup("LeanInfoviewInit", [[ - autocmd FileType lean3 lua require'lean.infoview'.make_buffer_focusable() - autocmd FileType lean lua require'lean.infoview'.make_buffer_focusable() + autocmd FileType lean3 lua require'lean.infoview'.__make_buffer_focusable() + autocmd FileType lean lua require'lean.infoview'.__make_buffer_focusable() ]]) end +local __last_win + +-- When a lean buffer associated with this infoview BufLeave's, we may want to update lean_windows. +function infoview.__lean_buffer_left() + __last_win = vim.api.nvim_get_current_win() + -- BufEnter is always triggered after BufLeave + vim.cmd(string.format([[ autocmd BufEnter * ++once lua require'lean.infoview'.__lean_buffer_left_post(%d) ]], + get_id())) +end + +-- Actually determines whether to update lean_windows after the inevitable BufEnter following __lean_buffer_left. +-- TODO: Ideally this would be a local function to __lean_buffer_left but sadly neovim doesn't +-- have very good Lua autocmd integration yet :( +function infoview.__lean_buffer_left_post(id) + -- if the original window no longer shows a lean buffer, remove it from the list + if not (vim.api.nvim_win_is_valid(__last_win) and is_lean_buffer(vim.api.nvim_win_get_buf(__last_win))) + then + if infoview._by_id[id] then infoview._by_id[id]:__clear_win(__last_win) end + end + __last_win = nil +end + --- Configure the infoview to update when this buffer is active. -function infoview.make_buffer_focusable() +function infoview.__make_buffer_focusable() + local bufenter = [[autocmd BufEnter lua require'lean.infoview'.__maybe_autoopen() ]] .. + [[require'lean.infoview'.get_current_infoview():focus_on_current_buffer()]] + local bufleave = [[autocmd BufLeave lua require'lean.infoview'.__lean_buffer_left()]] + local winclosed = [[autocmd WinClosed lua if require'lean.infoview'.get_current_infoview() ]] .. + [[then require'lean.infoview'.get_current_infoview():__clear_win]] .. + [[(tonumber(vim.fn.expand('')), true) end]] -- WinEnter is necessary for the edge case where you have -- a file open in a tab with an infoview and move to a -- new window in a new tab with that same file but no infoview - set_augroup("LeanInfoviewSetFocus", [[ - autocmd BufEnter lua require'lean.infoview'.maybe_autoopen() - autocmd BufEnter,WinEnter lua if require'lean.infoview'.get_current_infoview()]] .. - [[ then require'lean.infoview'.get_current_infoview():focus_on_current_buffer() end - ]], 0) + local winenter = [[autocmd WinEnter lua if require'lean.infoview'.get_current_infoview() ]] .. + [[then require'lean.infoview'.get_current_infoview():focus_on_current_buffer() end]] + set_augroup("LeanInfoviewSetFocus", table.concat({bufenter, bufleave, winclosed, winenter}, "\n"), 0) end --- Set whether a new infoview is automatically opened when entering Lean buffers. @@ -222,8 +253,13 @@ function infoview.set_autoopen(autoopen) options.autoopen = autoopen end +--- Set whether an infoview is automatically closed when leaving from its last associated Lean file. +function infoview.set_autoclose(autoclose) + options.autoclose = autoclose +end + --- Open an infoview for the current buffer if it isn't already open. -function infoview.maybe_autoopen() +function infoview.__maybe_autoopen() local id = get_id() if not infoview._by_id[id] then infoview._by_id[id] = Infoview:new(id, options.width, options.autoopen) end end diff --git a/lua/lean/init.lua b/lua/lean/init.lua index 4b72abf3..5852fc20 100644 --- a/lua/lean/init.lua +++ b/lua/lean/init.lua @@ -70,8 +70,8 @@ function lean.use_suggested_mappings(buffer_local) end --- Is the current buffer a lean buffer? -function lean.is_lean_buffer() - local filetype = vim.opt.filetype:get() +function lean.is_lean_buffer(buffer) + local filetype = vim.api.nvim_buf_get_option(buffer or 0, "ft") return filetype == "lean" or filetype == "lean3" end diff --git a/lua/tests/helpers.lua b/lua/tests/helpers.lua index 927a249c..8745ec4d 100644 --- a/lua/tests/helpers.lua +++ b/lua/tests/helpers.lua @@ -17,10 +17,11 @@ local default_config = { }, mappings = false, infoview = { - enable = false + enable = false, + autoclose = false }, lsp3 = { - enable = false + enable = false, }, lsp = { enable = false diff --git a/lua/tests/infoview/autoclose_spec.lua b/lua/tests/infoview/autoclose_spec.lua new file mode 100644 index 00000000..64bf00ce --- /dev/null +++ b/lua/tests/infoview/autoclose_spec.lua @@ -0,0 +1,220 @@ +local infoview = require('lean.infoview') +local fixtures = require('tests.fixtures') + +require('tests.helpers').setup { infoview = { enable = true, autoopen = false, autoclose = true } } +describe('infoview', function() + describe("autocloses", function() + describe("when switching to non-lean window", function() + it('from lean', + function(_) + vim.api.nvim_command('edit ' .. fixtures.lean_project.some_existing_file) + infoview.get_current_infoview():open() + assert.open_infoview() + vim.api.nvim_command('edit temp') + assert.is_not.open_infoview() + end) + + it('from lean3 ', + function(_) + vim.api.nvim_command('edit ' .. fixtures.lean3_project.some_existing_file) + assert.is_not.open_infoview() + infoview.get_current_infoview():open() + assert.open_infoview() + vim.api.nvim_command('edit temp') + assert.is_not.open_infoview() + end) + + it('from multiple splits', + function(_) + vim.api.nvim_command('edit ' .. fixtures.lean_project.some_existing_file) + vim.api.nvim_command('vsplit ' .. fixtures.lean3_project.some_existing_file) + infoview.get_current_infoview():open() + assert.open_infoview() + vim.api.nvim_command('edit temp') + assert.open_infoview() + vim.api.nvim_command('wincmd l') + vim.api.nvim_command('edit temp') + assert.is_not.open_infoview() + end) + + it('from multiple splits, same buffer', + function(_) + vim.api.nvim_command('edit ' .. fixtures.lean3_project.some_existing_file) + vim.api.nvim_command('vsplit ' .. fixtures.lean3_project.some_existing_file) + infoview.get_current_infoview():open() + assert.open_infoview() + vim.api.nvim_command('edit temp') + assert.open_infoview() + vim.api.nvim_command('wincmd l') + vim.api.nvim_command('edit temp') + assert.is_not.open_infoview() + end) + end) + describe("when closing", function() + vim.api.nvim_command('tabnew') + vim.api.nvim_command('edit temp') + vim.api.nvim_command('vsplit') + it('single window', + function(_) + vim.api.nvim_command('edit ' .. fixtures.lean_project.some_existing_file) + infoview.get_current_infoview():open() + assert.equals(1, infoview.get_current_infoview().lean_windows.get_count()) + assert.open_infoview() + vim.api.nvim_command('close') + assert.equals(0, infoview.get_current_infoview().lean_windows.get_count()) + assert.is_not.open_infoview() + end) + + vim.api.nvim_command('vsplit') + it('multiple windows', + function(_) + vim.api.nvim_command('edit ' .. fixtures.lean_project.some_existing_file) + infoview.get_current_infoview():open() + assert.open_infoview() + vim.api.nvim_command('vsplit') + vim.api.nvim_command('edit ' .. fixtures.lean3_project.some_existing_file) + assert.equals(2, infoview.get_current_infoview().lean_windows.get_count()) + assert.open_infoview() + vim.api.nvim_command('close') + assert.equals(1, infoview.get_current_infoview().lean_windows.get_count()) + assert.open_infoview() + vim.api.nvim_command('close') + assert.equals(0, infoview.get_current_infoview().lean_windows.get_count()) + assert.is_not.open_infoview() + end) + + vim.api.nvim_command('vsplit') + it('multiple windows, non-last externally', + function(_) + vim.api.nvim_command('edit ' .. fixtures.lean_project.some_existing_file) + infoview.get_current_infoview():open() + assert.open_infoview() + vim.api.nvim_command('vsplit') + vim.api.nvim_command('edit ' .. fixtures.lean3_project.some_existing_file) + assert.equals(2, infoview.get_current_infoview().lean_windows.get_count()) + assert.open_infoview() + vim.api.nvim_command('2close') + assert.equals(1, infoview.get_current_infoview().lean_windows.get_count()) + assert.open_infoview() + vim.api.nvim_command('close') + assert.equals(0, infoview.get_current_infoview().lean_windows.get_count()) + assert.is_not.open_infoview() + end) + + vim.api.nvim_command('vsplit') + it('multiple windows, same buffer', + function(_) + vim.api.nvim_command('edit ' .. fixtures.lean_project.some_existing_file) + infoview.get_current_infoview():open() + assert.open_infoview() + vim.api.nvim_command('vsplit') + vim.api.nvim_command('edit ' .. fixtures.lean_project.some_existing_file) + assert.open_infoview() + assert.equals(2, infoview.get_current_infoview().lean_windows.get_count()) + vim.api.nvim_command('close') + assert.equals(1, infoview.get_current_infoview().lean_windows.get_count()) + assert.open_infoview() + vim.api.nvim_command('close') + assert.equals(0, infoview.get_current_infoview().lean_windows.get_count()) + assert.is_not.open_infoview() + end) + it('tab\'s last window', + function(_) + vim.api.nvim_command('edit ' .. fixtures.lean_project.some_existing_file) + infoview.get_current_infoview():open() + assert.open_infoview() + assert.is.equal(2, #vim.api.nvim_list_tabpages()) + vim.api.nvim_command('close') + assert.is.equal(1, #vim.api.nvim_list_tabpages()) + end) + end) + vim.api.nvim_command('tabnew') + vim.api.nvim_command('edit temp') + vim.api.nvim_command('vsplit') + it("when both closing and switching", function() + vim.api.nvim_command('edit ' .. fixtures.lean_project.some_existing_file) + infoview.get_current_infoview():open() + assert.open_infoview() + vim.api.nvim_command('vsplit') + vim.api.nvim_command('edit ' .. fixtures.lean3_project.some_existing_file) + assert.equals(2, infoview.get_current_infoview().lean_windows.get_count()) + assert.open_infoview() + vim.api.nvim_command('close') + assert.equals(1, infoview.get_current_infoview().lean_windows.get_count()) + assert.open_infoview() + vim.api.nvim_command('edit temp') + assert.equals(0, infoview.get_current_infoview().lean_windows.get_count()) + assert.is_not.open_infoview() + end) + vim.api.nvim_command('tabclose') + end) + describe("does not autoclose", function() + vim.api.nvim_command('tabnew') + vim.api.nvim_command('edit temp') + vim.api.nvim_command('vsplit') + it('when closing single window externally', + function(_) + vim.api.nvim_command('edit ' .. fixtures.lean_project.some_existing_file) + infoview.get_current_infoview():open() + vim.api.nvim_command('wincmd l') + assert.equals(1, infoview.get_current_infoview().lean_windows.get_count()) + vim.api.nvim_command('1close') + assert.open_infoview() + assert.equals(0, infoview.get_current_infoview().lean_windows.get_count()) + end) + vim.api.nvim_command('edit temp') + + vim.api.nvim_command('vsplit') + it('when closing multiple windows externally', + function(_) + vim.api.nvim_command('edit ' .. fixtures.lean_project.some_existing_file) + infoview.get_current_infoview():open() + vim.api.nvim_command('vsplit') + vim.api.nvim_command('edit ' .. fixtures.lean3_project.some_existing_file) + assert.equals(2, infoview.get_current_infoview().lean_windows.get_count()) + vim.api.nvim_command('wincmd l') + vim.api.nvim_command('wincmd l') + vim.api.nvim_command('1close') + assert.equals(1, infoview.get_current_infoview().lean_windows.get_count()) + vim.api.nvim_command('1close') + assert.open_infoview() + assert.equals(0, infoview.get_current_infoview().lean_windows.get_count()) + end) + + vim.api.nvim_command('vsplit') + it('when closing irrelevant window', + function(_) + vim.api.nvim_command('edit ' .. fixtures.lean_project.some_existing_file) + infoview.get_current_infoview():open() + assert.equals(1, infoview.get_current_infoview().lean_windows.get_count()) + vim.api.nvim_command('2close') + assert.open_infoview() + assert.equals(1, infoview.get_current_infoview().lean_windows.get_count()) + vim.api.nvim_command('edit temp') + end) + + vim.api.nvim_command('vsplit') + it('when autoclose disabled', + function(_) + vim.api.nvim_command('edit ' .. fixtures.lean_project.some_existing_file) + infoview.get_current_infoview():open() + infoview.set_autoclose(false) + assert.equals(1, infoview.get_current_infoview().lean_windows.get_count()) + vim.api.nvim_command('close') + assert.open_infoview() + assert.equals(0, infoview.get_current_infoview().lean_windows.get_count()) + vim.api.nvim_command('edit temp') + end) + end) + + it('continues to autoclose when autoclose re-enabled', + function(_) + vim.api.nvim_command('edit ' .. fixtures.lean_project.some_existing_file) + infoview.get_current_infoview():open() + infoview.set_autoclose(true) + assert.equals(1, infoview.get_current_infoview().lean_windows.get_count()) + vim.api.nvim_command('close') + assert.is_not.open_infoview() + assert.equals(0, infoview.get_current_infoview().lean_windows.get_count()) + end) +end) diff --git a/lua/tests/infoview/lsp_spec.lua b/lua/tests/infoview/lsp_spec.lua index 5a48cb17..bd30a2f1 100644 --- a/lua/tests/infoview/lsp_spec.lua +++ b/lua/tests/infoview/lsp_spec.lua @@ -8,7 +8,7 @@ local function infoview_lsp_update(pos) vim.api.nvim_win_set_cursor(0, pos) -- wait for server pass local result, _ = vim.wait(10000, function() - infoview.__update() + infoview.get_current_infoview():update() -- wait for update data - will be empty if server pass incomplete local update_result, _ = vim.wait(500, function() local curr = current_infoview:get_contents() @@ -30,7 +30,7 @@ describe('infoview', function() it('immediate close', function() vim.api.nvim_command('edit ' .. fixtures.lean3_project.some_existing_file) helpers.wait_for_ready_lsp() - infoview.__update() + infoview.get_current_infoview():update() infoview.get_current_infoview():close() vim.wait(5000, function() assert.is_not.has_match("Error", vim.api.nvim_exec("messages", true), nil, true)