From 0502628bda741591bc7f04c31c14a4fac387ddea Mon Sep 17 00:00:00 2001 From: "Kunal K. Sarkhel" Date: Fri, 12 Jun 2026 20:13:09 -0400 Subject: [PATCH 1/3] Fix stale rendering suppression from utility floats Narrow floating-window suppression so render-latex only hides images for completion popups, focusable floating windows, and command-line floats when command-line hiding is explicitly enabled. This prevents non-focusable utility floats from LazyVim-style UI plugins from permanently suppressing equation image placement after cursor movement. The previous behavior treated every floating window as blocking, which could delete already-rendered images and keep placement suppressed until the float closed. Add regression coverage for completion popups continuing to suppress image placement, focusable floating windows continuing to suppress image placement, non-focusable utility floats being ignored, and command-line UI floats respecting render.hide_on_cmdline. --- doc/render-latex.txt | 6 +-- lua/render_latex/ui.lua | 8 +++- tests/render_latex_spec.lua | 82 +++++++++++++++++++++++++++++++++++++ 3 files changed, 92 insertions(+), 4 deletions(-) diff --git a/doc/render-latex.txt b/doc/render-latex.txt index dbcf5ad..bf57058 100644 --- a/doc/render-latex.txt +++ b/doc/render-latex.txt @@ -37,11 +37,11 @@ and: Inline math is intentionally not image-rendered. It uses highlights and conceal for smooth editing and predictable cursor behavior. -Images are hidden while popups or floating windows are open. Plain command-line -typing does not hide images by default; opt in with +Images are hidden while completion popups or focusable floating windows are +open. Plain command-line typing does not hide images by default; opt in with `render.hide_on_cmdline = true`. When command-line hiding is disabled, cmdline UI floats are ignored but completion popups still hide images until -command-line mode exits. +command-line mode exits. Non-focusable utility floats are ignored. =============================================================================== REQUIREMENTS *render-latex-requirements* diff --git a/lua/render_latex/ui.lua b/lua/render_latex/ui.lua index 3f8db96..b9ed0dc 100644 --- a/lua/render_latex/ui.lua +++ b/lua/render_latex/ui.lua @@ -1,13 +1,19 @@ local M = {} +local Config = require("render_latex.config") + function M.has_popup_or_floating_windows() if vim.fn.pumvisible() == 1 then return true end + if not Config.render.hide_on_cmdline and vim.fn.getcmdtype() ~= "" then + return false + end + for _, winid in ipairs(vim.api.nvim_list_wins()) do local cfg = vim.api.nvim_win_get_config(winid) - if cfg.relative ~= nil and cfg.relative ~= "" then + if cfg.relative ~= nil and cfg.relative ~= "" and cfg.focusable ~= false then return true end end diff --git a/tests/render_latex_spec.lua b/tests/render_latex_spec.lua index f7d8d0c..1b00566 100644 --- a/tests/render_latex_spec.lua +++ b/tests/render_latex_spec.lua @@ -1727,6 +1727,19 @@ describe("render_latex.setup", function() end) describe("render_latex.ui", function() + it("keeps completion popups suppressed", function() + local previous_pumvisible = vim.fn.pumvisible + vim.fn.pumvisible = function() + return 1 + end + + local ok, detected = pcall(ui.has_popup_or_floating_windows) + vim.fn.pumvisible = previous_pumvisible + + assert.is_true(ok) + assert.is_true(detected) + end) + it("detects floating windows independently of cmdline settings", function() local buf = vim.api.nvim_create_buf(false, true) local win = vim.api.nvim_open_win(buf, false, { @@ -1744,6 +1757,75 @@ describe("render_latex.ui", function() assert.is_true(ok) assert.is_true(detected) end) + + it("ignores non-focusable utility floating windows", function() + local buf = vim.api.nvim_create_buf(false, true) + local win = vim.api.nvim_open_win(buf, false, { + relative = "editor", + row = 1, + col = 1, + width = 10, + height = 1, + style = "minimal", + focusable = false, + }) + + local ok, detected = pcall(ui.has_popup_or_floating_windows) + vim.api.nvim_win_close(win, true) + + assert.is_true(ok) + assert.is_false(detected) + end) + + it("ignores command-line UI floats when command-line hiding is disabled", function() + config.setup({ render = { hide_on_cmdline = false } }) + local previous_getcmdtype = vim.fn.getcmdtype + vim.fn.getcmdtype = function() + return ":" + end + local buf = vim.api.nvim_create_buf(false, true) + local win = vim.api.nvim_open_win(buf, false, { + relative = "editor", + row = 1, + col = 1, + width = 10, + height = 1, + style = "minimal", + }) + + local ok, detected = pcall(ui.has_popup_or_floating_windows) + vim.api.nvim_win_close(win, true) + vim.fn.getcmdtype = previous_getcmdtype + config.setup() + + assert.is_true(ok) + assert.is_false(detected) + end) + + it("detects command-line floats when command-line hiding is enabled", function() + config.setup({ render = { hide_on_cmdline = true } }) + local previous_getcmdtype = vim.fn.getcmdtype + vim.fn.getcmdtype = function() + return ":" + end + local buf = vim.api.nvim_create_buf(false, true) + local win = vim.api.nvim_open_win(buf, false, { + relative = "editor", + row = 1, + col = 1, + width = 10, + height = 1, + style = "minimal", + }) + + local ok, detected = pcall(ui.has_popup_or_floating_windows) + vim.api.nvim_win_close(win, true) + vim.fn.getcmdtype = previous_getcmdtype + config.setup() + + assert.is_true(ok) + assert.is_true(detected) + end) end) describe("render_latex.compat", function() From ef9a50066f1468d0330b8dd3d8c18ded977f2167 Mon Sep 17 00:00:00 2001 From: "Kunal K. Sarkhel" Date: Mon, 15 Jun 2026 22:45:01 -0400 Subject: [PATCH 2/3] Stop suppressing equation images for plugin floats Do not treat arbitrary floating windows as a reason to hide or skip equation image placement. Completion popup menus still suppress placement, and command-line mode still suppresses placement when render.hide_on_cmdline is enabled. The previous behavior allowed persistent plugin floats to set floating suppression globally. That caused rendered equations to be deleted on cursor movement and prevented them from being placed again while the float remained open. LazyVim, Obsidian, noice, snacks, and similar UI integrations can keep focusable helper floats around, so filtering only non-focusable floats was still too broad. Update tests to cover the revised suppression policy: completion popups still suppress image placement, normal floating windows do not suppress image placement, and command-line mode respects render.hide_on_cmdline. Update help text to document that plugin floats do not hide images. Validation: make test; make lint; make check; make smoke-base; make smoke-render-markdown; make smoke-doctor; make smoke-health; make smoke-help --- doc/render-latex.txt | 9 +++---- lua/render_latex/init.lua | 21 ++++++++++++++- lua/render_latex/ui.lua | 32 ++++++++++++++-------- tests/render_latex_spec.lua | 53 ++++++++++++++++++++++++++----------- 4 files changed, 82 insertions(+), 33 deletions(-) diff --git a/doc/render-latex.txt b/doc/render-latex.txt index bf57058..e539a24 100644 --- a/doc/render-latex.txt +++ b/doc/render-latex.txt @@ -37,11 +37,10 @@ and: Inline math is intentionally not image-rendered. It uses highlights and conceal for smooth editing and predictable cursor behavior. -Images are hidden while completion popups or focusable floating windows are -open. Plain command-line typing does not hide images by default; opt in with -`render.hide_on_cmdline = true`. When command-line hiding is disabled, cmdline -UI floats are ignored but completion popups still hide images until -command-line mode exits. Non-focusable utility floats are ignored. +Images are hidden while completion popups are open. Plain command-line typing +does not hide images by default; opt in with `render.hide_on_cmdline = true`. +Floating windows from other plugins do not hide images, but active float details +are reported by `:RenderLatex doctor` for troubleshooting. =============================================================================== REQUIREMENTS *render-latex-requirements* diff --git a/lua/render_latex/init.lua b/lua/render_latex/init.lua index e64ca83..0f8fd2b 100644 --- a/lua/render_latex/init.lua +++ b/lua/render_latex/init.lua @@ -261,6 +261,7 @@ function M.doctor_lines() local Install = require("render_latex.install") local Integrations = require("render_latex.integrations") local Renderer = require("render_latex.renderer") + local Ui = require("render_latex.ui") local Worker = require("render_latex.worker") local bufnr = vim.api.nvim_get_current_buf() @@ -272,6 +273,7 @@ function M.doctor_lines() local render = Renderer.resolved_options() local source = require("render_latex.sources").status(bufnr) local suppression = Renderer.suppression_status() + local floats = Ui.floating_windows() local filetype = vim.bo[bufnr].filetype local lines = { @@ -313,7 +315,24 @@ function M.doctor_lines() "conceallevel: " .. tostring(vim.wo.conceallevel), "concealcursor: " .. tostring(vim.wo.concealcursor), "suppressed by cmdline: " .. tostring(suppression.cmdline), - "image placement suppressed by popups/floating windows: " .. tostring(suppression.floating), + "image placement suppressed by popup/cmdline UI: " .. tostring(suppression.floating), + "active floating windows: " .. tostring(#floats), + }) + + for _, float in ipairs(floats) do + lines[#lines + 1] = table.concat({ + "- winid: " .. tostring(float.winid), + "relative: " .. tostring(float.relative), + "focusable: " .. tostring(float.focusable), + "zindex: " .. tostring(float.zindex), + "row: " .. tostring(float.row), + "col: " .. tostring(float.col), + "width: " .. tostring(float.width), + "height: " .. tostring(float.height), + }, ", ") + end + + vim.list_extend(lines, { "", "## Worker", "", diff --git a/lua/render_latex/ui.lua b/lua/render_latex/ui.lua index b9ed0dc..d82b934 100644 --- a/lua/render_latex/ui.lua +++ b/lua/render_latex/ui.lua @@ -2,22 +2,32 @@ local M = {} local Config = require("render_latex.config") +function M.floating_windows() + local floats = {} + for _, winid in ipairs(vim.api.nvim_list_wins()) do + local cfg = vim.api.nvim_win_get_config(winid) + if cfg.relative ~= nil and cfg.relative ~= "" then + floats[#floats + 1] = { + winid = winid, + relative = cfg.relative, + focusable = cfg.focusable, + zindex = cfg.zindex, + row = cfg.row, + col = cfg.col, + width = cfg.width, + height = cfg.height, + } + end + end + return floats +end + function M.has_popup_or_floating_windows() if vim.fn.pumvisible() == 1 then return true end - if not Config.render.hide_on_cmdline and vim.fn.getcmdtype() ~= "" then - return false - end - - for _, winid in ipairs(vim.api.nvim_list_wins()) do - local cfg = vim.api.nvim_win_get_config(winid) - if cfg.relative ~= nil and cfg.relative ~= "" and cfg.focusable ~= false then - return true - end - end - return false + return Config.render.hide_on_cmdline and vim.fn.getcmdtype() ~= "" end return M diff --git a/tests/render_latex_spec.lua b/tests/render_latex_spec.lua index 1b00566..932455a 100644 --- a/tests/render_latex_spec.lua +++ b/tests/render_latex_spec.lua @@ -1740,7 +1740,7 @@ describe("render_latex.ui", function() assert.is_true(detected) end) - it("detects floating windows independently of cmdline settings", function() + it("reports floating window details for diagnostics", function() local buf = vim.api.nvim_create_buf(false, true) local win = vim.api.nvim_open_win(buf, false, { relative = "editor", @@ -1751,14 +1751,18 @@ describe("render_latex.ui", function() style = "minimal", }) - local ok, detected = pcall(ui.has_popup_or_floating_windows) + local ok, floats = pcall(ui.floating_windows) vim.api.nvim_win_close(win, true) assert.is_true(ok) - assert.is_true(detected) + assert.are.equal(1, #floats) + assert.are.equal(win, floats[1].winid) + assert.are.equal("editor", floats[1].relative) + assert.are.equal(10, floats[1].width) + assert.are.equal(1, floats[1].height) end) - it("ignores non-focusable utility floating windows", function() + it("does not suppress image placement for floating windows", function() local buf = vim.api.nvim_create_buf(false, true) local win = vim.api.nvim_open_win(buf, false, { relative = "editor", @@ -1777,7 +1781,7 @@ describe("render_latex.ui", function() assert.is_false(detected) end) - it("ignores command-line UI floats when command-line hiding is disabled", function() + it("does not suppress command-line UI when command-line hiding is disabled", function() config.setup({ render = { hide_on_cmdline = false } }) local previous_getcmdtype = vim.fn.getcmdtype vim.fn.getcmdtype = function() @@ -1802,24 +1806,14 @@ describe("render_latex.ui", function() assert.is_false(detected) end) - it("detects command-line floats when command-line hiding is enabled", function() + it("suppresses command-line UI when command-line hiding is enabled", function() config.setup({ render = { hide_on_cmdline = true } }) local previous_getcmdtype = vim.fn.getcmdtype vim.fn.getcmdtype = function() return ":" end - local buf = vim.api.nvim_create_buf(false, true) - local win = vim.api.nvim_open_win(buf, false, { - relative = "editor", - row = 1, - col = 1, - width = 10, - height = 1, - style = "minimal", - }) local ok, detected = pcall(ui.has_popup_or_floating_windows) - vim.api.nvim_win_close(win, true) vim.fn.getcmdtype = previous_getcmdtype config.setup() @@ -1845,6 +1839,7 @@ describe("render_latex.doctor", function() assert.matches("# render%-latex doctor", text) assert.matches("image backend status", text) assert.matches("hide on cmdline", text) + assert.matches("active floating windows", text) assert.matches("kitty probing", text) assert.matches("tmux passthrough", text) assert.matches("## render%-markdown.nvim", text) @@ -1853,6 +1848,32 @@ describe("render_latex.doctor", function() assert.matches("The render loop does not inspect other plugins", text) end) + it("includes active floating window details", function() + local buf = vim.api.nvim_create_buf(false, true) + local win = vim.api.nvim_open_win(buf, false, { + relative = "editor", + row = 2, + col = 3, + width = 12, + height = 4, + style = "minimal", + focusable = false, + zindex = 80, + }) + + local lines = render_latex.doctor_lines() + vim.api.nvim_win_close(win, true) + local text = table.concat(lines, "\n") + + assert.matches("active floating windows: 1", text) + assert.matches("winid: " .. tostring(win), text) + assert.matches("relative: editor", text) + assert.matches("focusable: false", text) + assert.matches("zindex: 80", text) + assert.matches("width: 12", text) + assert.matches("height: 4", text) + end) + it("includes backend availability in tmux diagnostics", function() local previous_open_scratch = util.open_scratch local opened From 3cd10ba4e92c195ee24a6fa9abc6b30ad49166a0 Mon Sep 17 00:00:00 2001 From: "Kunal K. Sarkhel" Date: Tue, 16 Jun 2026 06:16:06 -0400 Subject: [PATCH 3/3] Fix Kitty image ghosting across tabs and windows Clear terminal image placements when leaving buffers, windows, or tabs, then requeue only markdown buffers visible in the current tab. This prevents Kitty graphics from old layouts from being rendered into a newly opened tab, buffer, or split. Restrict render visibility checks to current-tab windows for display image placement, inline fallback ranges, cursor reveal checks, and viewport filtering. Also avoid relying on the current window during BufWinLeave, where Neovim event ordering can make it point at the wrong window. Add regression coverage for transition cleanup, current-tab-only requeueing, inactive-tab markdown buffers, inactive-tab inline fallback behavior, and viewport filtering. Fixes https://github.com/techwizrd/render-latex.nvim/issues/9 --- lua/render_latex/annotations.lua | 21 +-- lua/render_latex/init.lua | 42 +++-- lua/render_latex/renderer.lua | 65 ++++--- lua/render_latex/util.lua | 32 ++++ lua/render_latex/viewport.lua | 9 +- tests/render_latex_spec.lua | 295 +++++++++++++++++++++++++++++-- 6 files changed, 394 insertions(+), 70 deletions(-) diff --git a/lua/render_latex/annotations.lua b/lua/render_latex/annotations.lua index fe1acc6..00c9c90 100644 --- a/lua/render_latex/annotations.lua +++ b/lua/render_latex/annotations.lua @@ -1,6 +1,7 @@ local Config = require("render_latex.config") local Compat = require("render_latex.compat") local Detect = require("render_latex.detect") +local Util = require("render_latex.util") local M = {} @@ -326,19 +327,13 @@ function M.render_inline_fallback(bufnr, ns, state, ranges) end local active = {} - for _, winid in ipairs(vim.fn.win_findbuf(bufnr)) do - if vim.api.nvim_win_is_valid(winid) then - local cursor = vim.api.nvim_win_get_cursor(winid) - local cursor_row = cursor[1] - 1 - local cursor_col = cursor[2] - for index, item in ipairs(items) do - if - cursor_row == item.row - and cursor_col >= item.start_col - and cursor_col < item.end_col - then - active[index] = true - end + for _, winid in ipairs(Util.current_tab_wins_for_buf(bufnr)) do + local cursor = vim.api.nvim_win_get_cursor(winid) + local cursor_row = cursor[1] - 1 + local cursor_col = cursor[2] + for index, item in ipairs(items) do + if cursor_row == item.row and cursor_col >= item.start_col and cursor_col < item.end_col then + active[index] = true end end end diff --git a/lua/render_latex/init.lua b/lua/render_latex/init.lua index 0f8fd2b..e83f687 100644 --- a/lua/render_latex/init.lua +++ b/lua/render_latex/init.lua @@ -32,15 +32,13 @@ local function register_autocmds() local function queue_visible_buffers() local seen = {} - for _, winid in ipairs(vim.api.nvim_list_wins()) do - local cfg = vim.api.nvim_win_get_config(winid) - if cfg.relative == nil or cfg.relative == "" then - local bufnr = vim.api.nvim_win_get_buf(winid) - if not seen[bufnr] and should_attach(bufnr) then - seen[bufnr] = true - Renderer.attach(bufnr) - Renderer.queue(bufnr) - end + local Util = require("render_latex.util") + for _, winid in ipairs(Util.current_tab_normal_wins()) do + local bufnr = vim.api.nvim_win_get_buf(winid) + if not seen[bufnr] and should_attach(bufnr) then + seen[bufnr] = true + Renderer.attach(bufnr) + Renderer.queue(bufnr) end end end @@ -58,8 +56,6 @@ local function register_autocmds() end vim.api.nvim_create_autocmd({ - "BufEnter", - "BufWinEnter", "InsertEnter", "InsertLeave", "CursorMoved", @@ -83,6 +79,17 @@ local function register_autocmds() end, }) + vim.api.nvim_create_autocmd({ "BufEnter", "BufWinEnter", "TabEnter", "WinEnter" }, { + group = Config.augroup, + callback = function(args) + local bufnr = args.buf or vim.api.nvim_get_current_buf() + if not should_attach(bufnr) then + Renderer.detach(bufnr) + end + update_transient_ui_suppression(true) + end, + }) + vim.api.nvim_create_autocmd("ColorScheme", { group = Config.augroup, callback = function() @@ -92,6 +99,13 @@ local function register_autocmds() end, }) + vim.api.nvim_create_autocmd({ "BufLeave", "TabLeave", "WinLeave" }, { + group = Config.augroup, + callback = function() + Renderer.hide_visible() + end, + }) + vim.api.nvim_create_autocmd("WinScrolled", { group = Config.augroup, callback = function(args) @@ -129,12 +143,13 @@ local function register_autocmds() vim.api.nvim_create_autocmd("BufWinLeave", { group = Config.augroup, callback = function(args) - Renderer.detach_window(args.buf, vim.api.nvim_get_current_win()) + Renderer.hide_visible() + Renderer.detach_inactive_windows(args.buf) update_transient_ui_suppression(false) end, }) - vim.api.nvim_create_autocmd({ "CmdlineEnter", "CmdlineChanged", "CmdlineLeave", "WinEnter" }, { + vim.api.nvim_create_autocmd({ "CmdlineEnter", "CmdlineChanged", "CmdlineLeave" }, { group = Config.augroup, callback = function() update_transient_ui_suppression(true) @@ -144,6 +159,7 @@ local function register_autocmds() vim.api.nvim_create_autocmd("WinClosed", { group = Config.augroup, callback = function(args) + Renderer.hide_visible() local winid = tonumber(args.match) if winid ~= nil then Renderer.detach_winid(winid) diff --git a/lua/render_latex/renderer.lua b/lua/render_latex/renderer.lua index d208597..a897baa 100644 --- a/lua/render_latex/renderer.lua +++ b/lua/render_latex/renderer.lua @@ -444,15 +444,13 @@ end local function visible_inline_ranges(bufnr) local ranges = {} local line_count = vim.api.nvim_buf_line_count(bufnr) - for _, winid in ipairs(vim.fn.win_findbuf(bufnr)) do - if Util.win_is_valid(winid) then - local top = math.max(0, vim.fn.line("w0", winid) - 1) - local bottom = math.min(line_count - 1, vim.fn.line("w$", winid) - 1) - ranges[#ranges + 1] = { start_row = top, end_row = bottom } - end + for _, winid in ipairs(Util.current_tab_wins_for_buf(bufnr)) do + local top = math.max(0, vim.fn.line("w0", winid) - 1) + local bottom = math.min(line_count - 1, vim.fn.line("w$", winid) - 1) + ranges[#ranges + 1] = { start_row = top, end_row = bottom } end if #ranges == 0 then - return nil + return {} end return Sources.inline_ranges(bufnr, ranges) end @@ -614,7 +612,7 @@ function M.hide_all() end end -local function hide_visible() +function M.hide_visible() local backend = ImageBackend.get() if backend ~= nil then pcall(backend.del, math.huge) @@ -766,19 +764,17 @@ local function collect_window_snapshot(bufnr, viewport_state, prefetch) windows = {}, } - for _, winid in ipairs(vim.fn.win_findbuf(bufnr)) do - if Util.win_is_valid(winid) then - local top, bottom = Viewport.viewport_range(viewport_state, winid, prefetch) - local text_top, text_bottom = Viewport.visible_text_bounds(winid) - snapshot.winids[#snapshot.winids + 1] = winid - snapshot.ranges[#snapshot.ranges + 1] = { top = top, bottom = bottom } - snapshot.windows[winid] = { - width = vim.api.nvim_win_get_width(winid), - text_top = text_top, - text_bottom = text_bottom, - cursor_row = vim.api.nvim_win_get_cursor(winid)[1] - 1, - } - end + for _, winid in ipairs(Util.current_tab_wins_for_buf(bufnr)) do + local top, bottom = Viewport.viewport_range(viewport_state, winid, prefetch) + local text_top, text_bottom = Viewport.visible_text_bounds(winid) + snapshot.winids[#snapshot.winids + 1] = winid + snapshot.ranges[#snapshot.ranges + 1] = { top = top, bottom = bottom } + snapshot.windows[winid] = { + width = vim.api.nvim_win_get_width(winid), + text_top = text_top, + text_bottom = text_bottom, + cursor_row = vim.api.nvim_win_get_cursor(winid)[1] - 1, + } end return snapshot @@ -1395,6 +1391,31 @@ function M.detach_window(bufnr, winid) clear_window_images(bufnr, state, winid) end +---@param bufnr integer +function M.detach_inactive_windows(bufnr) + local state = buffers[bufnr] + if state == nil then + return + end + + local winids = {} + for winid, _ in pairs(state.images) do + winids[winid] = true + end + for winid, _ in pairs(state.focused_keys) do + winids[winid] = true + end + for winid, _ in pairs(state.viewports) do + winids[winid] = true + end + + for winid, _ in pairs(winids) do + if not Util.win_is_valid(winid) or vim.api.nvim_win_get_buf(winid) ~= bufnr then + clear_window_images(bufnr, state, winid) + end + end +end + ---@param winid integer function M.detach_winid(winid) for bufnr, state in pairs(buffers) do @@ -1499,7 +1520,7 @@ function M.set_suppressed(reason, value) if suppression.cmdline then M.hide_all() elseif suppression.floating then - hide_visible() + M.hide_visible() end end diff --git a/lua/render_latex/util.lua b/lua/render_latex/util.lua index fa8d396..025e0b9 100644 --- a/lua/render_latex/util.lua +++ b/lua/render_latex/util.lua @@ -43,6 +43,38 @@ function M.win_is_valid(winid) return winid ~= nil and vim.api.nvim_win_is_valid(winid) end +---@param winid integer +function M.win_is_normal(winid) + if not M.win_is_valid(winid) then + return false + end + local cfg = vim.api.nvim_win_get_config(winid) + return cfg.relative == nil or cfg.relative == "" +end + +---@return integer[] +function M.current_tab_normal_wins() + local wins = {} + for _, winid in ipairs(vim.api.nvim_tabpage_list_wins(0)) do + if M.win_is_normal(winid) then + wins[#wins + 1] = winid + end + end + return wins +end + +---@param bufnr integer +---@return integer[] +function M.current_tab_wins_for_buf(bufnr) + local wins = {} + for _, winid in ipairs(M.current_tab_normal_wins()) do + if vim.api.nvim_win_get_buf(winid) == bufnr then + wins[#wins + 1] = winid + end + end + return wins +end + ---@param value string ---@return string function M.escape_pattern(value) diff --git a/lua/render_latex/viewport.lua b/lua/render_latex/viewport.lua index 73c7bca..9ceff4e 100644 --- a/lua/render_latex/viewport.lua +++ b/lua/render_latex/viewport.lua @@ -1,4 +1,5 @@ local Config = require("render_latex.config") +local Util = require("render_latex.util") local M = {} @@ -77,11 +78,9 @@ end ---@return { top: integer, bottom: integer }[] function M.viewport_ranges(bufnr, viewport_state, prefetch) local ranges = {} - for _, winid in ipairs(vim.fn.win_findbuf(bufnr)) do - if vim.api.nvim_win_is_valid(winid) then - local top, bottom = M.viewport_range(viewport_state, winid, prefetch) - ranges[#ranges + 1] = { top = top, bottom = bottom } - end + for _, winid in ipairs(Util.current_tab_wins_for_buf(bufnr)) do + local top, bottom = M.viewport_range(viewport_state, winid, prefetch) + ranges[#ranges + 1] = { top = top, bottom = bottom } end return ranges end diff --git a/tests/render_latex_spec.lua b/tests/render_latex_spec.lua index 932455a..885f702 100644 --- a/tests/render_latex_spec.lua +++ b/tests/render_latex_spec.lua @@ -1573,7 +1573,8 @@ describe("render_latex.setup", function() end) it("requeues all visible markdown buffers on ColorScheme", function() - local previous_list_wins = vim.api.nvim_list_wins + local previous_tabpage_list_wins = vim.api.nvim_tabpage_list_wins + local previous_win_is_valid = vim.api.nvim_win_is_valid local previous_win_get_config = vim.api.nvim_win_get_config local previous_win_get_buf = vim.api.nvim_win_get_buf local previous_attach = renderer.attach @@ -1606,9 +1607,12 @@ describe("render_latex.setup", function() local right = vim.api.nvim_create_buf(false, true) vim.bo[left].filetype = "markdown" vim.bo[right].filetype = "markdown" - vim.api.nvim_list_wins = function() + vim.api.nvim_tabpage_list_wins = function() return { 101, 202 } end + vim.api.nvim_win_is_valid = function(winid) + return winid == 101 or winid == 202 + end vim.api.nvim_win_get_config = function() return { relative = "" } end @@ -1622,7 +1626,8 @@ describe("render_latex.setup", function() vim.api.nvim_exec_autocmds("ColorScheme", {}) - vim.api.nvim_list_wins = previous_list_wins + vim.api.nvim_tabpage_list_wins = previous_tabpage_list_wins + vim.api.nvim_win_is_valid = previous_win_is_valid vim.api.nvim_win_get_config = previous_win_get_config vim.api.nvim_win_get_buf = previous_win_get_buf renderer.attach = previous_attach @@ -1638,6 +1643,213 @@ describe("render_latex.setup", function() assert.are.same({ left, right }, queued) end) + it("hides terminal images on context leave events", function() + local previous_hide_visible = renderer.hide_visible + local hide_count = 0 + + renderer.hide_visible = function() + hide_count = hide_count + 1 + end + + render_latex.setup({ install = { auto = false } }) + for _, event in ipairs({ "BufLeave", "TabLeave", "WinLeave" }) do + vim.api.nvim_exec_autocmds(event, { modeline = false }) + end + + renderer.hide_visible = previous_hide_visible + + assert.are.equal(3, hide_count) + end) + + it("hides terminal images on BufWinLeave", function() + local previous_hide_visible = renderer.hide_visible + local previous_detach_inactive_windows = renderer.detach_inactive_windows + local hide_count = 0 + local detached_bufnr + + renderer.hide_visible = function() + hide_count = hide_count + 1 + end + renderer.detach_inactive_windows = function(bufnr) + detached_bufnr = bufnr + end + + local buf = vim.api.nvim_create_buf(false, true) + render_latex.setup({ install = { auto = false } }) + vim.api.nvim_exec_autocmds("BufWinLeave", { buffer = buf, modeline = false }) + + renderer.hide_visible = previous_hide_visible + renderer.detach_inactive_windows = previous_detach_inactive_windows + + assert.are.equal(1, hide_count) + assert.are.equal(buf, detached_bufnr) + end) + + it("hides terminal images on WinClosed", function() + local previous_hide_visible = renderer.hide_visible + local previous_detach_winid = renderer.detach_winid + local hide_count = 0 + local detached_winid + + renderer.hide_visible = function() + hide_count = hide_count + 1 + end + renderer.detach_winid = function(winid) + detached_winid = winid + end + + render_latex.setup({ install = { auto = false } }) + vim.api.nvim_exec_autocmds("WinClosed", { pattern = "42", modeline = false }) + + renderer.hide_visible = previous_hide_visible + renderer.detach_winid = previous_detach_winid + + assert.are.equal(1, hide_count) + assert.are.equal(42, detached_winid) + end) + + it("requeues visible markdown buffers on context enter events", function() + local previous_tabpage_list_wins = vim.api.nvim_tabpage_list_wins + local previous_win_is_valid = vim.api.nvim_win_is_valid + local previous_win_get_config = vim.api.nvim_win_get_config + local previous_win_get_buf = vim.api.nvim_win_get_buf + local previous_attach = renderer.attach + local previous_queue = renderer.queue + local previous_suppression = renderer.suppression_status + local previous_set_suppressed = renderer.set_suppressed + local previous_has_popup = ui.has_popup_or_floating_windows + local previous_getcmdtype = vim.fn.getcmdtype + local attached = {} + local queued = {} + + renderer.attach = function(bufnr) + attached[#attached + 1] = bufnr + end + renderer.queue = function(bufnr) + queued[#queued + 1] = bufnr + end + renderer.suppression_status = function() + return { cmdline = false, floating = false } + end + renderer.set_suppressed = function() end + ui.has_popup_or_floating_windows = function() + return false + end + vim.fn.getcmdtype = function() + return "" + end + + local markdown = vim.api.nvim_create_buf(false, true) + local text = vim.api.nvim_create_buf(false, true) + vim.bo[markdown].filetype = "markdown" + vim.bo[text].filetype = "text" + vim.api.nvim_set_current_buf(markdown) + vim.api.nvim_tabpage_list_wins = function() + return { 101, 202 } + end + vim.api.nvim_win_is_valid = function(winid) + return winid == 101 or winid == 202 + end + vim.api.nvim_win_get_config = function() + return { relative = "" } + end + vim.api.nvim_win_get_buf = function(winid) + return winid == 101 and markdown or text + end + + render_latex.setup({ install = { auto = false } }) + attached = {} + queued = {} + + for _, event in ipairs({ "BufEnter", "BufWinEnter", "TabEnter", "WinEnter" }) do + vim.api.nvim_exec_autocmds(event, { buffer = markdown, modeline = false }) + end + + vim.api.nvim_tabpage_list_wins = previous_tabpage_list_wins + vim.api.nvim_win_is_valid = previous_win_is_valid + vim.api.nvim_win_get_config = previous_win_get_config + vim.api.nvim_win_get_buf = previous_win_get_buf + renderer.attach = previous_attach + renderer.queue = previous_queue + renderer.suppression_status = previous_suppression + renderer.set_suppressed = previous_set_suppressed + ui.has_popup_or_floating_windows = previous_has_popup + vim.fn.getcmdtype = previous_getcmdtype + + assert.are.same({ markdown, markdown, markdown, markdown }, attached) + assert.are.same({ markdown, markdown, markdown, markdown }, queued) + end) + + it("does not queue markdown buffers from inactive tabs", function() + local previous_list_wins = vim.api.nvim_list_wins + local previous_tabpage_list_wins = vim.api.nvim_tabpage_list_wins + local previous_win_is_valid = vim.api.nvim_win_is_valid + local previous_win_get_config = vim.api.nvim_win_get_config + local previous_win_get_buf = vim.api.nvim_win_get_buf + local previous_attach = renderer.attach + local previous_queue = renderer.queue + local previous_suppression = renderer.suppression_status + local previous_set_suppressed = renderer.set_suppressed + local previous_has_popup = ui.has_popup_or_floating_windows + local previous_getcmdtype = vim.fn.getcmdtype + local queued = {} + + renderer.attach = function() end + renderer.queue = function(bufnr) + queued[#queued + 1] = bufnr + end + renderer.suppression_status = function() + return { cmdline = false, floating = false } + end + renderer.set_suppressed = function() end + ui.has_popup_or_floating_windows = function() + return false + end + vim.fn.getcmdtype = function() + return "" + end + + local current = vim.api.nvim_create_buf(false, true) + local inactive = vim.api.nvim_create_buf(false, true) + vim.bo[current].filetype = "text" + vim.bo[inactive].filetype = "markdown" + vim.api.nvim_set_current_buf(current) + vim.api.nvim_list_wins = function() + return { 101, 202 } + end + vim.api.nvim_tabpage_list_wins = function() + return { 101 } + end + vim.api.nvim_win_is_valid = function(winid) + return winid == 101 or winid == 202 + end + vim.api.nvim_win_get_config = function() + return { relative = "" } + end + vim.api.nvim_win_get_buf = function(winid) + return winid == 101 and current or inactive + end + + render_latex.setup({ install = { auto = false } }) + queued = {} + + vim.api.nvim_exec_autocmds("TabEnter", { buffer = current, modeline = false }) + + vim.api.nvim_list_wins = previous_list_wins + vim.api.nvim_tabpage_list_wins = previous_tabpage_list_wins + vim.api.nvim_win_is_valid = previous_win_is_valid + vim.api.nvim_win_get_config = previous_win_get_config + vim.api.nvim_win_get_buf = previous_win_get_buf + renderer.attach = previous_attach + renderer.queue = previous_queue + renderer.suppression_status = previous_suppression + renderer.set_suppressed = previous_set_suppressed + ui.has_popup_or_floating_windows = previous_has_popup + vim.fn.getcmdtype = previous_getcmdtype + + assert.are.same({}, queued) + end) + it("uses the lightweight refresh path on WinScrolled", function() local previous_scroll = renderer.scroll local previous_queue = renderer.queue @@ -1668,8 +1880,10 @@ describe("render_latex.setup", function() end local buf = vim.api.nvim_create_buf(false, true) + vim.api.nvim_set_current_buf(buf) vim.bo[buf].filetype = "markdown" render_latex.setup({ install = { auto = false } }) + queued = {} vim.api.nvim_exec_autocmds("WinScrolled", { buffer = buf, modeline = false }) @@ -1710,8 +1924,10 @@ describe("render_latex.setup", function() end local buf = vim.api.nvim_create_buf(false, true) + vim.api.nvim_set_current_buf(buf) vim.bo[buf].filetype = "markdown" render_latex.setup({ install = { auto = false } }) + queued = {} vim.api.nvim_exec_autocmds("BufEnter", { buffer = buf, modeline = false }) @@ -2600,6 +2816,37 @@ describe("render_latex.worker", function() end) describe("render_latex.renderer", function() + it("does not run whole-buffer inline fallback for inactive-tab buffers", function() + config.setup({ render_modes = { vim.api.nvim_get_mode().mode } }) + local previous_inline = detect.inline + local previous_inline_ranges = detect.inline_ranges + local inline_count = 0 + local inline_ranges_count = 0 + + detect.inline = function() + inline_count = inline_count + 1 + return {} + end + detect.inline_ranges = function(_, ranges) + inline_ranges_count = inline_ranges_count + 1 + assert.are.same({}, ranges) + return {} + end + + local buf = vim.api.nvim_create_buf(true, true) + vim.bo[buf].filetype = "markdown" + vim.api.nvim_buf_set_lines(buf, 0, -1, false, { "inline $x$", "after" }) + + renderer.render(buf) + + detect.inline = previous_inline + detect.inline_ranges = previous_inline_ranges + config.setup() + + assert.are.equal(0, inline_count) + assert.are.equal(1, inline_ranges_count) + end) + local function with_jupynvim_render(opts, assertions) opts = opts or {} local previous_notebook = package.loaded["jupynvim.notebook"] @@ -3963,19 +4210,24 @@ end) describe("render_latex.viewport", function() it("computes each window range once when filtering visible equations", function() - local previous_win_findbuf = vim.fn.win_findbuf + local previous_tabpage_list_wins = vim.api.nvim_tabpage_list_wins local previous_win_is_valid = vim.api.nvim_win_is_valid + local previous_win_get_config = vim.api.nvim_win_get_config + local previous_win_get_buf = vim.api.nvim_win_get_buf local buf = vim.api.nvim_create_buf(false, true) vim.api.nvim_buf_set_lines(buf, 0, -1, false, { "one", "two", "three" }) - vim.fn.win_findbuf = function(target) - if target == buf then - return { 101 } - end - return {} + vim.api.nvim_tabpage_list_wins = function() + return { 101 } end vim.api.nvim_win_is_valid = function(winid) return winid == 101 end + vim.api.nvim_win_get_config = function() + return { relative = "" } + end + vim.api.nvim_win_get_buf = function() + return buf + end local previous_viewport_range = viewport.viewport_range local calls = 0 @@ -3986,8 +4238,10 @@ describe("render_latex.viewport", function() local ok, ranges = pcall(viewport.viewport_ranges, buf, {}, 0) viewport.viewport_range = previous_viewport_range - vim.fn.win_findbuf = previous_win_findbuf + vim.api.nvim_tabpage_list_wins = previous_tabpage_list_wins vim.api.nvim_win_is_valid = previous_win_is_valid + vim.api.nvim_win_get_config = previous_win_get_config + vim.api.nvim_win_get_buf = previous_win_get_buf assert.is_true(ok) assert.are.equal(1, calls) @@ -3996,21 +4250,26 @@ describe("render_latex.viewport", function() end) it("hides equations inside closed folds from visible equation filtering", function() - local previous_win_findbuf = vim.fn.win_findbuf + local previous_tabpage_list_wins = vim.api.nvim_tabpage_list_wins local previous_win_is_valid = vim.api.nvim_win_is_valid + local previous_win_get_config = vim.api.nvim_win_get_config + local previous_win_get_buf = vim.api.nvim_win_get_buf local previous_viewport_range = viewport.viewport_range local previous_fold_closed = viewport.fold_closed local buf = vim.api.nvim_create_buf(false, true) - vim.fn.win_findbuf = function(target) - if target == buf then - return { 101 } - end - return {} + vim.api.nvim_tabpage_list_wins = function() + return { 101 } end vim.api.nvim_win_is_valid = function(winid) return winid == 101 end + vim.api.nvim_win_get_config = function() + return { relative = "" } + end + vim.api.nvim_win_get_buf = function() + return buf + end viewport.viewport_range = function() return 0, 10 end @@ -4023,8 +4282,10 @@ describe("render_latex.viewport", function() { start_row = 5, end_row = 6, key = "visible" }, }, {}, 0) - vim.fn.win_findbuf = previous_win_findbuf + vim.api.nvim_tabpage_list_wins = previous_tabpage_list_wins vim.api.nvim_win_is_valid = previous_win_is_valid + vim.api.nvim_win_get_config = previous_win_get_config + vim.api.nvim_win_get_buf = previous_win_get_buf viewport.viewport_range = previous_viewport_range viewport.fold_closed = previous_fold_closed