local M = {} local fn, api = vim.fn, vim.api function M.is_visual_mode() local mode = api.nvim_get_mode().mode return mode == "v" or mode == "V" or mode == "\x16" end function M.get_char_at(line, byte_col, buf) if not buf or buf == 0 then buf = api.nvim_get_current_buf() end local line_str = fn.getbufoneline(buf, line) local char_idx = fn.charidx(line_str, (byte_col - 1)) local char_nr = fn.strgetchar(line_str, char_idx) if char_nr ~= -1 then return fn.nr2char(char_nr) else return nil end end --- Gets the zero-indexed lines from the given buffer. --- ---@param bufnr integer bufnr to get the lines from ---@param rows integer[] zero-indexed line numbers ---@return table a table mapping rows to lines function M.get_lines(bufnr, rows) rows = type(rows) == "table" and rows or { rows } local lines = {} for _, row in ipairs(rows) do lines[row] = (api.nvim_buf_get_lines(bufnr, row, row + 1, false) or { "" })[1] end return lines end --- Gets the zero-indexed line from the given buffer. --- ---@param bufnr integer ---@param row integer zero-indexed line number ---@return string the line at row in filename function M.get_line(bufnr, row) return M.get_lines(bufnr, { row })[row] end local str_utf16index = fn.has("nvim-0.11") == 1 and function(text, byte_col) return vim.str_utfindex(text, "utf-16", byte_col) end or function(text, byte_col) return select(2, vim.str_utfindex(text, byte_col)) end --- VSCode counts a position in UTF-16 code units, Nvim counts bytes. --- ---@param text string the line the column belongs to ---@param byte_col integer zero-indexed byte column, clamped to `text` ---@return integer zero-indexed UTF-16 column function M.utf16_col(text, byte_col) byte_col = math.max(0, math.min(byte_col, #text)) return byte_col == 0 and 0 or str_utf16index(text, byte_col) end ---@param text string the line the column belongs to ---@param byte_col integer zero-indexed byte column, clamped to `text` ---@return integer zero-indexed byte column just past the character byte_col points into local function byte_after_char(text, byte_col) byte_col = math.max(0, math.min(byte_col, #text)) if byte_col == #text then return byte_col end return byte_col + vim.str_utf_end(text, byte_col + 1) + 1 end ---@param buf integer ---@param line integer one-indexed line ---@param byte_col integer zero-indexed byte column ---@return lsp.Position function M.lsp_position(buf, line, byte_col) return { line = line - 1, character = M.utf16_col(M.get_line(buf, line - 1) or "", byte_col) } end --- Converts an inclusive Nvim byte range to an exclusive LSP range. --- --- The end stops at the line end, so that a linewise range - which passes the whole --- line length as its end column - stays a valid position instead of running past it. --- ---@param buf integer ---@param start_line integer one-indexed ---@param start_col integer zero-indexed byte column ---@param end_line integer one-indexed ---@param end_col integer zero-indexed byte column, inclusive ---@return lsp.Range function M.lsp_range(buf, start_line, start_col, end_line, end_col) local end_text = M.get_line(buf, end_line - 1) or "" if vim.o.selection ~= "exclusive" then -- step over the whole character: adding one UTF-16 unit splits a surrogate pair end_col = byte_after_char(end_text, end_col) end local end_char = M.utf16_col(end_text, end_col) return { start = M.lsp_position(buf, start_line, start_col), ["end"] = { line = end_line - 1, character = end_char }, } end --- Compare two positions ---@param a lsp.Position ---@param b lsp.Position ---@return -1|0|1 -1 if a < b, 0 if a == b, 1 if a > b function M.compare_position(a, b) if a.line > b.line then return 1 end if a.line == b.line and a.character > b.character then return 1 end if a.line == b.line and a.character == b.character then return 0 end return -1 end ---Debounce a function. ---@param func function function to debounce ---@param time number trialing time in ms ---@return function function M.debounce(func, time) local timer return function(...) local args = { ... } if timer and timer:is_active() then timer:close() end timer = vim.defer_fn(function() func(unpack(args)) end, time) end end -- Since Nvim 0.10.0, `virtcol2col` changed from returning the last byte of a -- multi-byte character to returning the first byte. However, we need the column -- of the last byte, which is consistent with the selected region in Nvim. -- See https://github.com/neovim/neovim/issues/29786 if fn.has("nvim-0.10.0") == 0 then M.virtcol2col = fn.virtcol2col else ---@diagnostic disable-next-line: duplicate-set-field M.virtcol2col = function(winid, lnum, virtcol) local byte_idx = fn.virtcol2col(winid, lnum, virtcol) - 1 local buf = api.nvim_win_get_buf(winid) local line = M.get_line(buf, lnum - 1) local char_idx = fn.charidx(line, byte_idx) local prefix = fn.strcharpart(line, 0, char_idx + 1) return #prefix end end -- Wrapper for nvim_set_option_value that sets the option only if the value differs function M.set_option_value(name, value, opts) opts = opts or {} local current = api.nvim_get_option_value(name, opts) if current ~= value then api.nvim_set_option_value(name, value, opts) end end -- Wrapper for set_option_value to set a buffer option function M.buf_set_option(buf, name, value) return M.set_option_value(name, value, { buf = buf }) end -- Wrapper for set_option_value to set a window option function M.win_set_option(win, name, value) return M.set_option_value(name, value, { win = win }) end ---Get a buffer variable safely. Returns nil if the variable does not exist. function M.buf_get_var(buf, name) local ok, value = pcall(api.nvim_buf_get_var, buf, name) if ok then return value end end return M