local log = require("sense.log") local utf8 = require("lua-utf8") local M = {} ---@private ---@param name string ---@param wininfo vim.fn.getwininfo.ret.item ---@param lines string[] ---@param highlights sense.ui.Highlight[] ---@param win_opts vim.api.keyset.win_config function M.restore_and_open_win(name, wininfo, lines, highlights, win_opts) local ok, fwinid = pcall(vim.tbl_get, vim.w, wininfo.winid, name) if not ok then -- TODO: log error about vim.w.__index error here: log.debug("error while reading window variables in", wininfo.winid, fwinid) fwinid = nil end M.ui_guard(function() if #lines == 0 then log.debug("nothing to render, abort") if fwinid and vim.api.nvim_win_is_valid(fwinid) then log.debug("close window:", fwinid) vim.api.nvim_win_close(fwinid, true) end return end local bufnr assert(win_opts.width > 0, "width should be positive at this point") if fwinid and vim.api.nvim_win_is_valid(fwinid) then vim.api.nvim_win_set_config(fwinid, win_opts) bufnr = vim.api.nvim_win_get_buf(fwinid) else fwinid, bufnr = M.open_win_buf(win_opts) vim.w[wininfo.winid][name] = fwinid end M.set_lines(bufnr, lines, highlights) end) end ---Render lines & highlights on buffer ---@param buf number ---@param lines string[] ---@param highlights sense.ui.Highlight[] function M.set_lines(buf, lines, highlights) vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines) for _, highlight in ipairs(highlights) do vim.api.nvim_buf_add_highlight( buf, 0, highlight.hl_group, highlight.line, highlight.col_start, highlight.col_end ) end end ---Fixed version of nvim_open_win ---@param buffer integer ---@param enter boolean ---@param config vim.api.keyset.win_config ---@return integer function M.nvim_open_win(buffer, enter, config) log.debug("utils.nvim_open_win") local win = vim.api.nvim_open_win(buffer, enter, config) if config.relative == "win" then if config.win == 0 then config.win = vim.api.nvim_get_current_win() end -- HACK: close windows when parent window is closed -- WHY IS THIS NOT A DEFAULT BEHAVIOR vim.api.nvim_create_autocmd("WinClosed", { group = vim.api.nvim_create_augroup("sense.fix_nvim_open_win", { clear = false }), callback = function(ev) if ev.match == tostring(config.win) then if vim.api.nvim_win_is_valid(win) then log.debug("close dangling window:", win) vim.api.nvim_win_close(win, true) end return true end end, desc = "Close floating window associated to window when closed", }) end return win end ---@private ---Open window with scratch buffer ---@param win_config vim.api.keyset.win_config function M.open_win_buf(win_config) log.debug("open_win_buf with", win_config) win_config = vim.tbl_deep_extend("force", win_config, { focusable = false, style = "minimal", zindex = 10, }) local buf = vim.api.nvim_create_buf(false, true) vim.bo[buf].buftype = "nofile" local win = M.nvim_open_win(buf, false, win_config) log.debug("window opened") return win, buf end ---@param str string ---@param max_width integer ---@return string, integer function M.truncate_line(str, max_width) local full_width = vim.fn.strdisplaywidth(str) if vim.fn.strdisplaywidth(str) <= max_width then return str, full_width end max_width = max_width - 1 local result = {} full_width = 0 for _, code in utf8.codes(str) do local char = utf8.char(code) local w = vim.fn.strdisplaywidth(char) if full_width + w > max_width then break end table.insert(result, char) full_width = full_width + w end return table.concat(result) .. "…", full_width + 1 end ---Calculate size given as integer or ratio ---@param size number ---@param base integer ---@return integer function M.calc_size_config(size, base) if size < 1 then return math.floor(base * size) else return math.floor(size) end end --- Suppress errors that may occur while render windows. --- --- The E523 error (Not allowed here) happens when 'secure' operations --- (including buffer or window management) are invoked while textlock is held --- or the Neovim UI is blocking. See #68. --- --- Also ignore E11 (Invalid in command-line window), which is thrown when --- Fidget tries to close the window while a command-line window is focused. --- See #136. --- --- This utility provides a workaround to simply supress the error. --- All other errors will be re-thrown. --- --- (Thanks @wookayin and @0xAdk!) --- NOTE(boltless): Copied from fidget.nvim --- ---@param callable fun() ---@return boolean suppressed_error function M.ui_guard(callable) local whitelist = { "E11: Invalid in command%-line window", "E523: Not allowed here", "E565: Not allowed to change", } local ok, err = pcall(callable) if ok then return true end if type(err) ~= "string" then -- Don't know how to deal with this kind of error object error(err) end for _, msg in ipairs(whitelist) do if string.find(err, msg) then return false end end error(err) end return M